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 new file mode 100644 index 000000000..58558184e --- /dev/null +++ b/client/src/dojo/debouncedQueries.ts @@ -0,0 +1,163 @@ +import { Component, Metadata, Schema } from "@dojoengine/recs"; +import { ToriiClient } from "@dojoengine/torii-client"; +import debounce from "lodash/debounce"; +import { + addArrivalsSubscription, + addHyperstructureSubscription, + addMarketSubscription, + addToSubscription, + addToSubscriptionOneKeyModelbyRealmEntityId, + addToSubscriptionTwoKeyModelbyRealmEntityId, + syncPosition, +} from "./queries"; + +// Queue class to manage requests +class RequestQueue { + private queue: Array<() => Promise> = []; + private processing = false; + private batchSize = 3; // Number of concurrent requests + private batchDelayMs = 100; // Delay between batches + + 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(); + } + } + + private async processQueue() { + while (this.queue.length > 0) { + const batch = this.queue.splice(0, this.batchSize); + + try { + await Promise.all(batch.map((request) => request())); + } catch (error) { + console.error("Error processing request batch:", error); + } + + if (this.queue.length > 0) { + // Add delay between batches to prevent overwhelming the server + await new Promise((resolve) => setTimeout(resolve, this.batchDelayMs)); + } + } + this.processing = false; + } + + clear() { + this.queue = []; + } +} + +// Create separate queues for different types of requests +const positionQueue = new RequestQueue(); +const subscriptionQueue = new RequestQueue(); +const marketQueue = new RequestQueue(); +const hyperstructureQueue = new RequestQueue(); +// Debounced functions that add to queues +export const debouncedSyncPosition = debounce( + 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 +); + +export const debouncedAddToSubscriptionTwoKey = debounce( + async ( + client: ToriiClient, + components: Component[], + entityID: string[], + onComplete?: () => void, + ) => { + await subscriptionQueue.add( + () => addToSubscriptionTwoKeyModelbyRealmEntityId(client, components, entityID), + onComplete, + ); + }, + 250, + { leading: true }, +); + +export const debouncedAddToSubscriptionOneKey = debounce( + async ( + client: ToriiClient, + components: Component[], + entityID: string[], + onComplete?: () => void, + ) => { + 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 }, +); + +export const debouncedAddToSubscription = debounce( + async ( + client: ToriiClient, + components: Component[], + entityID: string[], + position?: { x: number; y: number }[], + onComplete?: () => void, + ) => { + await subscriptionQueue.add(() => addToSubscription(client, components, entityID, position), onComplete); + }, + 250, + { leading: true }, +); + +export const debouncedAddMarketSubscription = debounce( + async ( + client: ToriiClient, + components: Component[], + onComplete?: () => void, + ) => { + await marketQueue.add(() => addMarketSubscription(client, components), onComplete); + }, + 500, + { leading: true }, +); + +export const debouncedAddHyperstructureSubscription = debounce( + async ( + client: ToriiClient, + components: Component[], + onComplete?: () => void, + ) => { + await hyperstructureQueue.add(() => addHyperstructureSubscription(client, components), onComplete); + }, + 500, + { leading: true }, +); + +// Utility function to clear all queues if needed +export const clearAllQueues = () => { + positionQueue.clear(); + subscriptionQueue.clear(); + marketQueue.clear(); + hyperstructureQueue.clear(); +}; diff --git a/client/src/dojo/queries.ts b/client/src/dojo/queries.ts index c1620c90e..20f2e334d 100644 --- a/client/src/dojo/queries.ts +++ b/client/src/dojo/queries.ts @@ -25,7 +25,7 @@ export const syncPosition = async ( components, [], [], - 30_000, + 5_000, ); }; @@ -53,7 +53,7 @@ export const addToSubscriptionTwoKeyModelbyRealmEntityId = async ( components as any, [], [], - 30_000, + 5_000, ); const end = performance.now(); console.log("AddToSubscriptionEnd", end - start); @@ -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,110 @@ export const addMarketSubscription = async ( const end = performance.now(); console.log("MarketEnd", end - start); }; + +export const addHyperstructureSubscription = async ( + client: ToriiClient, + components: Component[], +) => { + const start = performance.now(); + await getEntities( + client, + { + Composite: { + operator: "Or", + clauses: [ + { + Keys: { + keys: [undefined, undefined], + pattern_matching: "FixedLen", + models: ["s0_eternum-Epoch", "s0_eternum-Progress"], + }, + }, + { + Keys: { + keys: [undefined, undefined, undefined], + pattern_matching: "FixedLen", + models: ["s0_eternum-Contribution"], + }, + }, + ], + }, + }, + components as any, + [], + [], + 40_000, + false, + ); + const end = performance.now(); + console.log("HyperstructureEnd", 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 29acc548d..25d8fbf31 100644 --- a/client/src/dojo/setup.ts +++ b/client/src/dojo/setup.ts @@ -1,25 +1,83 @@ +import { AppStore } from "@/hooks/store/useUIStore"; +import { LoadingStateKey } from "@/hooks/store/useWorldLoading"; import { BUILDING_CATEGORY_POPULATION_CONFIG_ID, HYPERSTRUCTURE_CONFIG_ID, WORLD_CONFIG_ID, } from "@bibliothecadao/eternum"; import { DojoConfig } from "@dojoengine/core"; -import { getEntities, getEvents } from "@dojoengine/state"; -import { Clause } from "@dojoengine/torii-client"; +import { Component, Metadata, Schema } from "@dojoengine/recs"; +import { getEntities, getEvents, setEntities } from "@dojoengine/state"; +import { Clause, EntityKeysClause, ToriiClient } from "@dojoengine/torii-client"; +import { debounce } from "lodash"; import { createClientComponents } from "./createClientComponents"; import { createSystemCalls } from "./createSystemCalls"; import { ClientConfigManager } from "./modelManager/ConfigManager"; import { setupNetwork } from "./setupNetwork"; -import { setupWorker } from "./worker"; export type SetupResult = Awaited>; export const configManager = ClientConfigManager.instance(); -export async function setup({ ...config }: DojoConfig) { +export const syncEntitiesDebounced = async ( + client: ToriiClient, + components: Component[], + entityKeyClause: EntityKeysClause[], + logging: boolean = true, + historical: boolean = false, +) => { + if (logging) console.log("Starting syncEntities"); + + let entityBatch: Record = {}; + + const debouncedSetEntities = debounce(() => { + if (Object.keys(entityBatch).length > 0) { + // console.log("Applying batch update", entityBatch); + setEntities(entityBatch, components, logging); + entityBatch = {}; // Clear the batch after applying + } + }, 200); // Increased debounce time to 1 second for larger batches + + // Handle entity updates + const entitySub = await client.onEntityUpdated(entityKeyClause, (fetchedEntities: any, data: any) => { + if (logging) console.log("Entity updated", fetchedEntities); + // Merge new data with existing data for this entity + entityBatch[fetchedEntities] = { + ...entityBatch[fetchedEntities], + ...data, + }; + debouncedSetEntities(); + }); + + // Handle event message updates + const eventSub = await client.onEventMessageUpdated( + entityKeyClause, + historical, + (fetchedEntities: any, data: any) => { + if (logging) console.log("Event message updated", fetchedEntities); + // Merge new data with existing data for this entity + entityBatch[fetchedEntities] = { + ...entityBatch[fetchedEntities], + ...data, + }; + debouncedSetEntities(); + }, + ); + + // Return combined subscription that can cancel both + return { + cancel: () => { + entitySub.cancel(); + eventSub.cancel(); + }, + }; +}; + +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[] = [ { @@ -52,20 +110,44 @@ export async function setup({ ...config }: DojoConfig) { }, { Keys: { - keys: [HYPERSTRUCTURE_CONFIG_ID.toString(), undefined], + keys: [HYPERSTRUCTURE_CONFIG_ID.toString()], pattern_matching: "VariableLen", models: [], }, }, ]; - 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, { @@ -82,10 +164,9 @@ 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", + "s0_eternum-Guild", ], }, }, @@ -94,38 +175,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 setupWorker( - { - rpcUrl: config.rpcUrl, - toriiUrl: config.toriiUrl, - relayUrl: config.relayUrl, - worldAddress: config.manifest.world.address || "", - }, - network.contractComponents as any, - [], - 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, @@ -153,7 +211,9 @@ export async function setup({ ...config }: DojoConfig) { }, false, false, - ); + ).finally(() => { + setLoading(LoadingStateKey.Events, false); + }); return { network, diff --git a/client/src/hooks/helpers/use-resource-arrivals.tsx b/client/src/hooks/helpers/use-resource-arrivals.tsx index ef7af8933..4530c7223 100644 --- a/client/src/hooks/helpers/use-resource-arrivals.tsx +++ b/client/src/hooks/helpers/use-resource-arrivals.tsx @@ -1,20 +1,14 @@ import { ContractAddress, ID, Position } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { - Entity, - Has, - HasValue, - NotValue, - defineQuery, - getComponentValue, - isComponentUpdate, - runQuery, -} from "@dojoengine/recs"; +import { Entity, Has, HasValue, NotValue, defineQuery, getComponentValue, isComponentUpdate } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useDojo } from "../context/DojoContext"; import useNextBlockTimestamp from "../useNextBlockTimestamp"; +const DONKEY_RESOURCE_TRACKER = 452312848583266388373324160190187140051835877600158453279131187530910662656n; +const LORDS_RESOURCE_TRACKER = 7237005577332262213973186563042994240829374041602535252466099000494570602496n; + export type ArrivalInfo = { entityId: ID; recipientEntityId: ID; @@ -23,96 +17,90 @@ export type ArrivalInfo = { isOwner: boolean; hasResources: boolean; isHome: boolean; - // resources: Resource[]; +}; + +const getCurrentDonkeyWeightMinimum = () => { + return Number(localStorage.getItem("WEIGHT_MINIMUM") || 0) * 1000; }; const usePlayerArrivals = () => { const { account: { account }, setup: { - components: { Position, Owner, EntityOwner, OwnedResourcesTracker, ArrivalTime, Weight, Resource, Structure }, + components: { Position, Owner, EntityOwner, OwnedResourcesTracker, ArrivalTime, Weight, Structure }, }, } = useDojo(); + const minWeight = getCurrentDonkeyWeightMinimum(); + const playerStructures = useEntityQuery([ Has(Structure), HasValue(Owner, { address: ContractAddress(account.address) }), ]); - const [playerStructurePositions, setPlayerStructurePositions] = useState<(Position & { entityId: ID })[]>([]); - - useEffect(() => { - const positions = playerStructures.map((entityId) => { + const playerStructurePositions = useMemo(() => { + return playerStructures.map((entityId) => { const position = getComponentValue(Position, entityId); return { x: position?.x ?? 0, y: position?.y ?? 0, entityId: position?.entity_id || 0 }; }); - setPlayerStructurePositions(positions); }, [playerStructures, Position]); const [entitiesWithInventory, setEntitiesWithInventory] = useState([]); - const queryFragments = [ - Has(Weight), - Has(ArrivalTime), - Has(EntityOwner), - NotValue(OwnedResourcesTracker, { resource_types: 0n }), - ]; - - const getArrivalsWithResourceOnPosition = useCallback((positions: Position[]) => { - const arrivals = positions.flatMap((position) => { - return Array.from(runQuery([HasValue(Position, { x: position.x, y: position.y }), ...queryFragments])); - }); - return arrivals; - }, []); + const hasMinWeight = useCallback( + (entity: Entity) => { + const weight = getComponentValue(Weight, entity); + return !!(weight?.value && Number(weight.value) >= minWeight); + }, + [minWeight], + ); const createArrivalInfo = useCallback( (id: Entity): ArrivalInfo | undefined => { + // Get required component values const position = getComponentValue(Position, id); - if (!position) return undefined; - const arrivalTime = getComponentValue(ArrivalTime, id); - if (!arrivalTime) return undefined; - + const ownedResourceTracker = getComponentValue(OwnedResourcesTracker, id); const entityOwner = getComponentValue(EntityOwner, id); - const ownerEntityId = getEntityIdFromKeys([BigInt(entityOwner?.entity_owner_id || 0)]); - const owner = getComponentValue(Owner, ownerEntityId); - if (owner?.address !== ContractAddress(account.address)) { - return undefined; - } + // Return early if missing required components + if (!position || !arrivalTime) return undefined; - const ownedResourceTracker = getComponentValue(OwnedResourcesTracker, id); + // Check if entity has special resource types that don't need weight check + const hasSpecialResources = + ownedResourceTracker?.resource_types === DONKEY_RESOURCE_TRACKER || + ownedResourceTracker?.resource_types === LORDS_RESOURCE_TRACKER; - const hasResources = !!ownedResourceTracker && ownedResourceTracker.resource_types !== 0n; + // Determine if entity meets weight requirements + const meetsWeightRequirement = hasSpecialResources || hasMinWeight(id); + // Get owner information + const ownerEntityId = getEntityIdFromKeys([BigInt(entityOwner?.entity_owner_id || 0)]); + const owner = getComponentValue(Owner, ownerEntityId); + const isOwner = owner?.address === ContractAddress(account.address); + + // Check if entity has resources + const hasResources = + meetsWeightRequirement && !!ownedResourceTracker && ownedResourceTracker.resource_types !== 0n; + + // Find matching player structure at position const playerStructurePosition = playerStructurePositions.find( (structurePosition) => structurePosition.x === position.x && structurePosition.y === position.y, ); - const isHome = !!playerStructurePosition; - return { entityId: position.entity_id, recipientEntityId: playerStructurePosition?.entityId || 0, arrivesAt: arrivalTime.arrives_at, - isOwner: true, + isOwner, position: { x: position.x, y: position.y }, hasResources, - isHome, + isHome: !!playerStructurePosition, }; }, [account, playerStructurePositions], ); - // initial load - useEffect(() => { - const arrivals = getArrivalsWithResourceOnPosition(playerStructurePositions) - .map(createArrivalInfo) - .filter((arrival): arrival is ArrivalInfo => arrival !== undefined) - .filter((arrival) => arrival.hasResources); - setEntitiesWithInventory(arrivals); - }, [playerStructurePositions, getArrivalsWithResourceOnPosition, createArrivalInfo]); - const isMine = useCallback( (entity: Entity) => { const entityOwner = getComponentValue(EntityOwner, entity); @@ -123,12 +111,21 @@ const usePlayerArrivals = () => { ); useEffect(() => { - const query = defineQuery([Has(Position), ...queryFragments], { runOnInit: false }); + const query = defineQuery( + [ + Has(Position), + Has(Weight), + Has(ArrivalTime), + Has(EntityOwner), + NotValue(OwnedResourcesTracker, { resource_types: 0n }), + ], + { runOnInit: false }, + ); const handleArrivalUpdate = (arrivals: ArrivalInfo[], newArrival: ArrivalInfo | undefined) => { if (!newArrival) return arrivals; - if (!newArrival.hasResources || !newArrival.isHome) { + if (!newArrival.hasResources || !newArrival.isHome || !newArrival.isOwner) { return arrivals.filter((arrival) => arrival.entityId !== newArrival.entityId); } @@ -147,10 +144,7 @@ const usePlayerArrivals = () => { isComponentUpdate(update, ArrivalTime) || isComponentUpdate(update, OwnedResourcesTracker) ) { - const isThisMine = isMine(update.entity); - - isThisMine && - setEntitiesWithInventory((arrivals) => handleArrivalUpdate(arrivals, createArrivalInfo(update.entity))); + setEntitiesWithInventory((arrivals) => handleArrivalUpdate(arrivals, createArrivalInfo(update.entity))); } }); diff --git a/client/src/hooks/helpers/useEntities.tsx b/client/src/hooks/helpers/useEntities.tsx index d32d1ca0c..c145b0c36 100644 --- a/client/src/hooks/helpers/useEntities.tsx +++ b/client/src/hooks/helpers/useEntities.tsx @@ -9,7 +9,7 @@ import { type ID, } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { Has, getComponentValue, type ComponentValue } from "@dojoengine/recs"; +import { Has, HasValue, getComponentValue, type ComponentValue } from "@dojoengine/recs"; import { useMemo } from "react"; import { shortString } from "starknet"; import { useDojo } from "../context/DojoContext"; @@ -23,7 +23,7 @@ export type PlayerStructure = ComponentValue; }; -type RealmWithPosition = ComponentValue & { +export type RealmWithPosition = ComponentValue & { position: ComponentValue; name: string; owner: ComponentValue; @@ -43,53 +43,18 @@ export const useEntities = () => { const { getEntityName } = useEntitiesUtils(); // Get all realms - const allRealms = useEntityQuery([Has(Realm)]); - - const filterPlayerRealms = useMemo(() => { - return allRealms.filter((id) => { - const owner = getComponentValue(Owner, id); - return owner && ContractAddress(owner.address) === ContractAddress(address); - }); - }, [allRealms, address]); - - const filterOtherRealms = useMemo(() => { - return allRealms.filter((id) => { - const owner = getComponentValue(Owner, id); - return owner && ContractAddress(owner.address) !== ContractAddress(address); - }); - }, [allRealms, address]); + const playerRealmsQuery = useEntityQuery([Has(Realm), HasValue(Owner, { address: address })]); // Get all structures - const allStructures = useEntityQuery([Has(Structure), Has(Position), Has(Owner)]); - - const filterPlayerStructures = useMemo(() => { - return allStructures.filter((id) => { - const owner = getComponentValue(Owner, id); - return owner && ContractAddress(owner.address) === ContractAddress(address); - }); - }, [allStructures, address]); - - const filterOtherStructures = useMemo(() => { - return allStructures.filter((id) => { - const owner = getComponentValue(Owner, id); - return owner && ContractAddress(owner.address) !== ContractAddress(address); - }); - }, [allStructures, address]); + const playerStructuresQuery = useEntityQuery([ + Has(Structure), + Has(Position), + Has(Owner), + HasValue(Owner, { address: address }), + ]); const playerRealms = useMemo(() => { - return filterPlayerRealms.map((id) => { - const realm = getComponentValue(Realm, id); - return { - ...realm, - position: getComponentValue(Position, id), - name: getRealmNameById(realm!.realm_id), - owner: getComponentValue(Owner, id), - } as RealmWithPosition; - }); - }, [filterPlayerRealms]); - - const otherRealms = useMemo(() => { - return filterOtherRealms.map((id) => { + return playerRealmsQuery.map((id) => { const realm = getComponentValue(Realm, id); return { ...realm, @@ -98,10 +63,10 @@ export const useEntities = () => { owner: getComponentValue(Owner, id), } as RealmWithPosition; }); - }, [filterOtherRealms]); + }, [playerRealmsQuery]); const playerStructures = useMemo(() => { - return filterPlayerStructures + return playerStructuresQuery .map((id) => { const structure = getComponentValue(Structure, id); if (!structure) return; @@ -121,24 +86,7 @@ export const useEntities = () => { if (b.category === StructureType[StructureType.Realm]) return 1; return a.category.localeCompare(b.category); }); - }, [filterPlayerStructures]); - - const otherStructures = useMemo(() => { - return filterOtherStructures - .map((id) => { - const structure = getComponentValue(Structure, id); - if (!structure || structure.category === StructureType[StructureType.Realm]) return; - - const position = getComponentValue(Position, id); - - const structureName = getEntityName(structure.entity_id); - - const name = structureName ? `${structure?.category} ${structureName}` : structure.category || ""; - return { ...structure, position: position!, name, owner: getComponentValue(Owner, id) }; - }) - .filter((structure): structure is PlayerStructure => structure !== undefined) - .sort((a, b) => a.category.localeCompare(b.category)); - }, [filterOtherStructures]); + }, [playerStructuresQuery]); const getPlayerRealms = (filterFn?: (realm: RealmWithPosition) => boolean) => { return useMemo(() => { @@ -147,12 +95,6 @@ export const useEntities = () => { }, [playerRealms, filterFn]); }; - const getOtherRealms = (filterFn?: (realm: RealmWithPosition) => boolean) => { - return useMemo(() => { - return filterFn ? otherRealms.filter(filterFn) : otherRealms; - }, [otherRealms, filterFn]); - }; - const getPlayerStructures = (filterFn?: (structure: PlayerStructure) => boolean) => { return useMemo(() => { const structures = filterFn ? playerStructures.filter(filterFn) : playerStructures; @@ -160,17 +102,9 @@ export const useEntities = () => { }, [playerStructures, filterFn]); }; - const getOtherStructures = (filterFn?: (structure: PlayerStructure) => boolean) => { - return useMemo(() => { - return filterFn ? otherStructures.filter(filterFn) : otherStructures; - }, [otherStructures, filterFn]); - }; - return { playerRealms: getPlayerRealms, - otherRealms: getOtherRealms, playerStructures: getPlayerStructures, - otherStructures: getOtherStructures, }; }; diff --git a/client/src/hooks/helpers/useGuilds.tsx b/client/src/hooks/helpers/useGuilds.tsx index 94e1e6ff5..b197add82 100644 --- a/client/src/hooks/helpers/useGuilds.tsx +++ b/client/src/hooks/helpers/useGuilds.tsx @@ -308,5 +308,6 @@ export const useGuilds = () => { usePlayerWhitelist, getGuildFromEntityId, getPlayersInPlayersGuild, + getPlayerListInGuild, }; }; diff --git a/client/src/hooks/helpers/useRealm.tsx b/client/src/hooks/helpers/useRealm.tsx index 88e2690d9..a62fb4bf7 100644 --- a/client/src/hooks/helpers/useRealm.tsx +++ b/client/src/hooks/helpers/useRealm.tsx @@ -2,12 +2,12 @@ import { type ClientComponents } from "@/dojo/createClientComponents"; import { configManager } from "@/dojo/setup"; import { ContractAddress, - type ID, getOrderName, getQuestResources as getStartingResources, + type ID, } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { type ComponentValue, type Entity, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs"; +import { Has, HasValue, getComponentValue, runQuery, type ComponentValue, type Entity } from "@dojoengine/recs"; import { useMemo } from "react"; import { shortString } from "starknet"; import realmIdsByOrder from "../../data/realmids_by_order.json"; @@ -265,7 +265,7 @@ export function getRealms(): RealmInfo[] { const position = getComponentValue(Position, entity); const population = getComponentValue(Population, entity); - if (!realm || !owner || !position) return; + if (!realm || !owner || !position) return null; const { realm_id, entity_id, produced_resources, order } = realm; @@ -291,7 +291,7 @@ export function getRealms(): RealmInfo[] { hasWonder: realm.has_wonder, }; }) - .filter((realm) => realm !== undefined); + .filter((realm): realm is RealmInfo => realm !== null); } export function usePlayerRealms(): RealmInfo[] { @@ -312,7 +312,7 @@ export function usePlayerRealms(): RealmInfo[] { const position = getComponentValue(Position, entity); const population = getComponentValue(Population, entity); - if (!realm || !owner || !position) return; + if (!realm || !owner || !position) return null; const { realm_id, entity_id, produced_resources, order } = realm; @@ -338,7 +338,7 @@ export function usePlayerRealms(): RealmInfo[] { hasWonder: realm.has_wonder, }; }) - .filter((realm) => realm !== undefined); + .filter((realm): realm is RealmInfo => realm !== null); }, [realmEntities]); return realms; } 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/resources/InventoryResources.tsx b/client/src/ui/components/resources/InventoryResources.tsx index 0b4b7cd2f..235d28010 100644 --- a/client/src/ui/components/resources/InventoryResources.tsx +++ b/client/src/ui/components/resources/InventoryResources.tsx @@ -1,4 +1,4 @@ -import { addToSubscription } from "@/dojo/queries"; +import { debouncedAddToSubscription } from "@/dojo/debouncedQueries"; import { useDojo } from "@/hooks/context/DojoContext"; import { useResourceBalance, useResourcesUtils } from "@/hooks/helpers/useResources"; import { ResourceCost } from "@/ui/elements/ResourceCost"; @@ -52,7 +52,7 @@ export const InventoryResources = ({ setIsSyncing(true); try { console.log("AddToSubscriptionStart - 4"); - await addToSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, [ + await debouncedAddToSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, [ entityId.toString(), ]); localStorage.setItem(cacheKey, now.toString()); 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/MarketOrderPanel.tsx b/client/src/ui/components/trading/MarketOrderPanel.tsx index 031e46a29..749af7cc3 100644 --- a/client/src/ui/components/trading/MarketOrderPanel.tsx +++ b/client/src/ui/components/trading/MarketOrderPanel.tsx @@ -451,7 +451,7 @@ const OrderRow = memo( donkeyBalance || donkeyBalance === 0} + disabled={!isBuy && donkeysNeeded > donkeyBalance} onCancel={() => { setConfirmOrderModal(false); }} @@ -600,7 +600,7 @@ const OrderCreation = memo( const enoughDonkeys = useMemo(() => { if (resourceId === ResourcesIds.Donkey) return true; - return donkeyBalance > donkeysNeeded; + return donkeyBalance >= donkeysNeeded; }, [donkeyBalance, donkeysNeeded, resourceId]); return ( diff --git a/client/src/ui/components/trading/ResourceArrivals.tsx b/client/src/ui/components/trading/ResourceArrivals.tsx index d12b766f1..a1a86b841 100644 --- a/client/src/ui/components/trading/ResourceArrivals.tsx +++ b/client/src/ui/components/trading/ResourceArrivals.tsx @@ -1,16 +1,38 @@ 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, 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; +} + +const useSubscribedIdsStore = create((set) => ({ + subscribedIds: new Set(), + addSubscribedIds: (ids) => + set((state) => ({ + subscribedIds: new Set([...state.subscribedIds, ...ids]), + })), +})); + export const AllResourceArrivals = memo( ({ arrivals, className = "" }: { arrivals: ArrivalInfo[]; className?: string }) => { const dojo = useDojo(); - const [subscribedIds, setSubscribedIds] = useState>(new Set()); + const [displayCount, setDisplayCount] = useState(DISPLAYED_ARRIVALS); + const [showOnlyArrived, setShowOnlyArrived] = useState(true); + + const { nextBlockTimestamp } = useNextBlockTimestamp(); + const { subscribedIds, addSubscribedIds } = useSubscribedIdsStore(); useEffect(() => { // Create a single Set from newIds for O(1) lookup @@ -21,19 +43,26 @@ export const AllResourceArrivals = memo( if (unsubscribedIds.length === 0) return; - // Batch the state update with the API call - setSubscribedIds((prev) => { - // If nothing changed, return the previous state to prevent re-render - if (unsubscribedIds.every((id) => prev.has(id))) return prev; - return new Set([...prev, ...unsubscribedIds]); - }); + // Update zustand store + addSubscribedIds(unsubscribedIds); // Move API call outside of state updates addToSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, unsubscribedIds).catch( (error) => console.error("Fetch failed", error), ); console.log("AddToSubscriptionStart - 5"); - }, [arrivals, subscribedIds]); + }, [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 (
@@ -43,9 +72,22 @@ export const AllResourceArrivals = memo(
- {arrivals.map((arrival) => ( +
+ +
+ {displayedArrivals.map((arrival) => ( ))} + {hasMore && ( +
+ +
+ )}
); }, diff --git a/client/src/ui/components/trading/SelectResources.tsx b/client/src/ui/components/trading/SelectResources.tsx index f5abae5e9..cdfdf1c16 100644 --- a/client/src/ui/components/trading/SelectResources.tsx +++ b/client/src/ui/components/trading/SelectResources.tsx @@ -5,7 +5,7 @@ import ListSelect from "@/ui/elements/ListSelect"; import { NumberInput } from "@/ui/elements/NumberInput"; import { ResourceCost } from "@/ui/elements/ResourceCost"; import { divideByPrecision } from "@/ui/utils/utils"; -import { ID, resources } from "@bibliothecadao/eternum"; +import { ID, RESOURCE_TIERS, ResourcesIds, resources } from "@bibliothecadao/eternum"; import { useMemo } from "react"; export const SelectResources = ({ @@ -24,8 +24,18 @@ export const SelectResources = ({ const { getBalance } = useResourceBalance(); const { playResourceSound } = usePlayResourceSound(); + const orderedResources = useMemo(() => { + return Object.values(RESOURCE_TIERS) + .flat() + .filter((resourceId) => resourceId !== ResourcesIds.Lords) + .map((resourceId) => ({ + id: resourceId, + trait: ResourcesIds[resourceId], + })); + }, []); + const unselectedResources = useMemo( - () => resources.filter((res) => !selectedResourceIds.includes(res.id)), + () => orderedResources.filter((res) => !selectedResourceIds.includes(res.id)), [selectedResourceIds], ); @@ -39,10 +49,11 @@ export const SelectResources = ({ }; return ( -
+
{selectedResourceIds.map((id: any, index: any) => { const resource = getBalance(entity_id, id); - const options = [resources.find((res) => res.id === id), ...unselectedResources].map((res: any) => ({ + + const options = [orderedResources.find((res) => res.id === id), ...unselectedResources].map((res: any) => ({ id: res.id, label: ( @@ -65,7 +76,10 @@ export const SelectResources = ({ )} ({ + ...option, + searchText: resources.find((res) => res.id === option.id)?.trait, + }))} value={selectedResourceIds[index]} onChange={(value) => { const updatedResourceIds = [...selectedResourceIds]; @@ -77,6 +91,7 @@ export const SelectResources = ({ }); playResourceSound(value); }} + enableFilter={true} /> { + const normalizedSearchTerm = normalizeDiacriticalMarks(searchTerm.toLowerCase()); return entities.filter( (entity) => entity.entity_id === selectedEntityId || - entity.name.toLowerCase().includes(searchTerm.toLowerCase()) || - (entity.accountName && entity.accountName.toLowerCase().includes(searchTerm.toLowerCase())), + normalizeDiacriticalMarks(entity.name.toLowerCase()).includes(normalizedSearchTerm) || + (entity.accountName && + normalizeDiacriticalMarks(entity.accountName.toLowerCase()).includes(normalizedSearchTerm)), ); }; @@ -319,9 +321,17 @@ export const TransferBetweenEntities = ({
-
+
+
@@ -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/navigation/TopLeftNavigation.tsx b/client/src/ui/modules/navigation/TopLeftNavigation.tsx index ed6befec1..ebab40085 100644 --- a/client/src/ui/modules/navigation/TopLeftNavigation.tsx +++ b/client/src/ui/modules/navigation/TopLeftNavigation.tsx @@ -1,6 +1,6 @@ import { configManager } from "@/dojo/setup"; import { useDojo } from "@/hooks/context/DojoContext"; -import { useEntities, useEntitiesUtils } from "@/hooks/helpers/useEntities"; +import { PlayerStructure, useEntitiesUtils } from "@/hooks/helpers/useEntities"; import { useQuery } from "@/hooks/helpers/useQuery"; import useUIStore from "@/hooks/store/useUIStore"; import useNextBlockTimestamp from "@/hooks/useNextBlockTimestamp"; @@ -92,13 +92,11 @@ const WorkersHutTooltipContent = () => { ); }; -export const TopLeftNavigation = memo(() => { +export const TopLeftNavigation = memo(({ structures }: { structures: PlayerStructure[] }) => { const { setup } = useDojo(); const { isMapView, handleUrlChange, hexPosition } = useQuery(); - const { playerStructures } = useEntities(); const { getEntityInfo } = useEntitiesUtils(); - const structures = playerStructures(); const isSpectatorMode = useUIStore((state) => state.isSpectatorMode); const structureEntityId = useUIStore((state) => state.structureEntityId); diff --git a/client/src/ui/modules/navigation/TopNavigation.tsx b/client/src/ui/modules/navigation/TopNavigation.tsx index b8ebb09ab..551b1137c 100644 --- a/client/src/ui/modules/navigation/TopNavigation.tsx +++ b/client/src/ui/modules/navigation/TopNavigation.tsx @@ -1,5 +1,4 @@ import { useGetAllPlayers } from "@/hooks/helpers/use-get-all-players"; - import { memo } from "react"; import { Rewards } from "../rewards/Rewards"; import { SettingsWindow } from "../settings/Settings"; @@ -7,12 +6,11 @@ import { Social } from "../social/Social"; export const TopMiddleNavigation = memo(() => { const getPlayers = useGetAllPlayers(); - const players = getPlayers(); return ( <>
- +
diff --git a/client/src/ui/modules/settings/Settings.tsx b/client/src/ui/modules/settings/Settings.tsx index 700b8e54d..90f532b42 100644 --- a/client/src/ui/modules/settings/Settings.tsx +++ b/client/src/ui/modules/settings/Settings.tsx @@ -1,8 +1,8 @@ +import { ReactComponent as Controller } from "@/assets/icons/Controller.svg"; import { ReactComponent as Copy } from "@/assets/icons/common/copy.svg"; import { ReactComponent as Next } from "@/assets/icons/common/fast-forward.svg"; import { ReactComponent as Muted } from "@/assets/icons/common/muted.svg"; import { ReactComponent as Unmuted } from "@/assets/icons/common/unmuted.svg"; -import { ReactComponent as Controller } from "@/assets/icons/Controller.svg"; import { ReactComponent as DojoMark } from "@/assets/icons/dojo-mark-full-dark.svg"; import { ReactComponent as RealmsWorld } from "@/assets/icons/rw-logo.svg"; import { useDojo } from "@/hooks/context/DojoContext"; @@ -18,7 +18,7 @@ import Button from "@/ui/elements/Button"; import { Checkbox } from "@/ui/elements/Checkbox"; import { Headline } from "@/ui/elements/Headline"; import { RangeInput } from "@/ui/elements/RangeInput"; -import { addressToNumber, displayAddress } from "@/ui/utils/utils"; +import { addressToNumber, currencyIntlFormat, displayAddress } from "@/ui/utils/utils"; import { ContractAddress } from "@bibliothecadao/eternum"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; @@ -42,6 +42,16 @@ export const SettingsWindow = () => { const isSoundOn = useUIStore((state) => state.isSoundOn); const toggleSound = useUIStore((state) => state.toggleSound); + const getCurrentDonkeyWeightMinimum = () => { + return Number(localStorage.getItem("WEIGHT_MINIMUM") || 0); + }; + + const [donkeyWeightLimit, setDonkeyWeightLimit] = useState(getCurrentDonkeyWeightMinimum()); + + useEffect(() => { + localStorage.setItem("WEIGHT_MINIMUM", donkeyWeightLimit.toString()); + }, [donkeyWeightLimit]); + const { toggleFullScreen, isFullScreen } = useScreenOrientation(); const [fullScreen, setFullScreen] = useState(isFullScreen()); @@ -61,7 +71,7 @@ export const SettingsWindow = () => { const isOpen = useUIStore((state) => state.isPopupOpen(settings)); - const GRAPHICS_SETTING = localStorage.getItem("GRAPHICS_SETTING") as GraphicsSettings || GraphicsSettings.HIGH; + const GRAPHICS_SETTING = (localStorage.getItem("GRAPHICS_SETTING") as GraphicsSettings) || GraphicsSettings.HIGH; return ( togglePopup(settings)} show={isOpen} title={settings}> @@ -145,6 +155,18 @@ export const SettingsWindow = () => { + + Donkey Settings + + diff --git a/client/src/ui/modules/social/Social.tsx b/client/src/ui/modules/social/Social.tsx index 92178c677..a38abdb77 100644 --- a/client/src/ui/modules/social/Social.tsx +++ b/client/src/ui/modules/social/Social.tsx @@ -1,5 +1,5 @@ import { useDojo } from "@/hooks/context/DojoContext"; -import { useHyperstructureData } from "@/hooks/store/useLeaderBoardStore"; +import { useHyperstructureData, useLeaderBoardStore } from "@/hooks/store/useLeaderBoardStore"; import useUIStore from "@/hooks/store/useUIStore"; import { HintSection } from "@/ui/components/hints/HintModal"; import { social } from "@/ui/components/navigation/Config"; @@ -12,11 +12,11 @@ import { Tabs } from "@/ui/elements/tab"; import { ContractAddress, ID, Player } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; import { Has } from "@dojoengine/recs"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { EndSeasonButton } from "./EndSeasonButton"; import { PlayerId } from "./PlayerId"; -export const Social = ({ players }: { players: Player[] }) => { +export const Social = ({ getPlayers }: { getPlayers: () => Player[] }) => { const { setup: { components: { @@ -26,7 +26,7 @@ export const Social = ({ players }: { players: Player[] }) => { } = useDojo(); const [selectedTab, setSelectedTab] = useState(0); const [isExpanded, setIsExpanded] = useState(false); - + const [isLoading, setIsLoading] = useState(false); const [selectedGuild, setSelectedGuild] = useState(0); const [selectedPlayer, setSelectedPlayer] = useState(0n); @@ -35,8 +35,21 @@ export const Social = ({ players }: { players: Player[] }) => { const gameEnded = useEntityQuery([Has(GameEnded)]); + const [players, setPlayers] = useState(() => getPlayers()); + const playersByRank = useLeaderBoardStore((state) => state.playersByRank); + const updateLeaderboard = useHyperstructureData(); + const handleUpdatePoints = () => { + setIsLoading(true); + updateLeaderboard(); + }; + + useEffect(() => { + setPlayers(getPlayers()); + setIsLoading(false); + }, [playersByRank]); + const viewGuildMembers = (guildEntityId: ID) => { if (selectedGuild === guildEntityId) { setSelectedPlayer(0n); @@ -112,7 +125,7 @@ export const Social = ({ players }: { players: Player[] }) => {
{gameEnded.length === 0 && } -
diff --git a/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx b/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx index 7473c53e0..2e55caf1e 100644 --- a/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx +++ b/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx @@ -7,7 +7,6 @@ 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 { FragmentMinePanel } from "@/ui/components/fragmentMines/FragmentMinePanel"; import { HintSection } from "@/ui/components/hints/HintModal"; import { DisplayedAccess, HyperstructurePanel } from "@/ui/components/hyperstructures/HyperstructurePanel"; @@ -19,39 +18,15 @@ import { HintModalButton } from "@/ui/elements/HintModalButton"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; import { currencyFormat, currencyIntlFormat, divideByPrecision } from "@/ui/utils/utils"; import { BattleSide, ContractAddress, ID, ResourcesIds, findResourceById } from "@bibliothecadao/eternum"; -import { Metadata } from "@dojoengine/recs"; -import { S } from "@dojoengine/recs/dist/types-3444e4c1"; -import { getEntities } from "@dojoengine/state"; -import { ToriiClient } from "@dojoengine/torii-wasm"; import { ArrowRight } from "lucide-react"; -import { Component, useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Tabs } from "../../elements/tab"; export const WorldStructuresMenu = ({ className }: { className?: string }) => { const { account: { account }, - network: { toriiClient, contractComponents }, } = useDojo(); - const isStructuresLoading = useWorldStore((state) => state.isStructuresLoading); - const setStructuresLoading = useWorldStore((state) => state.setStructuresLoading); - - useEffect(() => { - const fetchData = async () => { - try { - await fetchHyperstructureData( - toriiClient, - contractComponents as any, - isStructuresLoading, - setStructuresLoading, - ); - } catch (error) { - console.error("Failed to fetch hyperstructure data:", error); - } - }; - fetchData(); - }, [toriiClient, contractComponents]); - const [selectedTab, setSelectedTab] = useState(0); const [showOnlyMine, setShowOnlyMine] = useState(false); @@ -142,14 +117,6 @@ export const WorldStructuresMenu = ({ className }: { className?: string }) => { [selectedTab, hyperstructures, fragmentMines, showOnlyMine, account.address, myHyperstructures], ); - if (isStructuresLoading) { - return ( -
-
Loading structures...
-
- ); - } - return ( <> @@ -342,47 +309,3 @@ const EntityHeader = ({ entity }: { entity: any }) => {
); }; - -const fetchHyperstructureData = async ( - client: ToriiClient, - components: Component[], - isStructuresLoading: boolean, - setStructuresLoading: (loading: boolean) => void, -) => { - if (!isStructuresLoading) { - return; - } - - console.log("Fetching hyperstructure data"); - await getEntities( - client, - { - Composite: { - operator: "Or", - clauses: [ - { - Keys: { - keys: [undefined, undefined], - pattern_matching: "VariableLen", - models: ["s0_eternum-Contribution"], - }, - }, - { - Keys: { - keys: [undefined, undefined, undefined], - pattern_matching: "VariableLen", - models: ["s0_eternum-Epoch", "s0_eternum-Progress"], - }, - }, - ], - }, - }, - components as any, - [], - [], - 40_000, - false, - ).finally(() => { - setStructuresLoading(false); - }); -}; diff --git a/client/src/ui/utils/utils.tsx b/client/src/ui/utils/utils.tsx index e565ab35e..69b08240b 100644 --- a/client/src/ui/utils/utils.tsx +++ b/client/src/ui/utils/utils.tsx @@ -530,3 +530,7 @@ export const calculateDonkeysNeeded = (orderWeight: number): number => { return Math.ceil(divideByPrecision(orderWeight) / donkeyCapacityGrams); }; + +export const normalizeDiacriticalMarks = (str: string) => { + return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); +};