diff --git a/client/src/App.tsx b/client/src/App.tsx index 3dd68dcd9..ad945072a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,7 @@ import "./index.css"; import { Toaster } from "./ui/components/Toaster"; import { TransactionNotification } from "./ui/components/TxEmit"; +import { WorldLoading } from "./ui/components/WorldLoading"; import { World } from "./ui/layouts/World"; function App({ backgroundImage }: { backgroundImage: string }) { @@ -9,6 +10,7 @@ function App({ backgroundImage }: { backgroundImage: string }) { + ); } diff --git a/client/src/dojo/debouncedQueries.ts b/client/src/dojo/debouncedQueries.ts index 83121766d..0f256ba09 100644 --- a/client/src/dojo/debouncedQueries.ts +++ b/client/src/dojo/debouncedQueries.ts @@ -2,6 +2,7 @@ import { Component, Metadata, Schema } from "@dojoengine/recs"; import { ToriiClient } from "@dojoengine/torii-client"; import debounce from "lodash/debounce"; import { + addArrivalsSubscription, addMarketSubscription, addToSubscription, addToSubscriptionOneKeyModelbyRealmEntityId, @@ -16,8 +17,11 @@ class RequestQueue { private batchSize = 3; // Number of concurrent requests private batchDelayMs = 100; // Delay between batches - async add(request: () => Promise) { - this.queue.push(request); + async add(request: () => Promise, onComplete?: () => void) { + this.queue.push(async () => { + await request(); + onComplete?.(); // Call onComplete after the request is processed + }); if (!this.processing) { this.processing = true; this.processQueue(); @@ -54,8 +58,13 @@ const marketQueue = new RequestQueue(); // Debounced functions that add to queues export const debouncedSyncPosition = debounce( - async (client: ToriiClient, components: Component[], entityID: string) => { - await positionQueue.add(() => syncPosition(client, components, entityID)); + async ( + client: ToriiClient, + components: Component[], + entityID: string, + onComplete?: () => void, + ) => { + await positionQueue.add(() => syncPosition(client, components, entityID), onComplete); }, 100, { leading: true }, // Add leading: true to execute immediately on first call @@ -66,8 +75,12 @@ export const debouncedAddToSubscriptionTwoKey = debounce( client: ToriiClient, components: Component[], entityID: string[], + onComplete?: () => void, ) => { - await subscriptionQueue.add(() => addToSubscriptionTwoKeyModelbyRealmEntityId(client, components, entityID)); + await subscriptionQueue.add( + () => addToSubscriptionTwoKeyModelbyRealmEntityId(client, components, entityID), + onComplete, + ); }, 250, { leading: true }, @@ -78,8 +91,25 @@ export const debouncedAddToSubscriptionOneKey = debounce( client: ToriiClient, components: Component[], entityID: string[], + onComplete?: () => void, ) => { - await subscriptionQueue.add(() => addToSubscriptionOneKeyModelbyRealmEntityId(client, components, entityID)); + await subscriptionQueue.add( + () => addToSubscriptionOneKeyModelbyRealmEntityId(client, components, entityID), + onComplete, + ); + }, + 250, + { leading: true }, +); + +export const debounceAddResourceArrivals = debounce( + async ( + client: ToriiClient, + components: Component[], + entityID: number[], + onComplete?: () => void, + ) => { + await subscriptionQueue.add(() => addArrivalsSubscription(client, components, entityID), onComplete); }, 250, { leading: true }, @@ -91,16 +121,21 @@ export const debouncedAddToSubscription = debounce( components: Component[], entityID: string[], position?: { x: number; y: number }[], + onComplete?: () => void, ) => { - await subscriptionQueue.add(() => addToSubscription(client, components, entityID, position)); + await subscriptionQueue.add(() => addToSubscription(client, components, entityID, position), onComplete); }, 250, { leading: true }, ); export const debouncedAddMarketSubscription = debounce( - async (client: ToriiClient, components: Component[]) => { - await marketQueue.add(() => addMarketSubscription(client, components)); + async ( + client: ToriiClient, + components: Component[], + onComplete?: () => void, + ) => { + await marketQueue.add(() => addMarketSubscription(client, components), onComplete); }, 500, { leading: true }, diff --git a/client/src/dojo/queries.ts b/client/src/dojo/queries.ts index 9dff82a6d..5ba5e37f3 100644 --- a/client/src/dojo/queries.ts +++ b/client/src/dojo/queries.ts @@ -136,10 +136,11 @@ export const addMarketSubscription = async ( await getEntities( client, { - Keys: { - keys: [undefined, undefined], - pattern_matching: "FixedLen", - models: ["s0_eternum-DetachedResource"], + Member: { + model: "s0_eternum-DetachedResource", + member: "resource_amount", + operator: "Gt", + value: { Primitive: { U128: "0" } }, }, }, components, @@ -151,3 +152,72 @@ export const addMarketSubscription = async ( const end = performance.now(); console.log("MarketEnd", end - start); }; + +export const addArrivalsSubscription = async ( + client: ToriiClient, + components: Component[], + entityIds: number[], +) => { + const start = performance.now(); + console.log("ArrivalsEnd: starting resource arrivals"); + await getEntities( + client, + // todo: waiting on ghlim to check issue with this query + // { + // Composite: { + // operator: "And", + // clauses: [ + // { + // Composite: { + // operator: "Or", + // clauses: entityIds.map((id) => ({ + // Member: { + // model: "s0_eternum-EntityOwner", + // member: "entity_owner_id", + // operator: "Eq", + // value: { Primitive: { U32: id } }, + // }, + // })), + // }, + // }, + // { + // Member: { + // model: "s0_eternum-OwnedResourcesTracker", + // member: "resource_types", + // operator: "Neq", + // value: { Primitive: { U256: "0" } }, + // }, + // }, + // ], + // }, + // }, + { + Composite: { + operator: "Or", + clauses: entityIds.map((id) => ({ + Member: { + model: "s0_eternum-EntityOwner", + member: "entity_owner_id", + operator: "Eq", + value: { Primitive: { U32: id } }, + }, + })), + }, + }, + + components, + [], + [ + "s0_eternum-Army", + "s0_eternum-Position", + "s0_eternum-EntityOwner", + "s0_eternum-Weight", + "s0_eternum-OwnedResourcesTracker", + "s0_eternum-ArrivalTime", + ], + 1000, + false, + ); + const end = performance.now(); + console.log("ArrivalsEnd", end - start); +}; diff --git a/client/src/dojo/setup.ts b/client/src/dojo/setup.ts index 03f4befd8..9fc3801b4 100644 --- a/client/src/dojo/setup.ts +++ b/client/src/dojo/setup.ts @@ -1,3 +1,5 @@ +import { AppStore } from "@/hooks/store/useUIStore"; +import { LoadingStateKey } from "@/hooks/store/useWorldLoading"; import { BUILDING_CATEGORY_POPULATION_CONFIG_ID, HYPERSTRUCTURE_CONFIG_ID, @@ -71,10 +73,11 @@ export const syncEntitiesDebounced = async ( }; }; -export async function setup({ ...config }: DojoConfig) { +export async function setup(config: DojoConfig & { state: AppStore }) { const network = await setupNetwork(config); const components = createClientComponents(network); const systemCalls = createSystemCalls(network); + const setLoading = config.state.setLoading; const configClauses: Clause[] = [ { @@ -114,13 +117,37 @@ export async function setup({ ...config }: DojoConfig) { }, ]; - await getEntities( - network.toriiClient, - { Composite: { operator: "Or", clauses: configClauses } }, - network.contractComponents as any, - ); + setLoading(LoadingStateKey.Config, true); + try { + await Promise.all([ + getEntities( + network.toriiClient, + { Composite: { operator: "Or", clauses: configClauses } }, + network.contractComponents as any, + ), + getEntities( + network.toriiClient, + { + Keys: { + keys: [undefined, undefined], + pattern_matching: "FixedLen", + models: ["s0_eternum-CapacityConfigCategory", "s0_eternum-ResourceCost"], + }, + }, + network.contractComponents as any, + [], + [], + 40_000, + false, + ), + ]); + } finally { + setLoading(LoadingStateKey.Config, false); + } // fetch all existing entities from torii + + setLoading(LoadingStateKey.SingleKey, true); await getEntities( network.toriiClient, { @@ -137,10 +164,8 @@ export async function setup({ ...config }: DojoConfig) { "s0_eternum-BankConfig", "s0_eternum-Bank", "s0_eternum-Trade", - "s0_eternum-Army", "s0_eternum-Structure", "s0_eternum-Battle", - "s0_eternum-EntityOwner", ], }, }, @@ -149,28 +174,15 @@ export async function setup({ ...config }: DojoConfig) { [], 40_000, false, - ); - - await getEntities( - network.toriiClient, - { - Keys: { - keys: [undefined, undefined], - pattern_matching: "FixedLen", - models: ["s0_eternum-CapacityConfigCategory", "s0_eternum-ResourceCost"], - }, - }, - network.contractComponents as any, - [], - [], - 40_000, - false, - ); + ).finally(() => { + setLoading(LoadingStateKey.SingleKey, false); + }); const sync = await syncEntitiesDebounced(network.toriiClient, network.contractComponents as any, [], false); configManager.setDojo(components); + setLoading(LoadingStateKey.Events, true); const eventSync = getEvents( network.toriiClient, network.contractComponents.events as any, @@ -198,7 +210,9 @@ export async function setup({ ...config }: DojoConfig) { }, false, false, - ); + ).finally(() => { + setLoading(LoadingStateKey.Events, false); + }); return { network, diff --git a/client/src/hooks/store/useUIStore.tsx b/client/src/hooks/store/useUIStore.tsx index ff9f5878a..307f1e4bc 100644 --- a/client/src/hooks/store/useUIStore.tsx +++ b/client/src/hooks/store/useUIStore.tsx @@ -10,6 +10,7 @@ import { ThreeStore, createThreeStoreSlice } from "./_threeStore"; import { BattleViewInfo } from "./types"; import { BlockchainStore, createBlockchainStore } from "./useBlockchainStore"; import { RealmStore, createRealmStoreSlice } from "./useRealmStore"; +import { WorldStore, createWorldStoreSlice } from "./useWorldLoading"; type TooltipType = { content: React.ReactNode; @@ -70,7 +71,7 @@ interface UIStore { setShowToS: (show: boolean) => void; } -export type AppStore = UIStore & PopupsStore & ThreeStore & BuildModeStore & RealmStore & BlockchainStore; +export type AppStore = UIStore & PopupsStore & ThreeStore & BuildModeStore & RealmStore & BlockchainStore & WorldStore; const useUIStore = create( subscribeWithSelector((set, get) => ({ @@ -139,6 +140,7 @@ const useUIStore = create( ...createBuildModeStoreSlice(set), ...createRealmStoreSlice(set), ...createBlockchainStore(set), + ...createWorldStoreSlice(set), })), ); diff --git a/client/src/hooks/store/useWorldLoading.tsx b/client/src/hooks/store/useWorldLoading.tsx index f46ace82c..5a1ab77b5 100644 --- a/client/src/hooks/store/useWorldLoading.tsx +++ b/client/src/hooks/store/useWorldLoading.tsx @@ -1,20 +1,52 @@ -// World loading state -import { create } from "zustand"; +/** + * Represents the loading state of different parts of the application. + * Each property indicates whether that part is currently loading data from the blockchain. + */ +export enum LoadingStateKey { + SelectedStructure = "selectedStructure", + Market = "market", + PlayerStructuresOneKey = "playerStructuresOneKey", + PlayerStructuresTwoKey = "playerStructuresTwoKey", + Arrivals = "arrivals", + Map = "map", + Bank = "bank", + World = "world", + Hyperstructure = "hyperstructure", + SingleKey = "singleKey", + Config = "config", + Events = "events", +} + +export type LoadingState = { + [key in LoadingStateKey]: boolean; +}; -interface WorldState { - isWorldLoading: boolean; - isMarketLoading: boolean; - isStructuresLoading: boolean; - setWorldLoading: (loading: boolean) => void; - setMarketLoading: (loading: boolean) => void; - setStructuresLoading: (loading: boolean) => void; +export interface WorldStore { + loadingStates: LoadingState; + setLoading: (key: LoadingStateKey, value: boolean) => void; } -export const useWorldStore = create((set) => ({ - isWorldLoading: true, - isMarketLoading: true, - isStructuresLoading: true, - setWorldLoading: (loading: boolean) => set({ isWorldLoading: loading }), - setMarketLoading: (loading: boolean) => set({ isMarketLoading: loading }), - setStructuresLoading: (loading: boolean) => set({ isStructuresLoading: loading }), -})); +export const createWorldStoreSlice = (set: any) => ({ + loadingStates: { + [LoadingStateKey.SelectedStructure]: false, + [LoadingStateKey.Market]: false, + [LoadingStateKey.PlayerStructuresOneKey]: false, + [LoadingStateKey.PlayerStructuresTwoKey]: false, + [LoadingStateKey.Arrivals]: false, + [LoadingStateKey.Map]: false, + [LoadingStateKey.Bank]: false, + [LoadingStateKey.World]: false, + [LoadingStateKey.Hyperstructure]: false, + [LoadingStateKey.SingleKey]: false, + [LoadingStateKey.Config]: false, + [LoadingStateKey.Events]: false, + }, + + setLoading: (key: LoadingStateKey, value: boolean) => + set((state: WorldStore) => ({ + loadingStates: { + ...state.loadingStates, + [key]: value, + }, + })), +}); diff --git a/client/src/main.tsx b/client/src/main.tsx index 4d97dc4ce..89f685c96 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -13,6 +13,7 @@ import App from "./App"; import { setup } from "./dojo/setup"; import { DojoProvider } from "./hooks/context/DojoContext"; import { StarknetProvider } from "./hooks/context/starknet-provider"; +import useUIStore from "./hooks/store/useUIStore"; import "./index.css"; import GameRenderer from "./three/GameRenderer"; import { PWAUpdatePopup } from "./ui/components/pwa-update-popup"; @@ -62,8 +63,10 @@ async function init() { root.render(); + const state = useUIStore.getState(); + const setupStart = performance.now(); - const setupResult = await setup(dojoConfig); + const setupResult = await setup({ state, ...dojoConfig }); const setupEnd = performance.now(); console.log("SetupEnd", setupEnd - setupStart); diff --git a/client/src/three/scenes/Worldmap.ts b/client/src/three/scenes/Worldmap.ts index 6af004669..734aa3eec 100644 --- a/client/src/three/scenes/Worldmap.ts +++ b/client/src/three/scenes/Worldmap.ts @@ -2,6 +2,7 @@ import { ArmyMovementManager, TravelPaths } from "@/dojo/modelManager/ArmyMoveme import { TileManager } from "@/dojo/modelManager/TileManager"; import { SetupResult } from "@/dojo/setup"; import useUIStore from "@/hooks/store/useUIStore"; +import { LoadingStateKey } from "@/hooks/store/useWorldLoading"; import { soundSelector } from "@/hooks/useUISound"; import { HexPosition, SceneName } from "@/types"; import { Position } from "@/types/Position"; @@ -675,6 +676,7 @@ export default class WorldmapScene extends HexagonScene { // Create a unique key for this chunk range const chunkKey = `${startCol - range},${startCol + range},${startRow - range},${startRow + range}`; + console.log({ chunkKey }); // Skip if we've already fetched this chunk if (this.fetchedChunks.has(chunkKey)) { @@ -687,6 +689,7 @@ export default class WorldmapScene extends HexagonScene { console.log(startCol, startRow, range); try { + this.state.setLoading(LoadingStateKey.Map, true); const promiseTiles = getEntities( this.dojo.network.toriiClient, { @@ -734,62 +737,62 @@ export default class WorldmapScene extends HexagonScene { 1000, false, ); - // const promisePositions = getEntities( - // this.dojo.network.toriiClient, - // { - // Composite: { - // operator: "And", - // clauses: [ - // { - // Composite: { - // operator: "And", - // clauses: [ - // { - // Member: { - // model: "s0_eternum-Position", - // member: "x", - // operator: "Gte", - // value: { Primitive: { U32: startCol - range } }, - // }, - // }, - // { - // Member: { - // model: "s0_eternum-Position", - // member: "x", - // operator: "Lte", - // value: { Primitive: { U32: startCol + range } }, - // }, - // }, - // { - // Member: { - // model: "s0_eternum-Position", - // member: "y", - // operator: "Gte", - // value: { Primitive: { U32: startRow - range } }, - // }, - // }, - // { - // Member: { - // model: "s0_eternum-Position", - // member: "y", - // operator: "Lte", - // value: { Primitive: { U32: startRow + range } }, - // }, - // }, - // ], - // }, - // }, - // ], - // }, - // }, - // this.dojo.network.contractComponents as any, - // [], - // ["s0_eternum-Tile"], - // 1000, - // false, - // ); - Promise.all([promiseTiles]).then(([tiles]) => { - // console.log(tiles, positions); + const promisePositions = getEntities( + this.dojo.network.toriiClient, + { + Composite: { + operator: "And", + clauses: [ + { + Member: { + model: "s0_eternum-Position", + member: "x", + operator: "Gte", + value: { Primitive: { U32: startCol - range } }, + }, + }, + { + Member: { + model: "s0_eternum-Position", + member: "x", + operator: "Lte", + value: { Primitive: { U32: startCol + range } }, + }, + }, + { + Member: { + model: "s0_eternum-Position", + member: "y", + operator: "Gte", + value: { Primitive: { U32: startRow - range } }, + }, + }, + { + Member: { + model: "s0_eternum-Position", + member: "y", + operator: "Lte", + value: { Primitive: { U32: startRow + range } }, + }, + }, + ], + }, + }, + this.dojo.network.contractComponents as any, + [], + [ + "s0_eternum-Army", + "s0_eternum-Position", + "s0_eternum-Health", + "s0_eternum-EntityOwner", + "s0_eternum-Protectee", + "s0_eternum-Stamina", + ], + 1000, + false, + ); + Promise.all([promiseTiles, promisePositions]).then(() => { + this.state.setLoading(LoadingStateKey.Map, false); }); } catch (error) { // If there's an error, remove the chunk from cached set so it can be retried diff --git a/client/src/three/systems/SystemManager.ts b/client/src/three/systems/SystemManager.ts index 4c819ace5..8b5afe1c5 100644 --- a/client/src/three/systems/SystemManager.ts +++ b/client/src/three/systems/SystemManager.ts @@ -87,6 +87,7 @@ export class SystemManager { isComponentUpdate(update, this.setup.components.Health) ) { const army = getComponentValue(this.setup.components.Army, update.entity); + if (!army) return; const position = getComponentValue(this.setup.components.Position, update.entity); diff --git a/client/src/ui/components/WorldLoading.tsx b/client/src/ui/components/WorldLoading.tsx new file mode 100644 index 000000000..71d753486 --- /dev/null +++ b/client/src/ui/components/WorldLoading.tsx @@ -0,0 +1,46 @@ +import useUIStore from "@/hooks/store/useUIStore"; +import { LoadingStateKey } from "@/hooks/store/useWorldLoading"; + +export const WorldLoading = () => { + const loadingStates = useUIStore((state) => state.loadingStates); + + const anyLoading = Object.values(loadingStates).some((isLoading) => isLoading); + + const getLoadingItems = () => { + const items = []; + if (loadingStates[LoadingStateKey.SelectedStructure]) items.push("Selected Structure"); + if (loadingStates[LoadingStateKey.Market]) items.push("Market"); + if (loadingStates[LoadingStateKey.PlayerStructuresOneKey] || loadingStates[LoadingStateKey.PlayerStructuresTwoKey]) + items.push("Player Structures"); + if (loadingStates[LoadingStateKey.Arrivals]) items.push("Arrivals"); + if (loadingStates[LoadingStateKey.Map]) items.push("Map"); + if (loadingStates[LoadingStateKey.Bank]) items.push("Bank"); + if (loadingStates[LoadingStateKey.World]) items.push("World"); + if (loadingStates[LoadingStateKey.Hyperstructure]) items.push("Hyperstructure"); + if (loadingStates[LoadingStateKey.SingleKey]) items.push("Single Key"); + if (loadingStates[LoadingStateKey.Config]) items.push("Config"); + if (loadingStates[LoadingStateKey.Events]) items.push("Events"); + return items.join(", "); + }; + + return ( +
+ {anyLoading && ( +
+ +
Loading: {getLoadingItems()}
+
+ )} +
+ ); +}; diff --git a/client/src/ui/components/trading/MarketModal.tsx b/client/src/ui/components/trading/MarketModal.tsx index b46a2e1ca..aafb821e3 100644 --- a/client/src/ui/components/trading/MarketModal.tsx +++ b/client/src/ui/components/trading/MarketModal.tsx @@ -15,7 +15,6 @@ import { useSetMarket } from "@/hooks/helpers/useTrade"; import useMarketStore from "@/hooks/store/useMarketStore"; import { useModalStore } from "@/hooks/store/useModalStore"; import useUIStore from "@/hooks/store/useUIStore"; -import { useWorldStore } from "@/hooks/store/useWorldLoading"; import { BuildingThumbs } from "@/ui/config"; import CircleButton from "@/ui/elements/CircleButton"; import { LoadingAnimation } from "@/ui/elements/LoadingAnimation"; @@ -61,8 +60,6 @@ export const MarketModal = () => { const bank = banks.length === 1 ? banks[0] : null; const battles = useBattlesByPosition(bank?.position || { x: 0, y: 0 }); - const isMarketLoading = useWorldStore((state) => state.isMarketLoading); - const currentBlockTimestamp = useUIStore.getState().nextBlockTimestamp || 0; const getStructure = useStructureByPosition(); @@ -342,14 +339,6 @@ export const MarketModal = () => { - {isMarketLoading && ( -
-
-
- Syncing market data... -
-
- )} ); }; diff --git a/client/src/ui/components/trading/ResourceArrivals.tsx b/client/src/ui/components/trading/ResourceArrivals.tsx index 346ffd570..a1a86b841 100644 --- a/client/src/ui/components/trading/ResourceArrivals.tsx +++ b/client/src/ui/components/trading/ResourceArrivals.tsx @@ -1,13 +1,17 @@ import { addToSubscription } from "@/dojo/queries"; import { useDojo } from "@/hooks/context/DojoContext"; import { ArrivalInfo } from "@/hooks/helpers/use-resource-arrivals"; +import useNextBlockTimestamp from "@/hooks/useNextBlockTimestamp"; +import Button from "@/ui/elements/Button"; +import { Checkbox } from "@/ui/elements/Checkbox"; import { Headline } from "@/ui/elements/Headline"; import { HintModalButton } from "@/ui/elements/HintModalButton"; -import { memo, useEffect } from "react"; +import { memo, useEffect, useState } from "react"; import { create } from "zustand"; import { EntityArrival } from "../entities/Entity"; import { HintSection } from "../hints/HintModal"; +const DISPLAYED_ARRIVALS = 3; interface SubscribedIdsStore { subscribedIds: Set; addSubscribedIds: (ids: string[]) => void; @@ -24,6 +28,10 @@ const useSubscribedIdsStore = create((set) => ({ export const AllResourceArrivals = memo( ({ arrivals, className = "" }: { arrivals: ArrivalInfo[]; className?: string }) => { const dojo = useDojo(); + const [displayCount, setDisplayCount] = useState(DISPLAYED_ARRIVALS); + const [showOnlyArrived, setShowOnlyArrived] = useState(true); + + const { nextBlockTimestamp } = useNextBlockTimestamp(); const { subscribedIds, addSubscribedIds } = useSubscribedIdsStore(); useEffect(() => { @@ -45,6 +53,17 @@ export const AllResourceArrivals = memo( console.log("AddToSubscriptionStart - 5"); }, [arrivals, subscribedIds, addSubscribedIds]); + const filteredArrivals = showOnlyArrived + ? arrivals.filter((arrival) => arrival.arrivesAt < nextBlockTimestamp) + : arrivals; + + const displayedArrivals = filteredArrivals.slice(0, displayCount); + const hasMore = displayCount < filteredArrivals.length; + + const loadMore = () => { + setDisplayCount((prev) => Math.min(prev + DISPLAYED_ARRIVALS, filteredArrivals.length)); + }; + return (
@@ -53,9 +72,22 @@ export const AllResourceArrivals = memo(
- {arrivals.map((arrival) => ( +
+ +
+ {displayedArrivals.map((arrival) => ( ))} + {hasMore && ( +
+ +
+ )}
); }, diff --git a/client/src/ui/components/worldmap/armies/SelectedArmy.tsx b/client/src/ui/components/worldmap/armies/SelectedArmy.tsx index 4897a64fe..bfb023583 100644 --- a/client/src/ui/components/worldmap/armies/SelectedArmy.tsx +++ b/client/src/ui/components/worldmap/armies/SelectedArmy.tsx @@ -2,7 +2,6 @@ import { useOwnArmiesByPosition } from "@/hooks/helpers/useArmies"; import { useEntities } from "@/hooks/helpers/useEntities"; import { useQuery } from "@/hooks/helpers/useQuery"; import useUIStore from "@/hooks/store/useUIStore"; -import { useWorldStore } from "@/hooks/store/useWorldLoading"; import { Position } from "@/types/Position"; import { ArmyChip } from "@/ui/components/military/ArmyChip"; import { InventoryResources } from "@/ui/components/resources/InventoryResources"; @@ -15,8 +14,6 @@ export const SelectedArmy = () => { const updateSelectedEntityId = useUIStore((state) => state.updateSelectedEntityId); const { isMapView } = useQuery(); - const isWorldLoading = useWorldStore((state) => state.isWorldLoading); - const [selectedArmyIndex, setSelectedArmyIndex] = useState(0); useEffect(() => { @@ -66,52 +63,34 @@ export const SelectedArmy = () => { const showTooltip = selectedHex && ownArmy && isMapView; return ( - <> -
- {isWorldLoading && ( -
- -
World state is loading...
-
- )} -
-
- {showTooltip && ( -
- {userArmies.length > 1 && ( -
-
Press Tab to cycle through armies
-
- Army {selectedArmyIndex + 1}/{userArmies.length} -
+ > + {showTooltip && ( +
+ {userArmies.length > 1 && ( +
+
Press Tab to cycle through armies
+
+ Army {selectedArmyIndex + 1}/{userArmies.length}
- )} - - - -
- )} -
- +
+ )} + + + +
+ )} +
); }; diff --git a/client/src/ui/layouts/World.tsx b/client/src/ui/layouts/World.tsx index 11b3e228b..f55e349d2 100644 --- a/client/src/ui/layouts/World.tsx +++ b/client/src/ui/layouts/World.tsx @@ -4,6 +4,7 @@ import { Redirect } from "wouter"; import useUIStore from "../../hooks/store/useUIStore"; import { + debounceAddResourceArrivals, debouncedAddMarketSubscription, debouncedAddToSubscription, debouncedAddToSubscriptionOneKey, @@ -12,7 +13,7 @@ import { useDojo } from "@/hooks/context/DojoContext"; import { PlayerStructure, useEntities } from "@/hooks/helpers/useEntities"; import { useStructureEntityId } from "@/hooks/helpers/useStructureEntityId"; import { useFetchBlockchainData } from "@/hooks/store/useBlockchainStore"; -import { useWorldStore } from "@/hooks/store/useWorldLoading"; +import { LoadingStateKey } from "@/hooks/store/useWorldLoading"; import { ADMIN_BANK_ENTITY_ID } from "@bibliothecadao/eternum"; import { getComponentValue } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; @@ -102,10 +103,7 @@ export const World = ({ backgroundImage }: { backgroundImage: string }) => { useStructureEntityId(); // We could optimise this deeper.... - - const worldLoading = useWorldStore((state) => state.isWorldLoading); - const setWorldLoading = useWorldStore((state) => state.setWorldLoading); - const setMarketLoading = useWorldStore((state) => state.setMarketLoading); + const setLoading = useUIStore((state) => state.setLoading); const dojo = useDojo(); const structureEntityId = useUIStore((state) => state.structureEntityId); @@ -133,7 +131,6 @@ export const World = ({ backgroundImage }: { backgroundImage: string }) => { getEntityIdFromKeys([BigInt(structureEntityId)]), ); - setWorldLoading(true); setSubscriptions((prev) => ({ ...prev, [structureEntityId.toString()]: true, @@ -141,44 +138,86 @@ export const World = ({ backgroundImage }: { backgroundImage: string }) => { ...Object.fromEntries(filteredStructures.map((structure) => [structure.entity_id.toString(), true])), })); - console.log("AddToSubscriptionStart - 1"); - debouncedAddToSubscription( - dojo.network.toriiClient, - dojo.network.contractComponents as any, - [structureEntityId.toString()], - [{ x: position?.x || 0, y: position?.y || 0 }], - ).finally(() => setWorldLoading(false)); + setLoading(LoadingStateKey.SelectedStructure, true); + const fetch = async () => { + console.log("AddToSubscriptionStart - 1"); + try { + await Promise.all([ + debouncedAddToSubscription( + dojo.network.toriiClient, + dojo.network.contractComponents as any, + [structureEntityId.toString()], + [{ x: position?.x || 0, y: position?.y || 0 }], + () => setLoading(LoadingStateKey.SelectedStructure, false), + ), + ]); + } catch (error) { + console.error("Fetch failed", error); + } + }; + + fetch(); }, [structureEntityId]); useEffect(() => { - if (filteredStructures.length === 0) return; - - setWorldLoading(true); - console.log("AddToSubscriptionStart - 2"); + const fetch = async () => { + setLoading(LoadingStateKey.PlayerStructuresOneKey, true); + setLoading(LoadingStateKey.PlayerStructuresTwoKey, true); + setLoading(LoadingStateKey.Arrivals, true); + + const isSyncing = true; + + try { + console.log("AddToSubscriptionStart - 2"); + await Promise.all([ + debouncedAddToSubscription( + dojo.network.toriiClient, + dojo.network.contractComponents as any, + [...filteredStructures.map((structure) => structure.entity_id.toString())], + [...filteredStructures.map((structure) => ({ x: structure.position.x, y: structure.position.y }))], + () => setLoading(LoadingStateKey.PlayerStructuresOneKey, false), + ), + debouncedAddToSubscriptionOneKey( + dojo.network.toriiClient, + dojo.network.contractComponents as any, + [...filteredStructures.map((structure) => structure.entity_id.toString())], + () => setLoading(LoadingStateKey.PlayerStructuresTwoKey, false), + ), + ]); + + await debounceAddResourceArrivals( + dojo.network.toriiClient, + dojo.network.contractComponents as any, + [...structures.map((structure) => structure.entity_id)], + () => setLoading(LoadingStateKey.Arrivals, false), + ); + } catch (error) { + console.error("Fetch failed", error); + } + }; + + fetch(); + }, [structures.length]); - Promise.all([ + useEffect(() => { + try { + setLoading(LoadingStateKey.Market, true); + setLoading(LoadingStateKey.Bank, true); + console.log("AddToSubscriptionStart - 3"); debouncedAddToSubscription( dojo.network.toriiClient, dojo.network.contractComponents as any, - [...filteredStructures.map((structure) => structure.entity_id.toString())], - [...filteredStructures.map((structure) => ({ x: structure.position.x, y: structure.position.y }))], - ), - debouncedAddToSubscriptionOneKey(dojo.network.toriiClient, dojo.network.contractComponents as any, [ - ...filteredStructures.map((structure) => structure.entity_id.toString()), - ]), - ]).finally(() => setWorldLoading(false)); - }, [structures.length]); - - useEffect(() => { - setMarketLoading(true); - console.log("AddToSubscriptionStart - 3"); - - Promise.all([ - debouncedAddToSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, [ - ADMIN_BANK_ENTITY_ID.toString(), - ]), - debouncedAddMarketSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any), - ]).finally(() => setMarketLoading(false)); + [ADMIN_BANK_ENTITY_ID.toString()], + [], + () => setLoading(LoadingStateKey.Bank, false), + ); + + debouncedAddMarketSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, () => + setLoading(LoadingStateKey.Market, false), + ); + } catch (error) { + console.error("Fetch failed", error); + } }, []); const battleViewContent = useMemo( diff --git a/client/src/ui/modules/navigation/QuestMenu.tsx b/client/src/ui/modules/navigation/QuestMenu.tsx index 2b86307ff..bc34b4c72 100644 --- a/client/src/ui/modules/navigation/QuestMenu.tsx +++ b/client/src/ui/modules/navigation/QuestMenu.tsx @@ -2,7 +2,6 @@ import { useDojo } from "@/hooks/context/DojoContext"; import { Prize, QuestStatus, useQuests, useUnclaimedQuestsCount } from "@/hooks/helpers/useQuests"; import { useRealm } from "@/hooks/helpers/useRealm"; import useUIStore from "@/hooks/store/useUIStore"; -import { useWorldStore } from "@/hooks/store/useWorldLoading"; import { useStartingTutorial } from "@/hooks/use-starting-tutorial"; import { questSteps, useTutorial } from "@/hooks/use-tutorial"; import Button from "@/ui/elements/Button"; @@ -19,7 +18,7 @@ export const QuestsMenu = memo(() => { }, } = useDojo(); - const worldLoading = useWorldStore((state) => state.isWorldLoading); + const questsLoaded = useUIStore((state) => state.loadingStates.playerStructuresTwoKey); useStartingTutorial(); @@ -34,7 +33,6 @@ export const QuestsMenu = memo(() => { const { handleStart } = useTutorial(questSteps.get(currentQuest?.id || QuestType.Settle)); - const isWorldLoading = useWorldStore((state) => state.isWorldLoading); const { unclaimedQuestsCount } = useUnclaimedQuestsCount(); const [isLoading, setIsLoading] = useState(false); @@ -81,7 +79,7 @@ export const QuestsMenu = memo(() => { }; const handleClaimMouseEnter = (e: React.MouseEvent) => { - if (worldLoading) return; + if (questsLoaded) return; const rect = e.currentTarget.getBoundingClientRect(); const tooltipWidth = 300; @@ -106,11 +104,11 @@ export const QuestsMenu = memo(() => { return ( unclaimedQuestsCount > 0 && - !isWorldLoading && ( + !questsLoaded && (
@@ -136,19 +134,19 @@ export const QuestsMenu = memo(() => { onClick={() => handleStart()} variant="outline" disabled={ - (currentQuest?.status === QuestStatus.Completed && currentQuest.id !== QuestType.Settle) || worldLoading + (currentQuest?.status === QuestStatus.Completed && currentQuest.id !== QuestType.Settle) || questsLoaded } className={clsx("tutorial-selector relative text-sm capitalize", { "!border-gold/70 !text-brown !bg-gold hover:!bg-gold/70 animate-pulse hover:animate-none": - currentQuest?.status !== QuestStatus.Completed && !worldLoading, + currentQuest?.status !== QuestStatus.Completed && !questsLoaded, })} > - {worldLoading ? "Loading..." : currentQuest?.name} + {questsLoaded ? "Loading..." : currentQuest?.name}
@@ -166,25 +164,25 @@ export const QuestsMenu = memo(() => { className="text-sm font-semibold capitalize" onClick={handleClaimAllQuests} variant="red" - disabled={worldLoading} + disabled={questsLoaded} > - {worldLoading ? "Loading..." : "Skip All Quests"} + {questsLoaded ? "Loading..." : "Skip All Quests"}
) : ( @@ -192,9 +190,9 @@ export const QuestsMenu = memo(() => { variant="primary" className="text-sm font-semibold capitalize w-6" onClick={() => setSkipQuest(true)} - disabled={worldLoading} + disabled={questsLoaded} > - {worldLoading ? "..." : "Skip"} + {questsLoaded ? "..." : "Skip"} )}
diff --git a/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx b/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx index 7473c53e0..22e5a2d58 100644 --- a/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx +++ b/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx @@ -7,7 +7,8 @@ import { useFragmentMines } from "@/hooks/helpers/useFragmentMines"; import { useGuilds } from "@/hooks/helpers/useGuilds"; import { useHyperstructureProgress, useHyperstructures } from "@/hooks/helpers/useHyperstructures"; import { useResourceBalance } from "@/hooks/helpers/useResources"; -import { useWorldStore } from "@/hooks/store/useWorldLoading"; +import useUIStore from "@/hooks/store/useUIStore"; +import { LoadingStateKey } from "@/hooks/store/useWorldLoading"; import { FragmentMinePanel } from "@/ui/components/fragmentMines/FragmentMinePanel"; import { HintSection } from "@/ui/components/hints/HintModal"; import { DisplayedAccess, HyperstructurePanel } from "@/ui/components/hyperstructures/HyperstructurePanel"; @@ -33,18 +34,16 @@ export const WorldStructuresMenu = ({ className }: { className?: string }) => { network: { toriiClient, contractComponents }, } = useDojo(); - const isStructuresLoading = useWorldStore((state) => state.isStructuresLoading); - const setStructuresLoading = useWorldStore((state) => state.setStructuresLoading); + const hyperstructuresLoaded = useUIStore((state) => state.loadingStates.hyperstructure); + const setLoading = useUIStore((state) => state.setLoading); useEffect(() => { const fetchData = async () => { try { - await fetchHyperstructureData( - toriiClient, - contractComponents as any, - isStructuresLoading, - setStructuresLoading, - ); + setLoading(LoadingStateKey.Hyperstructure, false), + await fetchHyperstructureData(toriiClient, contractComponents as any, hyperstructuresLoaded, () => + setLoading(LoadingStateKey.Hyperstructure, true), + ); } catch (error) { console.error("Failed to fetch hyperstructure data:", error); } @@ -142,7 +141,7 @@ export const WorldStructuresMenu = ({ className }: { className?: string }) => { [selectedTab, hyperstructures, fragmentMines, showOnlyMine, account.address, myHyperstructures], ); - if (isStructuresLoading) { + if (hyperstructuresLoaded) { return (
Loading structures...
@@ -347,7 +346,7 @@ const fetchHyperstructureData = async ( client: ToriiClient, components: Component[], isStructuresLoading: boolean, - setStructuresLoading: (loading: boolean) => void, + onCompleted?: () => void, ) => { if (!isStructuresLoading) { return; @@ -383,6 +382,6 @@ const fetchHyperstructureData = async ( 40_000, false, ).finally(() => { - setStructuresLoading(false); + onCompleted?.(); }); };