diff --git a/.github/workflows/test-contracts.yml b/.github/workflows/test-contracts.yml index c0ee63ab9..b74af2da8 100644 --- a/.github/workflows/test-contracts.yml +++ b/.github/workflows/test-contracts.yml @@ -44,12 +44,13 @@ jobs: realm_test, guild_test, transport_test, + test_ownership_systems, ] fail-fast: false steps: - name: Download Dojo release artifact run: | - curl -L -o dojo-linux-x86_64.tar.gz https://github.com/dojoengine/dojo/releases/download/v1.0.0-alpha.12/dojo_v1.0.0-alpha.12_linux_amd64.tar.gz + curl -L -o dojo-linux-x86_64.tar.gz https://github.com/dojoengine/dojo/releases/download/v1.0.0-alpha.17/dojo_v1.0.0-alpha.17_linux_amd64.tar.gz tar -xzf dojo-linux-x86_64.tar.gz sudo mv sozo /usr/local/bin/ - name: Checkout repository diff --git a/.github/workflows/test-season-pass.yml b/.github/workflows/test-season-pass.yml new file mode 100644 index 000000000..c5ae5a2b7 --- /dev/null +++ b/.github/workflows/test-season-pass.yml @@ -0,0 +1,31 @@ +name: test season pass + +on: + pull_request: + paths-ignore: + - "contracts/**" + - "client/**" + - "**/manifest.json" + - "discord-bot/**" + - "config/**" + - ".github/**" + - "pnpm-lock.yaml" + +env: + SCARB_VERSION: v2.8.2 + +jobs: + test-season-pass: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: software-mansion/setup-scarb@v1 + with: + tool-versions: season_pass/contracts/.tool-versions + - uses: foundry-rs/setup-snfoundry@v3 + with: + tool-versions: season_pass/contracts/.tool-versions + - run: scarb build + working-directory: season_pass/contracts + - run: snforge test + working-directory: season_pass/contracts diff --git a/.gitignore b/.gitignore index b512c09d4..4698abb44 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -node_modules \ No newline at end of file +node_modules +target +.snfoundry_cache \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index f3239a05d..89154d46e 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ scarb 2.7.0 -dojo 1.0.0-alpha.12 +dojo 1.0.0-alpha.17 diff --git a/balancing/src/App.tsx b/balancing/src/App.tsx index e826bc641..8ad6c0231 100644 --- a/balancing/src/App.tsx +++ b/balancing/src/App.tsx @@ -7,7 +7,7 @@ import { ResourceTable } from "./components/modules/resource-table"; function App() { return ( <> -
+
Production diff --git a/client/.env.preview b/client/.env.preview index 2378e57ea..0aa422f65 100644 --- a/client/.env.preview +++ b/client/.env.preview @@ -1,7 +1,7 @@ VITE_PUBLIC_MASTER_ADDRESS="0x1a3e37c77be7de91a9177c6b57956faa6da25607e567b10a25cf64fea5e533b" VITE_PUBLIC_MASTER_PRIVATE_KEY="0x4ab5a607d92f0870cfd82ef9cecb2fe903830441180fd432b831a8863c08097" -VITE_PUBLIC_WORLD_ADDRESS="0x76ca3dfc3e96843716f882546f0db96b7da0cf988bdba284b469d0defb2f48f" -VITE_PUBLIC_ACCOUNT_CLASS_HASH="0x05400e90f7e0ae78bd02c77cd75527280470e2fe19c54970dd79dc37a9d3645c" +VITE_PUBLIC_WORLD_ADDRESS="0x320b2713e324fe3125bbc42d85ff69cb3c0908b436fa38a35746dbc45deeb11" +VITE_PUBLIC_ACCOUNT_CLASS_HASH="0x07dc7899aa655b0aae51eadff6d801a58e97dd99cf4666ee59e704249e51adf2" VITE_EVENT_KEY="0x1a2f334228cee715f1f0f54053bb6b5eac54fa336e0bc1aacf7516decb0471d" VITE_PUBLIC_TORII="https://api.cartridge.gg/x/eternum-42/torii" VITE_PUBLIC_NODE_URL="https://api.cartridge.gg/x/eternum-42/katana/" diff --git a/client/.env.production b/client/.env.production index b4d1dc05e..b63cedb2e 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1,7 +1,7 @@ VITE_PUBLIC_MASTER_ADDRESS="0x779c2c098f066ddde5850ec8426511e46e6499adf0b5c77e8961917413b57db" VITE_PUBLIC_MASTER_PRIVATE_KEY="0x189765c7b9daa2efdd3025d3236a929ce5e510834b8cba4f002a0d5c1accb5a" -VITE_PUBLIC_WORLD_ADDRESS="0x76ca3dfc3e96843716f882546f0db96b7da0cf988bdba284b469d0defb2f48f" -VITE_PUBLIC_ACCOUNT_CLASS_HASH="0x05400e90f7e0ae78bd02c77cd75527280470e2fe19c54970dd79dc37a9d3645c" +VITE_PUBLIC_WORLD_ADDRESS="0x320b2713e324fe3125bbc42d85ff69cb3c0908b436fa38a35746dbc45deeb11" +VITE_PUBLIC_ACCOUNT_CLASS_HASH="0x07dc7899aa655b0aae51eadff6d801a58e97dd99cf4666ee59e704249e51adf2" VITE_EVENT_KEY="0x1a2f334228cee715f1f0f54053bb6b5eac54fa336e0bc1aacf7516decb0471d" VITE_PUBLIC_TORII="https://api.cartridge.gg/x/eternum-42/torii" VITE_PUBLIC_NODE_URL="https://api.cartridge.gg/x/eternum-42/katana/" @@ -10,3 +10,7 @@ VITE_PUBLIC_GAME_VERSION="v0.9.0" VITE_PUBLIC_SHOW_FPS=false VITE_PUBLIC_GRAPHICS_DEV=false VITE_PUBLIC_TORII_RELAY="/dns4/api.cartridge.gg/tcp/443/x-parity-wss/%2Fx%2Feternum-42%2Ftorii%2Fwss" + +VITE_SEASON_PASS_ADDRESS="0x0" +VITE_REALMS_ADDRESS="0x0" +VITE_LORDS_ADDRESS="0x0" \ No newline at end of file diff --git a/client/.env.sample b/client/.env.sample index e3250fdf2..3f8507d28 100644 --- a/client/.env.sample +++ b/client/.env.sample @@ -1,7 +1,7 @@ -VITE_PUBLIC_MASTER_ADDRESS="0xb3ff441a68610b30fd5e2abbf3a1548eb6ba6f3559f2862bf2dc757e5828ca" -VITE_PUBLIC_MASTER_PRIVATE_KEY="0x2bbf4f9fd0bbb2e60b0316c1fe0b76cf7a4d0198bd493ced9b8df2a3a24d68a" -VITE_PUBLIC_WORLD_ADDRESS="0x177a3f3d912cf4b55f0f74eccf3b7def7c6144efeba033e9f21d9cdb0230c64" -VITE_PUBLIC_ACCOUNT_CLASS_HASH="0x05400e90f7e0ae78bd02c77cd75527280470e2fe19c54970dd79dc37a9d3645c" +VITE_PUBLIC_MASTER_ADDRESS="0x127fd5f1fe78a71f8bcd1fec63e3fe2f0486b6ecd5c86a0466c3a21fa5cfcec" +VITE_PUBLIC_MASTER_PRIVATE_KEY="0xc5b2fcab997346f3ea1c00b002ecf6f382c5f9c9659a3894eb783c5320f912" +VITE_PUBLIC_WORLD_ADDRESS="0x320b2713e324fe3125bbc42d85ff69cb3c0908b436fa38a35746dbc45deeb11" +VITE_PUBLIC_ACCOUNT_CLASS_HASH="0x07dc7899aa655b0aae51eadff6d801a58e97dd99cf4666ee59e704249e51adf2" VITE_NETWORK_FEE_TOKEN="0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" VITE_EVENT_KEY="0x1a2f334228cee715f1f0f54053bb6b5eac54fa336e0bc1aacf7516decb0471d" VITE_PUBLIC_TORII="http://localhost:8080" @@ -12,5 +12,9 @@ VITE_PUBLIC_DEV=false VITE_PUBLIC_SHOW_FPS=true VITE_PUBLIC_GRAPHICS_DEV=false +VITE_SEASON_PASS_ADDRESS="0x18cd31a545b13597adeafa40979a7456aae9d12bd0b9a9879089ed051402c6c" +VITE_REALMS_ADDRESS="0x57e1cb22f5688a833ceff37d8569297877dd8fc1896e36f1a0c8b2d30b7de85" +VITE_LORDS_ADDRESS="0x5839f41cadbe12678b2e7b67bd6daa5bd54b7b19bdc835c87fe4662dbf7a93c" + # You will have to change this to your local torii. It will be in the printout. VITE_PUBLIC_TORII_RELAY="/ip4/0.0.0.0/udp/9091/webrtc-direct/certhash/uEiAN3-ttwg-yphp9ZgqhSCGNsWLXIo0eHEsrLRUzp4SpKg" \ No newline at end of file diff --git a/client/dojoConfig.ts b/client/dojoConfig.ts index 906c7ef7d..76fef068f 100644 --- a/client/dojoConfig.ts +++ b/client/dojoConfig.ts @@ -22,7 +22,7 @@ export const dojoConfig = createDojoConfig({ masterAddress: VITE_PUBLIC_MASTER_ADDRESS, masterPrivateKey: VITE_PUBLIC_MASTER_PRIVATE_KEY, accountClassHash: - VITE_PUBLIC_ACCOUNT_CLASS_HASH || "0x05400e90f7e0ae78bd02c77cd75527280470e2fe19c54970dd79dc37a9d3645c", + VITE_PUBLIC_ACCOUNT_CLASS_HASH || "0x07dc7899aa655b0aae51eadff6d801a58e97dd99cf4666ee59e704249e51adf2", feeTokenAddress: VITE_PUBLIC_FEE_TOKEN_ADDRESS || "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", manifest, }); diff --git a/client/package.json b/client/package.json index 3c114e500..b9ee7c8b1 100644 --- a/client/package.json +++ b/client/package.json @@ -13,17 +13,17 @@ "coverage": "vitest run --coverage" }, "peerDependencies": { - "starknet": "6.7.0" + "starknet": "6.11.0" }, "dependencies": { "@bibliothecadao/eternum": "workspace:^", - "@dojoengine/core": "1.0.0-alpha.14", - "@dojoengine/create-burner": "1.0.0-alpha.14", - "@dojoengine/react": "1.0.0-alpha.14", + "@dojoengine/core": "1.0.0-alpha.22", + "@dojoengine/create-burner": "1.0.0-alpha.22", + "@dojoengine/react": "1.0.0-alpha.22", "@dojoengine/recs": "^2.0.13", - "@dojoengine/state": "1.0.0-alpha.14", - "@dojoengine/torii-client": "1.0.0-alpha.14", - "@dojoengine/utils": "1.0.0-alpha.14", + "@dojoengine/state": "1.0.0-alpha.22", + "@dojoengine/torii-client": "1.0.0-alpha.22", + "@dojoengine/utils": "1.0.0-alpha.22", "@headlessui/react": "^1.7.18", "@latticexyz/utils": "^2.0.0-next.12", "@radix-ui/react-select": "^2.0.0", diff --git a/client/src/assets/icons/Coins.svg b/client/src/assets/icons/Coins.svg index b6b5eab59..2070b60d7 100644 --- a/client/src/assets/icons/Coins.svg +++ b/client/src/assets/icons/Coins.svg @@ -1,3 +1,3 @@ - + diff --git a/client/src/assets/icons/Crown.svg b/client/src/assets/icons/Crown.svg index cf235b7cb..86a1a321b 100644 --- a/client/src/assets/icons/Crown.svg +++ b/client/src/assets/icons/Crown.svg @@ -1,3 +1,3 @@ - + diff --git a/client/src/assets/icons/Scroll.svg b/client/src/assets/icons/Scroll.svg index b48d7d82c..f9e6a69e2 100644 --- a/client/src/assets/icons/Scroll.svg +++ b/client/src/assets/icons/Scroll.svg @@ -1,3 +1,3 @@ - + diff --git a/client/src/assets/icons/Sparkles.svg b/client/src/assets/icons/Sparkles.svg index 28c21c02e..fb08b80ba 100644 --- a/client/src/assets/icons/Sparkles.svg +++ b/client/src/assets/icons/Sparkles.svg @@ -1,3 +1,3 @@ - + diff --git a/client/src/assets/icons/Swap.svg b/client/src/assets/icons/Swap.svg index 640193acf..2f15f40b3 100644 --- a/client/src/assets/icons/Swap.svg +++ b/client/src/assets/icons/Swap.svg @@ -1,3 +1,3 @@ - + diff --git a/client/src/data/orders.ts b/client/src/data/orders.ts index f73106541..86fc401db 100644 --- a/client/src/data/orders.ts +++ b/client/src/data/orders.ts @@ -1,23 +1,22 @@ export const order_statments = [ - "The Order of Power are one of the oldest orders and fixtures of the history of the realms. They compete by working harder and smarter than everyone else, are natural leaders, and have endless self-confidence. Their confidence can also be their greatest weakness.", - "The Order of Anger loves the intensity of high strung situations, their emotions welling deep inside to provide fuel for the challenge. They are always seeking conflict and particularly so with the Order of Protection, who despises their constant troublemaking. Their need for confrontation can often be their downfall, as there is always a bigger fish.", - "The Order of Brilliance believes that true perfection lies in the school of thought. They are renowned for their academies, in which they provide basic education in order to find the most promising minds. In the lifelong pursuit of knowledge they catalog and store the history of the realms and conduct extensive research into mana usage.", - "The Order of Detection are attuned to empathy. Some use it for good, some use it for manipulation. They can communicate wordlessly. Their leadership hierarchy is through persuasion not physical power, and are masters of propoganda. Their rumors can be enough to change the tides of fate though, as an army location is misreported or a merchant caravan route leaked.", - "The Order of Enlightenment seeks harmony and peace above all else, instead using their intelligence to help guide the world and those around them. The path to enlightenment is unique to every individual, and this Order wishes to turn swords into plows wherever they go. As skilled diplomats many seek them out for negotiations, their wisdom and countenance more valuble than gold. Although strangers to violence in general, their individual defensive measures can be extreme.", + "The Order of Giants values physical might, not just to crush their foes, but as the backbone of the great constructions in the Realms, their strength making them invaluable to Builders. Their size also makes them vulnerable to faster and smaller opponents and their leaders often win their position through combat prowess, which has led to a culture of poor strategic decisions in preference of displaying their full strength.", + "The Order of Perfection loves to both appreciate and create beautiful things. They disregard confrontation in favor of creation, appreciation, and protection of art and and refinement of culture. Although often dismissed as just artists and bards, those with intelligence know that the most dangerous creations originate from the Order of Perfection, and hope to never see those creations come to light.", + "The Order of Rage was said to be birthed from the Order of Anger, as not all could display their anger without losing their life. Instead, the Order of Rage values an implacable front to hide a deep well of anger. When they reach positions of power, they tend to enact schemes of revenge on those who hurt them, and in doing so often dig a grave for two.", "The order of the Fox thrives off of mischief and competition. They can hide, then are sly, they compete with cunning and trickery, not raw strength. The children of the Fox play tricks on other orders, and with age those trucks become increasingly severe.", + "The Twins are another one of the oldest orders. Amongst themselves, they tend to morph to the styles and sensibilities of the most dominant member of their order. They move in-sync with one-another and live communally. Amongst other orders, they have an uncanny ability to mimic the mannerisms and patterns of those they choose. They can change personalities as one would change outfits.", "The Order of Fury values extreme and overwhelming force in all situations. They have a need to dominate, and if they cannot dominate, to destroy it with utter frenzied rage. The Order of Fury has little care for life itself, only the ability to end a life. Their disciples are pitted against each other over years until only the most vicious remain. The nature of the Order ensures competency, but destroys its own power structures constantly due to infighting and assasinations.", - "The Order of Giants values physical might, not just to crush their foes, but as the backbone of the great constructions in the Realms, their strength making them invaluable to Builders. Their size also makes them vulnerable to faster and smaller opponents and their leaders often win their position through combat prowess, which has led to a culture of poor strategic decisions in preference of displaying their full strength.", - "The Order of Perfection loves to both appreciate and create beautiful things. They disregard confrontation in favor of creation, appreciation, and protection of art and and refinement of culture. Although often dismissed as just artists and bards, those with intelligence know that the most dangerous creations originate from the Order of Perfection, and hope to never see those creations come to light.", "The order of Reflection thrives off of time spent with their own thoughts, believing that interaction with an everchanging Realm dulls their ability to see the ethereal. They’re most often found searching for insights into the past and the future by isolating themselves in the darkness. There are rumors of disturbing rituals to aid in conjuring the visions they seek, and their trade in the less savoury markets banned in some Realms does nothing to dispel these rumors.", + "The Order of Detection are attuned to empathy. Some use it for good, some use it for manipulation. They can communicate wordlessly. Their leadership hierarchy is through persuasion not physical power, and are masters of propoganda. Their rumors can be enough to change the tides of fate though, as an army location is misreported or a merchant caravan route leaked.", "The Order of Skill thrives off of constant self-improvement and self-reliance but still prefer to be part of a team. They are energetic and can accomplish any task they set their mind to. They play by the rules and compete based on their own merit. Their need to demonstrate their proficiency can perversely lead to the very opposite, when they show their hand too early in battle or political games.", + "The Order of Brilliance believes that true perfection lies in the school of thought. They are renowned for their academies, in which they provide basic education in order to find the most promising minds. In the lifelong pursuit of knowledge they catalog and store the history of the realms and conduct extensive research into mana usage.", + "The Order of Protection values stability between the Realms, they wish to build economies to help feed and clothe the destitute. They will often create neutral zones such as Inns whereby adventurers can rest at ease. Their desire to do good can often be their downfall, as not all pay kindness for kindness, especially those of the Dark.", + "The Order of Power are one of the oldest orders and fixtures of the history of the realms. They compete by working harder and smarter than everyone else, are natural leaders, and have endless self-confidence. Their confidence can also be their greatest weakness.", "The Order of Titans are said to have come from the Giants and share many similarities. The people say that the Giants are like earth, and the Titans the metal they find within it. They are strongly attuned to a sense of justice in the world, however their justice can vary greatly. For Titans, going against the Lord or Lady of a realm is an unforgivable sin, they play by the rules to a fault and can be caught up in their own rules by a skilled or less honourable enemy.", - "The Twins are another one of the oldest orders. Amongst themselves, they tend to morph to the styles and sensibilities of the most dominant member of their order. They move in-sync with one-another and live communally. Amongst other orders, they have an uncanny ability to mimic the mannerisms and patterns of those they choose. They can change personalities as one would change outfits.", "The order of Vitriol loves debate and offers their opinions with complete disregard to the feelings of others. From an early age they face ruthless criticism from their teachers, parents, and leaders. They hold both themselves and others to impossible standards.", - "The Order of Rage was said to be birthed from the Order of Anger, as not all could display their anger without losing their life. Instead, the Order of Rage values an implacable front to hide a deep well of anger. When they reach positions of power, they tend to enact schemes of revenge on those who hurt them, and in doing so often dig a grave for two.", - - "The Order of Protection values stability between the Realms, they wish to build economies to help feed and clothe the destitute. They will often create neutral zones such as Inns whereby adventurers can rest at ease. Their desire to do good can often be their downfall, as not all pay kindness for kindness, especially those of the Dark.", + "The Order of Anger loves the intensity of high strung situations, their emotions welling deep inside to provide fuel for the challenge. They are always seeking conflict and particularly so with the Order of Protection, who despises their constant troublemaking. Their need for confrontation can often be their downfall, as there is always a bigger fish.", + "The Order of Enlightenment seeks harmony and peace above all else, instead using their intelligence to help guide the world and those around them. The path to enlightenment is unique to every individual, and this Order wishes to turn swords into plows wherever they go. As skilled diplomats many seek them out for negotiations, their wisdom and countenance more valuble than gold. Although strangers to violence in general, their individual defensive measures can be extreme.", ]; diff --git a/client/src/dojo/contractComponents.ts b/client/src/dojo/contractComponents.ts index b668bda19..f2d828433 100644 --- a/client/src/dojo/contractComponents.ts +++ b/client/src/dojo/contractComponents.ts @@ -381,20 +381,6 @@ export function defineContractComponents(world: World) { }, ); })(), - EntityMetadata: (() => { - return defineComponent( - world, - { entity_id: RecsType.Number, entity_type: RecsType.Number }, - { - metadata: { - namespace: "eternum", - name: "EntityMetadata", - types: ["u32", "u32"], - customTypes: [], - }, - }, - ); - })(), EntityName: (() => { return defineComponent( world, @@ -479,14 +465,14 @@ export function defineContractComponents(world: World) { }, ); })(), - HasClaimedStartingResources: (() => { + Quest: (() => { return defineComponent( world, - { entity_id: RecsType.Number, config_id: RecsType.Number, claimed: RecsType.Boolean }, + { entity_id: RecsType.Number, config_id: RecsType.Number, completed: RecsType.Boolean }, { metadata: { namespace: "eternum", - name: "HasClaimedStartingResources", + name: "Quest", types: ["u32", "u32", "bool"], customTypes: [], }, @@ -516,14 +502,14 @@ export function defineContractComponents(world: World) { completed: RecsType.Boolean, last_updated_by: RecsType.BigInt, last_updated_timestamp: RecsType.Number, - private: RecsType.Boolean, + access: RecsType.String, }, { metadata: { namespace: "eternum", name: "Hyperstructure", - types: ["u32", "u16", "bool", "contractaddress", "u64", "bool"], - customTypes: [], + types: ["u32", "u16", "bool", "contractaddress", "u64", "enum"], + customTypes: ["Access"], }, }, ); @@ -551,12 +537,18 @@ export function defineContractComponents(world: World) { HyperstructureConfig: (() => { return defineComponent( world, - { config_id: RecsType.Number, time_between_shares_change: RecsType.Number }, + { + config_id: RecsType.Number, + time_between_shares_change: RecsType.Number, + points_per_cycle: RecsType.BigInt, + points_for_win: RecsType.BigInt, + points_on_completion: RecsType.BigInt, + }, { metadata: { namespace: "eternum", name: "HyperstructureConfig", - types: ["u32", "u64"], + types: ["u32", "u64", "u128", "u128", "u128"], customTypes: [], }, }, @@ -942,13 +934,7 @@ export function defineContractComponents(world: World) { { entity_id: RecsType.Number, realm_id: RecsType.Number, - resource_types_packed: RecsType.BigInt, - resource_types_count: RecsType.Number, - cities: RecsType.Number, - harbors: RecsType.Number, - rivers: RecsType.Number, - regions: RecsType.Number, - wonder: RecsType.Number, + produced_resources: RecsType.BigInt, order: RecsType.Number, level: RecsType.Number, }, @@ -956,26 +942,68 @@ export function defineContractComponents(world: World) { metadata: { namespace: "eternum", name: "Realm", - types: ["u32", "u32", "u128", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8"], + types: ["u32", "u32", "u128", "u8", "u8"], customTypes: [], }, }, ); })(), - RealmFreeMintConfig: (() => { + QuestConfig: (() => { return defineComponent( world, - { config_id: RecsType.Number, detached_resource_id: RecsType.Number, detached_resource_count: RecsType.Number }, + { config_id: RecsType.Number, production_material_multiplier: RecsType.Number }, { metadata: { namespace: "eternum", - name: "RealmFreeMintConfig", + name: "QuestConfig", + types: ["u32", "u16"], + customTypes: [], + }, + }, + ); + })(), + QuestRewardConfig: (() => { + return defineComponent( + world, + { quest_id: RecsType.Number, detached_resource_id: RecsType.Number, detached_resource_count: RecsType.Number }, + { + metadata: { + namespace: "eternum", + name: "QuestRewardConfig", types: ["u32", "u32", "u32"], customTypes: [], }, }, ); })(), + RealmLevelConfig: (() => { + return defineComponent( + world, + { level: RecsType.Number, required_resources_id: RecsType.Number, required_resource_count: RecsType.Number }, + { + metadata: { + namespace: "eternum", + name: "RealmLevelConfig", + types: ["u8", "u32", "u8"], + customTypes: [], + }, + }, + ); + })(), + RealmMaxLevelConfig: (() => { + return defineComponent( + world, + { config_id: RecsType.Number, max_level: RecsType.Number }, + { + metadata: { + namespace: "eternum", + name: "RealmMaxLevelConfig", + types: ["u32", "u8"], + customTypes: [], + }, + }, + ); + })(), Resource: (() => { return defineComponent( world, @@ -1201,6 +1229,27 @@ export function defineContractComponents(world: World) { }, ); })(), + TravelFoodCostConfig: (() => { + return defineComponent( + world, + { + config_id: RecsType.Number, + unit_type: RecsType.Number, + explore_wheat_burn_amount: RecsType.BigInt, + explore_fish_burn_amount: RecsType.BigInt, + travel_wheat_burn_amount: RecsType.BigInt, + travel_fish_burn_amount: RecsType.BigInt, + }, + { + metadata: { + namespace: "eternum", + name: "TravelFoodCostConfig", + types: ["u32", "u8", "u128", "u128", "u128", "u128"], + customTypes: [], + }, + }, + ); + })(), TravelStaminaCostConfig: (() => { return defineComponent( world, @@ -1230,6 +1279,7 @@ export function defineContractComponents(world: World) { crossbowman_strength: RecsType.Number, advantage_percent: RecsType.Number, disadvantage_percent: RecsType.Number, + max_troop_count: RecsType.Number, pillage_health_divisor: RecsType.Number, army_free_per_structure: RecsType.Number, army_extra_per_building: RecsType.Number, @@ -1242,7 +1292,7 @@ export function defineContractComponents(world: World) { metadata: { namespace: "eternum", name: "TroopConfig", - types: ["u32", "u32", "u8", "u8", "u16", "u16", "u16", "u8", "u8", "u8", "u8", "u8", "u8", "u16"], + types: ["u32", "u32", "u8", "u8", "u16", "u16", "u16", "u64", "u8", "u8", "u8", "u8", "u8", "u8", "u16"], customTypes: [], }, }, @@ -1654,8 +1704,7 @@ const eventsComponents = (world: World) => { owner_address: RecsType.BigInt, owner_name: RecsType.BigInt, realm_name: RecsType.BigInt, - resource_types_packed: RecsType.BigInt, - resource_types_count: RecsType.Number, + produced_resources: RecsType.BigInt, cities: RecsType.Number, harbors: RecsType.Number, rivers: RecsType.Number, @@ -1684,7 +1733,6 @@ const eventsComponents = (world: World) => { "u8", "u8", "u8", - "u8", "u32", "u32", "u64", diff --git a/client/src/dojo/createSystemCalls.ts b/client/src/dojo/createSystemCalls.ts index c6171b295..279fec761 100644 --- a/client/src/dojo/createSystemCalls.ts +++ b/client/src/dojo/createSystemCalls.ts @@ -215,18 +215,14 @@ export function createSystemCalls({ provider }: SetupNetworkResult) { await provider.army_merge_troops(props); }; - const mint_starting_resources = async (props: SystemProps.MintStartingResources) => { - await provider.mint_starting_resources(props); + const claim_quest = async (props: SystemProps.ClaimQuestProps) => { + await provider.claim_quest(props); }; const mint_resources = async (props: SystemProps.MintResourcesProps) => { await provider.mint_resources(props); }; - const mint_resources_and_claim_quest = async (props: SystemProps.MintResourcesAndClaimProps) => { - await provider.mint_resources_and_claim_quest(props); - }; - const create_hyperstructure = async (props: SystemProps.CreateHyperstructureProps) => { await provider.create_hyperstructure(props); }; @@ -235,8 +231,8 @@ export function createSystemCalls({ provider }: SetupNetworkResult) { await provider.contribute_to_construction(props); }; - const set_private = async (props: SystemProps.SetPrivateProps) => { - await provider.set_private(props); + const set_access = async (props: SystemProps.SetAccessProps) => { + await provider.set_access(props); }; const end_game = async (props: SystemProps.EndGameProps) => { @@ -350,13 +346,12 @@ export function createSystemCalls({ provider }: SetupNetworkResult) { create_hyperstructure: withQueueing(withErrorHandling(create_hyperstructure)), contribute_to_construction: withQueueing(withErrorHandling(contribute_to_construction)), - set_private: withQueueing(withErrorHandling(set_private)), + set_access: withQueueing(withErrorHandling(set_access)), set_co_owners: withQueueing(withErrorHandling(set_co_owners)), end_game: withQueueing(withErrorHandling(end_game)), + claim_quest: withQueueing(withErrorHandling(claim_quest)), mint_resources: withQueueing(withErrorHandling(mint_resources)), - mint_starting_resources: withQueueing(withErrorHandling(mint_starting_resources)), - mint_resources_and_claim_quest: withQueueing(withErrorHandling(mint_resources_and_claim_quest)), army_buy_troops: withQueueing(withErrorHandling(army_buy_troops)), army_merge_troops: withQueueing(withErrorHandling(army_merge_troops)), diff --git a/client/src/dojo/modelManager/ArmyMovementManager.ts b/client/src/dojo/modelManager/ArmyMovementManager.ts index fcc1189ef..8c602cbef 100644 --- a/client/src/dojo/modelManager/ArmyMovementManager.ts +++ b/client/src/dojo/modelManager/ArmyMovementManager.ts @@ -9,15 +9,13 @@ import { import { CapacityConfigCategory, ContractAddress, - EternumGlobalConfig, ID, ResourcesIds, getDirectionBetweenAdjacentHexes, getNeighborHexes, } from "@bibliothecadao/eternum"; -import { getComponentValue, type ComponentValue, type Entity } from "@dojoengine/recs"; +import { getComponentValue, type Entity } from "@dojoengine/recs"; import { uuid } from "@latticexyz/utils"; -import { type ClientComponents } from "../createClientComponents"; import { configManager, type SetupResult } from "../setup"; import { ProductionManager } from "./ProductionManager"; import { StaminaManager } from "./StaminaManager"; @@ -79,7 +77,6 @@ export class ArmyMovementManager { private readonly fishManager: ProductionManager; private readonly wheatManager: ProductionManager; private readonly staminaManager: StaminaManager; - private readonly entityArmy: ComponentValue; constructor( private readonly setup: SetupResult, @@ -89,16 +86,6 @@ export class ArmyMovementManager { this.entityId = entityId; this.address = ContractAddress(this.setup.network.burnerManager.account?.address || 0n); const entityOwnerId = getComponentValue(this.setup.components.EntityOwner, this.entity); - this.entityArmy = getComponentValue(this.setup.components.Army, this.entity) ?? { - entity_id: 0, - troops: { - knight_count: 0n, - paladin_count: 0n, - crossbowman_count: 0n, - }, - battle_id: 0, - battle_side: "", - }; this.wheatManager = new ProductionManager(this.setup, entityOwnerId!.entity_owner_id, ResourcesIds.Wheat); this.fishManager = new ProductionManager(this.setup, entityOwnerId!.entity_owner_id, ResourcesIds.Fish); this.staminaManager = new StaminaManager(this.setup, entityId); @@ -107,12 +94,13 @@ export class ArmyMovementManager { private _canExplore(currentDefaultTick: number, currentArmiesTick: number): boolean { const stamina = this.staminaManager.getStamina(currentArmiesTick); - if (stamina.amount < configManager.getStaminaExploreConfig()) { + if (stamina.amount < configManager.getExploreStaminaCost()) { return false; } - const { wheat, fish } = this.getFood(currentDefaultTick); - const exploreFoodCosts = computeExploreFoodCosts(this.entityArmy.troops); + const entityArmy = getComponentValue(this.setup.components.Army, this.entity); + const exploreFoodCosts = computeExploreFoodCosts(entityArmy?.troops); + const { wheat, fish } = this.getFood(currentDefaultTick); if (fish < exploreFoodCosts.fishPayAmount) { return false; @@ -121,7 +109,7 @@ export class ArmyMovementManager { return false; } - if (this._getArmyRemainingCapacity() < EternumGlobalConfig.exploration.reward) { + if (this._getArmyRemainingCapacity() < configManager.getExploreReward()) { return false; } @@ -130,9 +118,10 @@ export class ArmyMovementManager { private readonly _calculateMaxTravelPossible = (currentDefaultTick: number, currentArmiesTick: number) => { const stamina = this.staminaManager.getStamina(currentArmiesTick); - const maxStaminaSteps = Math.floor((stamina.amount || 0) / configManager.getStaminaTravelConfig()); + const maxStaminaSteps = Math.floor((stamina.amount || 0) / configManager.getTravelStaminaCost()); - const travelFoodCosts = computeTravelFoodCosts(this.entityArmy.troops); + const entityArmy = getComponentValue(this.setup.components.Army, this.entity); + const travelFoodCosts = computeTravelFoodCosts(entityArmy?.troops); const { wheat, fish } = this.getFood(currentDefaultTick); const maxTravelWheatSteps = Math.floor(wheat / multiplyByPrecision(travelFoodCosts.wheatPayAmount)); @@ -258,7 +247,7 @@ export class ArmyMovementManager { private readonly _optimisticExplore = (col: number, row: number, currentArmiesTick: number) => { const overrideId = uuid(); - this._optimisticStaminaUpdate(overrideId, EternumGlobalConfig.stamina.exploreCost, currentArmiesTick); + this._optimisticStaminaUpdate(overrideId, configManager.getExploreStaminaCost(), currentArmiesTick); this._optimisticTileUpdate(overrideId, col, row); this._optimisticPositionUpdate(overrideId, col, row); @@ -297,7 +286,7 @@ export class ArmyMovementManager { private readonly _optimisticTravelHex = (col: number, row: number, pathLength: number, currentArmiesTick: number) => { const overrideId = uuid(); - this._optimisticStaminaUpdate(overrideId, EternumGlobalConfig.stamina.travelCost * pathLength, currentArmiesTick); + this._optimisticStaminaUpdate(overrideId, configManager.getTravelStaminaCost() * pathLength, currentArmiesTick); this.setup.components.Position.addOverride(overrideId, { entity: this.entity, diff --git a/client/src/dojo/modelManager/BattleManager.ts b/client/src/dojo/modelManager/BattleManager.ts index 0235e9853..69b04a582 100644 --- a/client/src/dojo/modelManager/BattleManager.ts +++ b/client/src/dojo/modelManager/BattleManager.ts @@ -2,6 +2,7 @@ import { DojoResult } from "@/hooks/context/DojoContext"; import { ArmyInfo } from "@/hooks/helpers/useArmies"; import { Structure } from "@/hooks/helpers/useStructures"; import { Health } from "@/types"; +import { multiplyByPrecision } from "@/ui/utils/utils"; import { BattleSide, EternumGlobalConfig, ID } from "@bibliothecadao/eternum"; import { ComponentValue, @@ -14,6 +15,7 @@ import { } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { ClientComponents } from "../createClientComponents"; +import { ClientConfigManager } from "./ConfigManager"; import { StaminaManager } from "./StaminaManager"; export enum BattleType { @@ -56,6 +58,7 @@ export enum ClaimStatus { Claimable = "Claim", NoSelectedArmy = "No selected army", BattleOngoing = "Battle ongoing", + DefenderPresent = "An army's defending the structure", NoStructureToClaim = "No structure to claim", StructureIsMine = "Can't claim your own structure", SelectedArmyIsDead = "Selected army is dead", @@ -66,10 +69,12 @@ export class BattleManager { dojo: DojoResult; battleType: BattleType | undefined; private battleIsClaimable: ClaimStatus | undefined; + private configManager: ClientConfigManager; constructor(battleEntityId: ID, dojo: DojoResult) { this.battleEntityId = battleEntityId; this.dojo = dojo; + this.configManager = ClientConfigManager.instance(); } public getUpdatedBattle(currentTimestamp: number) { @@ -202,44 +207,29 @@ export class BattleManager { battle_army_lifetime = updatedBattle.attack_army_lifetime; } - cloneArmy.troops.knight_count = - cloneArmy.troops.knight_count === 0n - ? 0n - : BigInt( - Math.floor( - Number(cloneArmy.troops.knight_count) * - this.getRemainingPercentageOfTroops( - battle_army.troops.knight_count, - battle_army_lifetime.troops.knight_count, - ), - ), - ); - - cloneArmy.troops.paladin_count = - cloneArmy.troops.paladin_count === 0n - ? 0n - : BigInt( - Math.floor( - Number(cloneArmy.troops.paladin_count) * - this.getRemainingPercentageOfTroops( - battle_army.troops.paladin_count, - battle_army_lifetime.troops.paladin_count, - ), - ), - ); - - cloneArmy.troops.crossbowman_count = - cloneArmy.troops.crossbowman_count === 0n - ? 0n - : BigInt( - Math.floor( - Number(cloneArmy.troops.crossbowman_count) * - this.getRemainingPercentageOfTroops( - battle_army.troops.crossbowman_count, - battle_army_lifetime.troops.crossbowman_count, - ), - ), - ); + const remainingKnights = + Number(cloneArmy.troops.knight_count) * + this.getRemainingPercentageOfTroops(battle_army.troops.knight_count, battle_army_lifetime.troops.knight_count); + cloneArmy.troops.knight_count = BigInt( + remainingKnights - (remainingKnights % EternumGlobalConfig.resources.resourcePrecision), + ); + + const remainingPaladins = + Number(cloneArmy.troops.paladin_count) * + this.getRemainingPercentageOfTroops(battle_army.troops.paladin_count, battle_army_lifetime.troops.paladin_count); + cloneArmy.troops.paladin_count = BigInt( + remainingPaladins - (remainingPaladins % EternumGlobalConfig.resources.resourcePrecision), + ); + + const remainingCrossbowmen = + Number(cloneArmy.troops.crossbowman_count) * + this.getRemainingPercentageOfTroops( + battle_army.troops.crossbowman_count, + battle_army_lifetime.troops.crossbowman_count, + ); + cloneArmy.troops.crossbowman_count = BigInt( + remainingCrossbowmen - (remainingCrossbowmen % EternumGlobalConfig.resources.resourcePrecision), + ); cloneArmy.health.current = this.getTroopFullHealth(cloneArmy.troops); @@ -253,36 +243,30 @@ export class BattleManager { defender: ArmyInfo | undefined, ): ClaimStatus { if (!selectedArmy) return ClaimStatus.NoSelectedArmy; - if (this.battleIsClaimable) return this.battleIsClaimable; if (this.isBattleOngoing(currentTimestamp)) { return ClaimStatus.BattleOngoing; } if (!structure) { - this.battleIsClaimable = ClaimStatus.NoStructureToClaim; return ClaimStatus.NoStructureToClaim; } if (this.getBattleType(structure) !== BattleType.Structure) { - this.battleIsClaimable = ClaimStatus.NoStructureToClaim; return ClaimStatus.NoStructureToClaim; } if (defender === undefined) { - this.battleIsClaimable = ClaimStatus.Claimable; return ClaimStatus.Claimable; } const updatedBattle = this.getUpdatedBattle(currentTimestamp); if (updatedBattle && updatedBattle.defence_army_health.current > 0n) { - this.battleIsClaimable = ClaimStatus.BattleOngoing; return ClaimStatus.BattleOngoing; } if (defender.health.current > 0n) { - this.battleIsClaimable = ClaimStatus.BattleOngoing; - return ClaimStatus.BattleOngoing; + return ClaimStatus.DefenderPresent; } if (structure.isMine) { @@ -293,7 +277,6 @@ export class BattleManager { return ClaimStatus.SelectedArmyIsDead; } - this.battleIsClaimable = ClaimStatus.Claimable; return ClaimStatus.Claimable; } @@ -412,15 +395,15 @@ export class BattleManager { } private getTroopFullHealth(troops: ComponentValue): bigint { - const health = EternumGlobalConfig.troop.health; + const troopHealth = this.configManager.getTroopConfig().health; - let total_knight_health = health * Number(troops.knight_count); - let total_paladin_health = health * Number(troops.paladin_count); - let total_crossbowman_health = health * Number(troops.crossbowman_count); + let totalKnightHealth = troopHealth * Number(troops.knight_count); + let totalPaladinHealth = troopHealth * Number(troops.paladin_count); + let totalCrossbowmanHealth = troopHealth * Number(troops.crossbowman_count); return BigInt( Math.floor( - (total_knight_health + total_paladin_health + total_crossbowman_health) / + (totalKnightHealth + totalPaladinHealth + totalCrossbowmanHealth) / EternumGlobalConfig.resources.resourceMultiplier, ), ); @@ -461,23 +444,23 @@ export class BattleManager { } return { - knight_count, - paladin_count, - crossbowman_count, + knight_count: knight_count - (knight_count % BigInt(EternumGlobalConfig.resources.resourcePrecision)), + paladin_count: paladin_count - (paladin_count % BigInt(EternumGlobalConfig.resources.resourcePrecision)), + crossbowman_count: + crossbowman_count - (crossbowman_count % BigInt(EternumGlobalConfig.resources.resourcePrecision)), }; }; private updateHealth(battle: ComponentValue, currentTimestamp: number) { const durationPassed: number = this.getElapsedTime(currentTimestamp); + const troopHealth = this.configManager.getTroopConfig().health; + const attackDelta = this.attackingDelta(battle); const defenceDelta = this.defendingDelta(battle); battle.attack_army_health.current = this.getUdpdatedHealth(defenceDelta, battle.attack_army_health, durationPassed); - if ( - battle.attack_army_health.current < - EternumGlobalConfig.troop.health * EternumGlobalConfig.resources.resourcePrecision - ) { + if (battle.attack_army_health.current < multiplyByPrecision(troopHealth)) { battle.attack_army_health.current = 0n; } @@ -486,10 +469,7 @@ export class BattleManager { battle.defence_army_health, durationPassed, ); - if ( - battle.defence_army_health.current < - EternumGlobalConfig.troop.health * EternumGlobalConfig.resources.resourcePrecision - ) { + if (battle.defence_army_health.current < multiplyByPrecision(troopHealth)) { battle.defence_army_health.current = 0n; } } @@ -519,7 +499,7 @@ export class BattleManager { return damages > health.current ? 0n : health.current - damages; } - private getRemainingPercentageOfTroops(current_troops: bigint, lifetime_troops: bigint) { + private getRemainingPercentageOfTroops(current_troops: bigint, lifetime_troops: bigint): number { if (lifetime_troops === 0n) return 0; return Number(current_troops) / Number(lifetime_troops); } diff --git a/client/src/dojo/modelManager/ConfigManager.ts b/client/src/dojo/modelManager/ConfigManager.ts index ed41aa498..417b1b3f2 100644 --- a/client/src/dojo/modelManager/ConfigManager.ts +++ b/client/src/dojo/modelManager/ConfigManager.ts @@ -1,47 +1,508 @@ -import { TravelTypes, WORLD_CONFIG_ID } from "@bibliothecadao/eternum"; +import { divideByPrecision } from "@/ui/utils/utils"; +import { + ADMIN_BANK_ENTITY_ID, + BUILDING_CATEGORY_POPULATION_CONFIG_ID, + BuildingType, + CapacityConfigCategory, + EternumGlobalConfig, + HYPERSTRUCTURE_CONFIG_ID, + POPULATION_CONFIG_ID, + ResourcesIds, + StructureType, + TickIds, + TravelTypes, + WORLD_CONFIG_ID, +} from "@bibliothecadao/eternum"; import { getComponentValue } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; - import { ContractComponents } from "../contractComponents"; export class ClientConfigManager { private static _instance: ClientConfigManager; private components!: ContractComponents; + resourceInputs: Record = {}; + resourceOutputs: Record = {}; + hyperstructureTotalCosts: Record = {}; + realmUpgradeCosts: Record = {}; + buildingCosts: Record = {}; + resourceBuildingCosts: Record = {}; + structureCosts: Record = {}; + public setDojo(components: ContractComponents) { this.components = components; + this.initializeResourceInputs(); + this.initializeHyperstructureTotalCosts(); + this.initializeRealmUpgradeCosts(); + this.initializeResourceBuildingCosts(); + this.initializeBuildingCosts(); + this.initializeStructureCosts(); } public static instance(): ClientConfigManager { if (!ClientConfigManager._instance) { ClientConfigManager._instance = new ClientConfigManager(); } + return ClientConfigManager._instance; } - getStaminaTravelConfig() { - const staminaConfig = getComponentValue( - this.components.TravelStaminaCostConfig, - getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(TravelTypes.Travel)]), - ); - return staminaConfig?.cost!; + private getValueOrDefault(callback: () => T, defaultValue: T): T { + if (!this.components) { + return defaultValue; + } + return callback(); + } + + private initializeResourceInputs() { + if (!this.components) return; + + for (const resourceType of Object.values(ResourcesIds).filter(Number.isInteger)) { + const productionConfig = getComponentValue( + this.components.ProductionConfig, + getEntityIdFromKeys([BigInt(resourceType)]), + ); + + const inputCount = productionConfig?.input_count ?? 0; + const inputs: { resource: ResourcesIds; amount: number }[] = []; + + for (let index = 0; index < inputCount; index++) { + const productionInput = getComponentValue( + this.components.ProductionInput, + getEntityIdFromKeys([BigInt(resourceType), BigInt(index)]), + ); + + if (productionInput) { + const resource = productionInput.input_resource_type; + const amount = divideByPrecision(Number(productionInput.input_resource_amount)); + inputs.push({ resource, amount }); + } + } + + this.resourceInputs[Number(resourceType)] = inputs; + } + } + + private initializeHyperstructureTotalCosts() { + const hyperstructureTotalCosts: { resource: ResourcesIds; amount: number }[] = []; + + for (const resourceId of Object.values(ResourcesIds).filter(Number.isInteger)) { + const hyperstructureResourceConfig = getComponentValue( + this.components.HyperstructureResourceConfig, + getEntityIdFromKeys([HYPERSTRUCTURE_CONFIG_ID, BigInt(resourceId)]), + ); + + const amount = + Number(hyperstructureResourceConfig?.amount_for_completion ?? 0) / + EternumGlobalConfig.resources.resourcePrecision; + + hyperstructureTotalCosts.push({ resource: resourceId as ResourcesIds, amount }); + + if (resourceId === ResourcesIds.AncientFragment) { + break; + } + } + + this.hyperstructureTotalCosts = hyperstructureTotalCosts; + } + + private initializeRealmUpgradeCosts() { + const realmMaxLevel = + getComponentValue(this.components.RealmMaxLevelConfig, getEntityIdFromKeys([WORLD_CONFIG_ID]))?.max_level ?? 0; + + for (let index = 1; index <= realmMaxLevel; index++) { + const realmLevelConfig = getComponentValue( + this.components.RealmLevelConfig, + getEntityIdFromKeys([BigInt(index)]), + ); + + const resourcesCount = realmLevelConfig?.required_resource_count ?? 0; + const detachedResourceId = realmLevelConfig?.required_resources_id ?? 0; + + const resources: { resource: ResourcesIds; amount: number }[] = []; + + for (let index = 0; index < resourcesCount; index++) { + const resource = getComponentValue( + this.components.DetachedResource, + getEntityIdFromKeys([BigInt(detachedResourceId), BigInt(index)]), + ); + if (resource) { + const resourceId = resource.resource_type; + const amount = divideByPrecision(Number(resource.resource_amount)); + resources.push({ resource: resourceId, amount }); + } + } + this.realmUpgradeCosts[index] = resources; + } + } + + private initializeResourceBuildingCosts() { + for (const resourceId of Object.values(ResourcesIds).filter(Number.isInteger)) { + const buildingConfig = getComponentValue( + this.components.BuildingConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(BuildingType.Resource), BigInt(resourceId)]), + ); + + const resourceCostCount = buildingConfig?.resource_cost_count || 0; + const resourceCostId = buildingConfig?.resource_cost_id || 0; + + const resourceCosts: { resource: ResourcesIds; amount: number }[] = []; + for (let index = 0; index < resourceCostCount; index++) { + const resourceCost = getComponentValue( + this.components.ResourceCost, + getEntityIdFromKeys([BigInt(resourceCostId), BigInt(index)]), + ); + if (!resourceCost) { + continue; + } + + const resourceType = resourceCost.resource_type; + const amount = Number(resourceCost.amount) / EternumGlobalConfig.resources.resourcePrecision; + + resourceCosts.push({ resource: resourceType, amount }); + } + this.resourceBuildingCosts[Number(resourceId)] = resourceCosts; + } + } + + private initializeBuildingCosts() { + for (const buildingType of Object.values(BuildingType).filter(Number.isInteger)) { + const resourceType = this.getResourceBuildingProduced(Number(buildingType)); + + const buildingConfig = getComponentValue( + this.components.BuildingConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(buildingType), BigInt(resourceType)]), + ); + + const resourceCostCount = buildingConfig?.resource_cost_count || 0; + const resourceCostId = buildingConfig?.resource_cost_id || 0; + + const resourceCosts: { resource: ResourcesIds; amount: number }[] = []; + for (let index = 0; index < resourceCostCount; index++) { + const resourceCost = getComponentValue( + this.components.ResourceCost, + getEntityIdFromKeys([BigInt(resourceCostId), BigInt(index)]), + ); + if (!resourceCost) { + continue; + } + + const resourceType = resourceCost.resource_type; + const amount = Number(resourceCost.amount) / this.getResourcePrecision(); + + resourceCosts.push({ resource: resourceType, amount }); + } + this.buildingCosts[Number(buildingType)] = resourceCosts; + } + } + + private initializeStructureCosts() { + this.structureCosts[StructureType.Hyperstructure] = this.getHyperstructureTotalCosts(); + } + + private getHyperstructureTotalCosts(): { + resource: ResourcesIds; + amount: number; + }[] { + const hyperstructureTotalCosts: { resource: ResourcesIds; amount: number }[] = []; + + for (const resourceId of Object.values(ResourcesIds).filter(Number.isInteger)) { + const entity = getEntityIdFromKeys([HYPERSTRUCTURE_CONFIG_ID, BigInt(resourceId)]); + const hyperstructureResourceConfig = getComponentValue(this.components.HyperstructureResourceConfig, entity); + const amount = divideByPrecision(Number(hyperstructureResourceConfig?.amount_for_completion)) ?? 0; + + hyperstructureTotalCosts.push({ resource: resourceId as ResourcesIds, amount }); + + if (resourceId === ResourcesIds.AncientFragment) { + break; + } + } + + return hyperstructureTotalCosts; + } + + getResourceWeight(resourceId: number): number { + return this.getValueOrDefault(() => { + const weightConfig = getComponentValue( + this.components.WeightConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(resourceId)]), + ); + return Number(weightConfig?.weight_gram ?? 0); + }, 0); + } + + getTravelStaminaCost() { + return this.getValueOrDefault(() => { + const staminaConfig = getComponentValue( + this.components.TravelStaminaCostConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(TravelTypes.Travel)]), + ); + return staminaConfig?.cost ?? 0; + }, 1); + } + + getExploreStaminaCost() { + return this.getValueOrDefault(() => { + const staminaConfig = getComponentValue( + this.components.TravelStaminaCostConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(TravelTypes.Explore)]), + ); + return staminaConfig?.cost ?? 0; + }, 1); } - getStaminaExploreConfig() { - const staminaConfig = getComponentValue( - this.components.TravelStaminaCostConfig, - getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(TravelTypes.Explore)]), + getExploreReward() { + return this.getValueOrDefault(() => { + const exploreConfig = getComponentValue(this.components.MapConfig, getEntityIdFromKeys([WORLD_CONFIG_ID])); + + return divideByPrecision(Number(exploreConfig?.reward_resource_amount ?? 0)); + }, 0); + } + + getTroopConfig() { + return this.getValueOrDefault( + () => { + const troopConfig = getComponentValue(this.components.TroopConfig, getEntityIdFromKeys([WORLD_CONFIG_ID])); + + return { + health: troopConfig?.health ?? 0, + knightStrength: troopConfig?.knight_strength ?? 0, + paladinStrength: troopConfig?.paladin_strength ?? 0, + crossbowmanStrength: troopConfig?.crossbowman_strength ?? 0, + advantagePercent: troopConfig?.advantage_percent ?? 0, + disadvantagePercent: troopConfig?.disadvantage_percent ?? 0, + maxTroopCount: divideByPrecision(troopConfig?.max_troop_count ?? 0), + pillageHealthDivisor: troopConfig?.pillage_health_divisor ?? 0, + baseArmyNumberForStructure: troopConfig?.army_free_per_structure ?? 0, + armyExtraPerMilitaryBuilding: troopConfig?.army_extra_per_building ?? 0, + maxArmiesPerStructure: troopConfig?.army_max_per_structure ?? 0, + battleLeaveSlashNum: troopConfig?.battle_leave_slash_num ?? 0, + battleLeaveSlashDenom: troopConfig?.battle_leave_slash_denom ?? 0, + battleTimeScale: troopConfig?.battle_time_scale ?? 0, + }; + }, + { + health: 0, + knightStrength: 0, + paladinStrength: 0, + crossbowmanStrength: 0, + advantagePercent: 0, + disadvantagePercent: 0, + maxTroopCount: 0, + pillageHealthDivisor: 0, + baseArmyNumberForStructure: 0, + armyExtraPerMilitaryBuilding: 0, + maxArmiesPerStructure: 0, + battleLeaveSlashNum: 0, + battleLeaveSlashDenom: 0, + battleTimeScale: 0, + }, ); - return staminaConfig?.cost!; } getBattleGraceTickCount() { - const battleConfig = getComponentValue(this.components.BattleConfig, getEntityIdFromKeys([WORLD_CONFIG_ID])); - return battleConfig?.battle_grace_tick_count!; + return this.getValueOrDefault(() => { + const battleConfig = getComponentValue(this.components.BattleConfig, getEntityIdFromKeys([WORLD_CONFIG_ID])); + return Number(battleConfig?.battle_grace_tick_count ?? 0); + }, 0); } getBattleDelay() { - const battleConfig = getComponentValue(this.components.BattleConfig, getEntityIdFromKeys([WORLD_CONFIG_ID])); - return battleConfig?.battle_delay_seconds!; + return this.getValueOrDefault(() => { + const battleConfig = getComponentValue(this.components.BattleConfig, getEntityIdFromKeys([WORLD_CONFIG_ID])); + + return Number(battleConfig?.battle_delay_seconds ?? 0); + }, 0); + } + + getTick(tickId: TickIds) { + return this.getValueOrDefault(() => { + const tickConfig = getComponentValue( + this.components.TickConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(tickId)]), + ); + + return Number(tickConfig?.tick_interval_in_seconds ?? 0); + }, 0); + } + + getBankConfig() { + return this.getValueOrDefault( + () => { + const bankConfig = getComponentValue(this.components.BankConfig, getEntityIdFromKeys([WORLD_CONFIG_ID])); + + return { + lordsCost: divideByPrecision(Number(bankConfig?.lords_cost)), + lpFeesNumerator: Number(bankConfig?.lp_fee_num ?? 0), + lpFeesDenominator: Number(bankConfig?.lp_fee_denom ?? 0), + }; + }, + { + lordsCost: 0, + lpFeesNumerator: 0, + lpFeesDenominator: 0, + }, + ); + } + + getAdminBankOwnerFee() { + const adminBank = getComponentValue(this.components.Bank, getEntityIdFromKeys([ADMIN_BANK_ENTITY_ID])); + + const numerator = Number(adminBank?.owner_fee_num) ?? 0; + const denominator = Number(adminBank?.owner_fee_denom) ?? 0; + return numerator / denominator; + } + + getAdminBankLpFee() { + const bankConfig = this.getBankConfig(); + + return bankConfig.lpFeesNumerator / bankConfig.lpFeesDenominator; + } + + getCapacityConfig(category: CapacityConfigCategory) { + return this.getValueOrDefault(() => { + const capacityConfig = getComponentValue(this.components.CapacityConfig, getEntityIdFromKeys([BigInt(category)])); + return Number(capacityConfig?.weight_gram ?? 0); + }, 0); + } + + getSpeedConfig(entityType: number): number { + return this.getValueOrDefault(() => { + const speedConfig = getComponentValue( + this.components.SpeedConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(entityType)]), + ); + + return speedConfig?.sec_per_km ?? 0; + }, 0); + } + + getBuildingPopConfig(buildingId: BuildingType): { + population: number; + capacity: number; + } { + return this.getValueOrDefault( + () => { + const buildingConfig = getComponentValue( + this.components.BuildingCategoryPopConfig, + getEntityIdFromKeys([BUILDING_CATEGORY_POPULATION_CONFIG_ID, BigInt(buildingId)]), + ); + + return { + population: buildingConfig?.population ?? 0, + capacity: buildingConfig?.capacity ?? 0, + }; + }, + { + population: 0, + capacity: 0, + }, + ); + } + + getHyperstructureConfig() { + return this.getValueOrDefault( + () => { + const hyperstructureConfig = getComponentValue( + this.components.HyperstructureConfig, + getEntityIdFromKeys([HYPERSTRUCTURE_CONFIG_ID]), + ); + + return { + timeBetweenSharesChange: hyperstructureConfig?.time_between_shares_change ?? 0, + pointsPerCycle: Number(hyperstructureConfig?.points_per_cycle) ?? 0, + pointsForWin: Number(hyperstructureConfig?.points_for_win) ?? 0, + pointsOnCompletion: Number(hyperstructureConfig?.points_on_completion) ?? 0, + }; + }, + { + timeBetweenSharesChange: 0, + pointsPerCycle: 0, + pointsForWin: 0, + pointsOnCompletion: 0, + }, + ); + } + + getHyperstructureTotalContributableAmount() { + return Object.values(this.hyperstructureTotalCosts).reduce((total, { resource, amount }) => { + return total + this.getResourceRarity(resource) * amount; + }, 0); + } + + getBasePopulationCapacity(): number { + return this.getValueOrDefault(() => { + return ( + getComponentValue(this.components.PopulationConfig, getEntityIdFromKeys([POPULATION_CONFIG_ID])) + ?.base_population ?? 0 + ); + }, 0); + } + + getResourceOutputs(resourceType: number): number { + return this.getValueOrDefault(() => { + const productionConfig = getComponentValue( + this.components.ProductionConfig, + getEntityIdFromKeys([BigInt(resourceType)]), + ); + + return Number(productionConfig?.amount ?? 0); + }, 0); + } + + getTravelFoodCostConfig(troopType: number) { + return this.getValueOrDefault( + () => { + const travelFoodCostConfig = getComponentValue( + this.components.TravelFoodCostConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(troopType)]), + ); + + return { + exploreWheatBurnAmount: Number(travelFoodCostConfig?.explore_wheat_burn_amount) ?? 0, + exploreFishBurnAmount: Number(travelFoodCostConfig?.explore_fish_burn_amount) ?? 0, + travelWheatBurnAmount: Number(travelFoodCostConfig?.travel_wheat_burn_amount) ?? 0, + travelFishBurnAmount: Number(travelFoodCostConfig?.travel_fish_burn_amount) ?? 0, + }; + }, + { + exploreWheatBurnAmount: 0, + exploreFishBurnAmount: 0, + travelWheatBurnAmount: 0, + travelFishBurnAmount: 0, + }, + ); + } + + getTroopStaminaConfig(troopId: number) { + return this.getValueOrDefault(() => { + const staminaConfig = getComponentValue( + this.components.StaminaConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(troopId)]), + ); + return staminaConfig?.max_stamina ?? 0; + }, 0); + } + + getResourceRarity(resourceId: ResourcesIds) { + return EternumGlobalConfig.resources.resourceRarity[resourceId] ?? 0; + } + + getResourcePrecision() { + return EternumGlobalConfig.resources.resourcePrecision; + } + + getResourceBuildingProduced(buildingType: BuildingType) { + return EternumGlobalConfig.buildings.buildingResourceProduced[buildingType] ?? 0; + } + + getBuildingBaseCostPercentIncrease() { + return this.getValueOrDefault(() => { + const buildingGeneralConfig = getComponentValue( + this.components.BuildingGeneralConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID]), + ); + return buildingGeneralConfig?.base_cost_percent_increase ?? 0; + }, 0); } } diff --git a/client/src/dojo/modelManager/LeaderboardManager.ts b/client/src/dojo/modelManager/LeaderboardManager.ts index a20dd9872..fa29154a4 100644 --- a/client/src/dojo/modelManager/LeaderboardManager.ts +++ b/client/src/dojo/modelManager/LeaderboardManager.ts @@ -1,13 +1,8 @@ import { GuildFromPlayerAddress } from "@/hooks/helpers/useGuilds"; -import { - ContractAddress, - EternumGlobalConfig, - HYPERSTRUCTURE_POINTS_ON_COMPLETION, - HYPERSTRUCTURE_POINTS_PER_CYCLE, - ID, -} from "@bibliothecadao/eternum"; +import { ContractAddress, ID, TickIds } from "@bibliothecadao/eternum"; import { ComponentValue } from "@dojoengine/recs"; import { ClientComponents } from "../createClientComponents"; +import { ClientConfigManager } from "./ConfigManager"; import { computeInitialContributionPoints } from "./utils/LeaderboardUtils"; export interface HyperstructureFinishedEvent { @@ -28,8 +23,11 @@ export class LeaderboardManager { private pointsOnCompletionPerPlayer; + private configManager: ClientConfigManager; + private constructor() { this.pointsOnCompletionPerPlayer = new Map>(); + this.configManager = ClientConfigManager.instance(); } public static instance() { @@ -90,6 +88,7 @@ export class LeaderboardManager { }; const contributions = getContributions(parsedEvent.hyperstructureEntityId); + const pointsOnCompletion = this.configManager.getHyperstructureConfig().pointsOnCompletion; contributions.forEach((contribution) => { if (!contribution) return; @@ -98,7 +97,7 @@ export class LeaderboardManager { const points = computeInitialContributionPoints( contribution.resource_type, contribution.amount, - HYPERSTRUCTURE_POINTS_ON_COMPLETION, + pointsOnCompletion, ); const playerPointsForEachHyperstructure = @@ -182,9 +181,9 @@ export class LeaderboardManager { const timePeriod = nextChange ? nextChange.timestamp - event.timestamp : currentTimestamp - event.timestamp; - const nbOfCycles = timePeriod / EternumGlobalConfig.tick.defaultTickIntervalInSeconds; + const nbOfCycles = timePeriod / this.configManager.getTick(TickIds.Default); - const totalPoints = nbOfCycles * HYPERSTRUCTURE_POINTS_PER_CYCLE; + const totalPoints = nbOfCycles * this.configManager.getHyperstructureConfig().pointsPerCycle; event.coOwners.forEach((coOwner) => { const key = getGuildFromPlayerAddress diff --git a/client/src/dojo/modelManager/MarketManager.ts b/client/src/dojo/modelManager/MarketManager.ts index a293ebdb3..722bbbd89 100644 --- a/client/src/dojo/modelManager/MarketManager.ts +++ b/client/src/dojo/modelManager/MarketManager.ts @@ -1,7 +1,7 @@ import { getEntityIdFromKeys } from "@/ui/utils/utils"; -import { ContractAddress, EternumGlobalConfig, ID, ResourcesIds } from "@bibliothecadao/eternum"; +import { ContractAddress, ID, ResourcesIds } from "@bibliothecadao/eternum"; import { getComponentValue } from "@dojoengine/recs"; -import { SetupResult } from "../setup"; +import { configManager, SetupResult } from "../setup"; export class MarketManager { bankEntityId: ID; @@ -89,7 +89,7 @@ export class MarketManager { } // Calculate the input amount after fee - const feeRateDenom = EternumGlobalConfig.banks.lpFeesDenominator; + const feeRateDenom = configManager.getBankConfig().lpFeesDenominator; const inputAmountWithFee = BigInt(inputAmount) * BigInt(feeRateDenom - feeRateNum); // Calculate output amount based on the constant product formula with fee @@ -114,7 +114,7 @@ export class MarketManager { } // Calculate the input amount after fee - const feeRateDenom = EternumGlobalConfig.banks.lpFeesDenominator; + const feeRateDenom = configManager.getBankConfig().lpFeesDenominator; const inputAmountWithFee = BigInt(inputAmount) * BigInt(feeRateDenom - feeRateNum); // Calculate output amount based on the constant product formula with fee @@ -140,7 +140,7 @@ export class MarketManager { if (!market) return 0; // Calculate the input amount of Lords needed to buy the desired amount of resource - const feeRateDenom = EternumGlobalConfig.banks.lpFeesDenominator; + const feeRateDenom = configManager.getBankConfig().lpFeesDenominator; const inputReserve = market.lords_amount; const outputReserve = market.resource_amount; @@ -172,7 +172,7 @@ export class MarketManager { if (!market) return 0; // Calculate the input amount of Resource needed to get the desired amount of Lords - const feeRateDenom = EternumGlobalConfig.banks.lpFeesDenominator; + const feeRateDenom = configManager.getBankConfig().lpFeesDenominator; const inputReserve = market.resource_amount; const outputReserve = market.lords_amount; diff --git a/client/src/dojo/modelManager/ProductionManager.ts b/client/src/dojo/modelManager/ProductionManager.ts index f59618eeb..cef845fb6 100644 --- a/client/src/dojo/modelManager/ProductionManager.ts +++ b/client/src/dojo/modelManager/ProductionManager.ts @@ -1,15 +1,7 @@ -import { getEntityIdFromKeys, gramToKg } from "@/ui/utils/utils"; -import { - BuildingType, - CapacityConfigCategory, - EternumGlobalConfig, - RESOURCE_INPUTS_SCALED, - ResourcesIds, - WEIGHTS_GRAM, - type ID, -} from "@bibliothecadao/eternum"; +import { getEntityIdFromKeys, gramToKg, multiplyByPrecision } from "@/ui/utils/utils"; +import { BuildingType, CapacityConfigCategory, ResourcesIds, type ID } from "@bibliothecadao/eternum"; import { getComponentValue } from "@dojoengine/recs"; -import { type SetupResult } from "../setup"; +import { configManager, type SetupResult } from "../setup"; export class ProductionManager { entityId: ID; @@ -37,7 +29,7 @@ export class ProductionManager { return production !== undefined && (production.building_count > 0 || production.consumption_rate > 0); } - public netRate(currentTick: number): [boolean, number] { + public netRate(): [boolean, number] { return this._netRate(this.resourceId); } @@ -57,6 +49,11 @@ export class ProductionManager { return this._balance(currentTick, this.resourceId); } + public timeUntilFinishTick(currentTick: number): number { + const finishTick = this._finish_tick(); + return finishTick > currentTick ? finishTick - currentTick : 0; + } + public timeUntilValueReached(currentTick: number, value: number): number { const production = this._getProduction(this.resourceId); const resource = this._getResource(this.resourceId); @@ -82,16 +79,13 @@ export class ProductionManager { } public getStoreCapacity(): number { + const storehouseCapacityKg = gramToKg(configManager.getCapacityConfig(CapacityConfigCategory.Storehouse)); const quantity = getComponentValue( this.setup.components.BuildingQuantityv2, getEntityIdFromKeys([BigInt(this.entityId || 0), BigInt(BuildingType.Storehouse)]), )?.value || 0; - return ( - (Number(quantity) * gramToKg(Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Storehouse])) + - gramToKg(Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Storehouse]))) * - EternumGlobalConfig.resources.resourcePrecision - ); + return multiplyByPrecision(Number(quantity) * storehouseCapacityKg + storehouseCapacityKg); } public isConsumingInputsWithoutOutput(currentTick: number): boolean { @@ -100,25 +94,38 @@ export class ProductionManager { return production?.production_rate > 0n && !this._inputs_available(currentTick, this.resourceId); } - private _balance(currentTick: number, resourceId: ResourcesIds): number { - const resource = this._getResource(resourceId); - - const [sign, rate] = this._netRate(resourceId); + public balanceFromComponents( + resourceId: ResourcesIds, + rate: number, + sign: boolean, + resourceBalance: bigint | undefined, + productionDuration: number, + depletionDuration: number, + ): number { + return this._calculateBalance(resourceId, rate, sign, resourceBalance, productionDuration, depletionDuration); + } + private _calculateBalance( + resourceId: ResourcesIds, + rate: number, + sign: boolean, + resourceBalance: bigint | undefined, + productionDuration: number, + depletionDuration: number, + ): number { if (rate !== 0) { if (sign) { // Positive net rate, increase balance - const productionDuration = this._productionDuration(currentTick, resourceId); - const balance = Number(resource?.balance || 0n) + productionDuration * rate; + const balance = Number(resourceBalance || 0n) + productionDuration * rate; const storeCapacity = this.getStoreCapacity(); - const maxAmountStorable = - (storeCapacity / (WEIGHTS_GRAM[resourceId] || 1000)) * EternumGlobalConfig.resources.resourcePrecision; + const maxAmountStorable = multiplyByPrecision( + storeCapacity / (configManager.getResourceWeight(resourceId) || 1000), + ); const result = Math.min(balance, maxAmountStorable); return result; } else { // Negative net rate, decrease balance but not below zero - const depletionDuration = this._depletionDuration(currentTick, resourceId); - const balance = Number(resource?.balance || 0n) - -depletionDuration * rate; + const balance = Number(resourceBalance || 0n) - -depletionDuration * rate; if (balance < 0) { return 0; } else { @@ -127,11 +134,20 @@ export class ProductionManager { } } else { // No net rate change, return current balance - const currentBalance = Number(resource?.balance || 0n); + const currentBalance = Number(resourceBalance || 0n); return currentBalance; } } + private _balance(currentTick: number, resourceId: ResourcesIds): number { + const resource = this._getResource(resourceId); + + const [sign, rate] = this._netRate(resourceId); + const productionDuration = this._productionDuration(currentTick, resourceId); + const productionDepletion = this._depletionDuration(currentTick, resourceId); + return this._calculateBalance(resourceId, rate, sign, resource?.balance, productionDuration, productionDepletion); + } + private _finish_tick(): number { const productionDeadline = this._getProductionDeadline(this.entityId); const production = this._getProduction(this.resourceId); @@ -178,7 +194,7 @@ export class ProductionManager { // If there is no production or resource, return current tick if (!production || !resource) return currentTick; - const [_, value] = this.netRate(currentTick); + const [_, value] = this.netRate(); const balance = this.balance(currentTick); if (value != 0) { @@ -199,7 +215,7 @@ export class ProductionManager { } private _inputs_available(currentTick: number, resourceId: ResourcesIds): boolean { - const inputs = RESOURCE_INPUTS_SCALED[resourceId]; + const inputs = configManager.resourceInputs[resourceId]; // Ensure inputs is an array before proceeding if (inputs.length == 0) { diff --git a/client/src/dojo/modelManager/TileManager.ts b/client/src/dojo/modelManager/TileManager.ts index 849ad970e..ed2c24c02 100644 --- a/client/src/dojo/modelManager/TileManager.ts +++ b/client/src/dojo/modelManager/TileManager.ts @@ -9,6 +9,7 @@ import { getNeighborHexes, ID, RealmLevels, + ResourcesIds, StructureType, } from "@bibliothecadao/eternum"; import { getComponentValue, Has, HasValue, NotValue, runQuery } from "@dojoengine/recs"; @@ -123,6 +124,21 @@ export class TileManager { ) => { let overrideId = uuid(); const entity = getEntityIdFromKeys([this.col, this.row, col, row].map((v) => BigInt(v))); + + let produced_resource_type = 0; + + switch (buildingType) { + case BuildingType.Farm: + produced_resource_type = ResourcesIds.Wheat; + break; + case BuildingType.FishingVillage: + produced_resource_type = ResourcesIds.Fish; + break; + case BuildingType.Resource: + produced_resource_type = resourceType || 0; + break; + } + this.setup.components.Building.addOverride(overrideId, { entity, value: { @@ -131,7 +147,7 @@ export class TileManager { inner_col: col, inner_row: row, category: BuildingType[buildingType], - produced_resource_type: resourceType ? resourceType : 0, + produced_resource_type, bonus_percent: 0, entity_id: entityId, outer_entity_id: entityId, diff --git a/client/src/dojo/modelManager/__tests__/BattleManager.test.ts b/client/src/dojo/modelManager/__tests__/BattleManager.test.ts index bf22393ee..71c2a5431 100644 --- a/client/src/dojo/modelManager/__tests__/BattleManager.test.ts +++ b/client/src/dojo/modelManager/__tests__/BattleManager.test.ts @@ -3,6 +3,7 @@ import { BattleSide, StructureType } from "@bibliothecadao/eternum"; import { Entity, getComponentValue, runQuery } from "@dojoengine/recs"; import { describe, expect, it, vi } from "vitest"; import { BattleManager, BattleStartStatus, BattleType, ClaimStatus } from "../BattleManager"; +import { ClientConfigManager } from "../ConfigManager"; import { ARMY_TROOP_COUNT, BATTLE_ENTITY_ID, @@ -33,6 +34,7 @@ vi.mock("@bibliothecadao/eternum", async (importOriginal) => { troop: { health: 1 }, resources: { resourceMultiplier: 1, + resourcePrecision: 1, }, tick: { armiesTickIntervalInSeconds: 1, @@ -389,19 +391,30 @@ describe("getUpdatedBattle", () => { }); describe("getTroopFullHealth", () => { - const battleManager = new BattleManager(BATTLE_ENTITY_ID, mockDojoResult); - it("should return the correct amount of health", () => { + const mockConfigManager = { + getTroopConfig: vi.fn().mockReturnValue({ health: 1 }), + } as unknown as ClientConfigManager; + + const battleManager = new BattleManager(BATTLE_ENTITY_ID, mockDojoResult); + battleManager["configManager"] = mockConfigManager; + const army = generateMockArmyInfo(true); const fullHealth = battleManager["getTroopFullHealth"](army.troops); + expect(mockConfigManager.getTroopConfig).toHaveBeenCalled(); expect(fullHealth).toBe(10n); }); }); describe("getUpdatedArmy", () => { + const mockConfigManager = { + getTroopConfig: vi.fn().mockReturnValue({ health: 1 }), + } as unknown as ClientConfigManager; + const battleManager = new BattleManager(BATTLE_ENTITY_ID, mockDojoResult); + battleManager["configManager"] = mockConfigManager; it("should return undefined if army is undefined", () => { const battle = generateMockBatle(true); @@ -637,7 +650,7 @@ describe("isClaimable", () => { const isClaimable = battleManager.isClaimable(CURRENT_TIMESTAMP, army, structure, defender); - expect(isClaimable).toBe(ClaimStatus.BattleOngoing); + expect(isClaimable).toBe(ClaimStatus.DefenderPresent); }); it("should return false if structure protector has health", () => { @@ -652,7 +665,7 @@ describe("isClaimable", () => { const isClaimable = battleManager.isClaimable(CURRENT_TIMESTAMP, army, structure, defender); - expect(isClaimable).toBe(ClaimStatus.BattleOngoing); + expect(isClaimable).toBe(ClaimStatus.DefenderPresent); }); it("should return false if the structure is mine", () => { diff --git a/client/src/dojo/modelManager/__tests__/__BattleManagerMock__.ts b/client/src/dojo/modelManager/__tests__/__BattleManagerMock__.ts index 51d05ca1c..f6065efe5 100644 --- a/client/src/dojo/modelManager/__tests__/__BattleManagerMock__.ts +++ b/client/src/dojo/modelManager/__tests__/__BattleManagerMock__.ts @@ -116,15 +116,9 @@ export const generateMockArmyInfo = ( realm: { entity_id: ARMY_ENTITY_ID, realm_id: 1, - resource_types_packed: 1n, - resource_types_count: 1, - cities: 1, - harbors: 1, - rivers: 1, - regions: 1, - wonder: 1, - order: 1, + produced_resources: 1n, level: 1, + order: 1, }, homePosition: { entity_id: ARMY_ENTITY_ID, x: 0, y: 0 }, }; @@ -149,3 +143,22 @@ export const generateMockStructure = (structureType: StructureType, isMine?: boo }, }; }; + +export const generateMockTroopConfig = () => { + return { + health: 1, + knightStrength: 1, + paladinStrength: 1, + crossbowmanStrength: 1, + advantagePercent: 1000, + disadvantagePercent: 1000, + maxTroopCount: 500000, + pillageHealthDivisor: 8, + baseArmyNumberForStructure: 3, + armyExtraPerMilitaryBuilding: 1, + maxArmiesPerStructure: 7, + battleLeaveSlashNum: 25, + battleLeaveSlashDenom: 100, + battleTimeScale: 1000, + }; +}; diff --git a/client/src/dojo/modelManager/utils/LeaderboardUtils.test.ts b/client/src/dojo/modelManager/utils/LeaderboardUtils.test.ts deleted file mode 100644 index 5b21773fc..000000000 --- a/client/src/dojo/modelManager/utils/LeaderboardUtils.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { TOTAL_CONTRIBUTABLE_AMOUNT } from "./LeaderboardUtils"; - -const EXPECTED_TOTAL_CONTRIBUTABLE_AMOUNT = 2.27; - -vi.mock("@bibliothecadao/eternum", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...(actual as any), - EternumGlobalConfig: { - troop: { health: 1 }, - resources: { - resourcePrecision: 1, - }, - }, - HYPERSTRUCTURE_TOTAL_COSTS_SCALED: [ - { resource: 1, amount: 1 }, - { resource: 2, amount: 1 }, - ], - HyperstructureResourceMultipliers: { - ["1"]: 1.0, - ["2"]: 1.0, - }, - }; -}); - -describe("TOTAL_CONTRIBUTABLE_AMOUNT", () => { - it("should return a valid amount", () => { - expect(TOTAL_CONTRIBUTABLE_AMOUNT).toBe(EXPECTED_TOTAL_CONTRIBUTABLE_AMOUNT); - }); -}); - -// describe("getTotalPointsPercentage", () => { -// it("should return a valid amount for resource 1", () => { -// expect(getTotalPointsPercentage(1, 1n)).toBe(0.5); -// }); - -// it("should return a valid amount for resource 2", () => { -// expect(getTotalPointsPercentage(2, 1n)).toBe(0.5); -// }); -// }); - -// describe("computeInitialContributionPoints", () => { -// it("should return a valid initial points contribution", () => { -// expect(computeInitialContributionPoints(1, 1n, 100)).toBe(50); -// }); - -// it("should return a valid initial points contribution for resource 2", () => { -// expect(computeInitialContributionPoints(2, 1n, 100)).toBe(50); -// }); -// }); diff --git a/client/src/dojo/modelManager/utils/LeaderboardUtils.ts b/client/src/dojo/modelManager/utils/LeaderboardUtils.ts index 42b7f0664..9e779c697 100644 --- a/client/src/dojo/modelManager/utils/LeaderboardUtils.ts +++ b/client/src/dojo/modelManager/utils/LeaderboardUtils.ts @@ -1,26 +1,7 @@ import { ClientComponents } from "@/dojo/createClientComponents"; -import { - EternumGlobalConfig, - HYPERSTRUCTURE_POINTS_ON_COMPLETION, - HYPERSTRUCTURE_RESOURCE_MULTIPLIERS, - HYPERSTRUCTURE_TOTAL_COSTS_SCALED, - ResourcesIds, -} from "@bibliothecadao/eternum"; +import { ResourcesIds } from "@bibliothecadao/eternum"; import { ComponentValue } from "@dojoengine/recs"; - -export const TOTAL_CONTRIBUTABLE_AMOUNT: number = HYPERSTRUCTURE_TOTAL_COSTS_SCALED.reduce( - (total, { resource, amount }) => { - return ( - total + - (HYPERSTRUCTURE_RESOURCE_MULTIPLIERS[resource as keyof typeof HYPERSTRUCTURE_RESOURCE_MULTIPLIERS] ?? 0) * amount - ); - }, - 0, -); - -function getResourceMultiplier(resourceType: ResourcesIds): number { - return HYPERSTRUCTURE_RESOURCE_MULTIPLIERS[resourceType] ?? 0; -} +import { ClientConfigManager } from "../ConfigManager"; export function computeInitialContributionPoints( resourceType: ResourcesIds, @@ -31,23 +12,23 @@ export function computeInitialContributionPoints( } export function getTotalPointsPercentage(resourceType: ResourcesIds, resourceQuantity: bigint): number { + const configManager = ClientConfigManager.instance(); + const effectiveContribution = - Number(resourceQuantity / BigInt(EternumGlobalConfig.resources.resourcePrecision)) * - getResourceMultiplier(resourceType); - return effectiveContribution / TOTAL_CONTRIBUTABLE_AMOUNT; + Number(resourceQuantity / BigInt(configManager.getResourcePrecision())) * + configManager.getResourceRarity(resourceType); + const totalContributableAmount = configManager.getHyperstructureTotalContributableAmount(); + + return effectiveContribution / totalContributableAmount; } export const calculateCompletionPoints = ( contributions: ComponentValue[], ) => { + const configManager = ClientConfigManager.instance(); + const pointsOnCompletion = configManager.getHyperstructureConfig().pointsOnCompletion; + return contributions.reduce((acc, contribution) => { - return ( - acc + - computeInitialContributionPoints( - contribution.resource_type, - contribution.amount, - HYPERSTRUCTURE_POINTS_ON_COMPLETION, - ) - ); + return acc + computeInitialContributionPoints(contribution.resource_type, contribution.amount, pointsOnCompletion); }, 0); }; diff --git a/client/src/dojo/setup.ts b/client/src/dojo/setup.ts index 803712b38..53dd4393b 100644 --- a/client/src/dojo/setup.ts +++ b/client/src/dojo/setup.ts @@ -15,8 +15,7 @@ export async function setup({ ...config }: DojoConfig) { const systemCalls = createSystemCalls(network); // fetch all existing entities from torii - const sync = await getSyncEntities(network.toriiClient, network.contractComponents as any, [], 1000); - + const sync = await getSyncEntities(network.toriiClient, network.contractComponents as any, undefined, [], 1000); const eventSync = getSyncEvents(network.toriiClient, network.contractComponents.events as any, undefined, []); configManager.setDojo(components); diff --git a/client/src/hooks/helpers/useEntities.tsx b/client/src/hooks/helpers/useEntities.tsx index 076bbaa4f..31fd3bcd1 100644 --- a/client/src/hooks/helpers/useEntities.tsx +++ b/client/src/hooks/helpers/useEntities.tsx @@ -9,15 +9,7 @@ import { type ID, } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { - Has, - HasValue, - getComponentValue, - runQuery, - type Component, - type ComponentValue, - type Entity, -} from "@dojoengine/recs"; +import { Has, getComponentValue, type Component, type ComponentValue, type Entity } from "@dojoengine/recs"; import { useMemo } from "react"; import { shortString } from "starknet"; import { useDojo } from "../context/DojoContext"; @@ -179,22 +171,6 @@ export const useEntities = () => { }; }; -export const getPlayerStructures = () => { - const { - setup: { - components: { Structure, Owner, Realm, Position }, - }, - } = useDojo(); - const { getEntityName } = useEntitiesUtils(); - - const getStructures = (playerAddress: ContractAddress) => { - const playerStructures = runQuery([Has(Structure), HasValue(Owner, { address: playerAddress })]); - return formatStructures(Array.from(playerStructures), Structure, Realm, Position, getEntityName); - }; - - return getStructures; -}; - export const useEntitiesUtils = () => { const { account: { account }, @@ -330,31 +306,3 @@ export const getAddressNameFromEntityIds = ( owner !== undefined, ); }; - -const formatStructures = ( - structures: Entity[], - Structure: Component, - Realm: Component, - Position: Component, - getEntityName: (entityId: ID) => string | undefined, -) => { - return structures - .map((id) => { - const structure = getComponentValue(Structure, id); - if (!structure) return; - - const realm = getComponentValue(Realm, id); - const position = getComponentValue(Position, id); - - const structureName = getEntityName(structure.entity_id); - - const name = realm - ? getRealmNameById(realm.realm_id) - : structureName - ? `${structure?.category} ${structureName}` - : structure.category || ""; - return { ...structure, position: position!, name }; - }) - .filter((structure): structure is PlayerStructure => structure !== undefined) - .sort((a, b) => (b.category || "").localeCompare(a.category || "")); -}; diff --git a/client/src/hooks/helpers/useGetAllPlayers.tsx b/client/src/hooks/helpers/useGetAllPlayers.tsx index cf15ad9b2..299f36d34 100644 --- a/client/src/hooks/helpers/useGetAllPlayers.tsx +++ b/client/src/hooks/helpers/useGetAllPlayers.tsx @@ -1,4 +1,4 @@ -import { ContractAddress } from "@bibliothecadao/eternum"; +import { ContractAddress, Player } from "@bibliothecadao/eternum"; import { Has, NotValue, runQuery } from "@dojoengine/recs"; import { useDojo } from "../context/DojoContext"; import { getAddressNameFromEntityIds, useEntitiesUtils } from "./useEntities"; @@ -14,7 +14,7 @@ export const useGetAllPlayers = () => { const playersEntityIds = runQuery([Has(Owner), Has(Realm)]); - const getPlayers = () => { + const getPlayers = (): Player[] => { const players = getAddressNameFromEntityIds(Array.from(playersEntityIds), Owner, getAddressNameFromEntity); const uniquePlayers = Array.from(new Map(players.map((player) => [player.address, player])).values()); @@ -40,7 +40,7 @@ export const useGetOtherPlayers = () => { NotValue(Owner, { address: ContractAddress(account.address) }), ]); - const getPlayers = () => { + const getPlayers = (): Player[] => { const players = getAddressNameFromEntityIds(Array.from(playersEntityIds), Owner, getAddressNameFromEntity); const uniquePlayers = Array.from(new Map(players.map((player) => [player.address, player])).values()); diff --git a/client/src/hooks/helpers/useHyperstructures.tsx b/client/src/hooks/helpers/useHyperstructures.tsx index 469d96b9e..fdb9dadd1 100644 --- a/client/src/hooks/helpers/useHyperstructures.tsx +++ b/client/src/hooks/helpers/useHyperstructures.tsx @@ -1,12 +1,7 @@ import { ClientComponents } from "@/dojo/createClientComponents"; -import { toHexString } from "@/ui/utils/utils"; -import { - ContractAddress, - EternumGlobalConfig, - HYPERSTRUCTURE_TOTAL_COSTS_SCALED, - ID, - ResourcesIds, -} from "@bibliothecadao/eternum"; +import { configManager } from "@/dojo/setup"; +import { divideByPrecision, toHexString } from "@/ui/utils/utils"; +import { ContractAddress, ID, ResourcesIds } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; import { Component, ComponentValue, Entity, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs"; import { toInteger } from "lodash"; @@ -22,13 +17,9 @@ export type ProgressWithPercentage = { amount: number; }; -type Progress = { - percentage: number; - progresses: ProgressWithPercentage[]; -}; - export const useHyperstructures = () => { const { + account: { account }, setup: { components: { Structure, Contribution, Position, Owner, EntityName }, }, @@ -43,13 +34,16 @@ export const useHyperstructures = () => { .values() .next().value; - const owner = toHexString(getComponentValue(Owner, ownerEntityIds || ("" as Entity))?.address || 0n); + const ownerComponent = getComponentValue(Owner, ownerEntityIds || ("" as Entity)); + const owner = toHexString(ownerComponent?.address || 0n); + const isOwner = ContractAddress(ownerComponent?.address ?? 0n) === ContractAddress(account.address); const entityName = getComponentValue(EntityName, hyperstructureEntityId); return { ...hyperstructure, ...position, ...contributions, owner, + isOwner, entityIdPoseidon: hyperstructureEntityId, name: entityName ? shortString.decodeShortString(entityName.name.toString()) @@ -179,23 +173,23 @@ const getAllProgressesAndTotalPercentage = ( hyperstructureEntityId: ID, ) => { let percentage = 0; - const allProgresses = HYPERSTRUCTURE_TOTAL_COSTS_SCALED.map(({ resource, amount: resourceCost }) => { - let foundProgress = progresses.find((progress) => progress!.resource_type === resource); - const resourcePercentage = !foundProgress - ? 0 - : Math.floor( - (Number(foundProgress.amount) / EternumGlobalConfig.resources.resourcePrecision / resourceCost!) * 100, - ); - let progress = { - hyperstructure_entity_id: hyperstructureEntityId, - resource_type: resource, - amount: !foundProgress ? 0 : Number(foundProgress.amount) / EternumGlobalConfig.resources.resourcePrecision, - percentage: resourcePercentage, - costNeeded: resourceCost, - }; - percentage += resourcePercentage; - return progress; - }); + const allProgresses = Object.values(configManager.hyperstructureTotalCosts).map( + ({ resource, amount: resourceCost }) => { + let foundProgress = progresses.find((progress) => progress!.resource_type === resource); + const resourcePercentage = !foundProgress + ? 0 + : Math.floor((divideByPrecision(Number(foundProgress.amount)) / resourceCost!) * 100); + let progress = { + hyperstructure_entity_id: hyperstructureEntityId, + resource_type: resource, + amount: !foundProgress ? 0 : divideByPrecision(Number(foundProgress.amount)), + percentage: resourcePercentage, + costNeeded: resourceCost, + }; + percentage += resourcePercentage; + return progress; + }, + ); const totalPercentage = percentage / allProgresses.length; return { allProgresses, percentage: totalPercentage }; }; diff --git a/client/src/hooks/helpers/useQuests.tsx b/client/src/hooks/helpers/useQuests.tsx index 74346e2a1..0d15dc962 100644 --- a/client/src/hooks/helpers/useQuests.tsx +++ b/client/src/hooks/helpers/useQuests.tsx @@ -1,10 +1,12 @@ import { TileManager } from "@/dojo/modelManager/TileManager"; +import { SetupResult } from "@/dojo/setup"; import { QuestId, questDetails } from "@/ui/components/quest/questDetails"; import { BuildingType, ContractAddress, ID, QuestType, ResourcesIds, StructureType } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; import { HasValue, NotValue, getComponentValue, runQuery } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useCallback, useMemo } from "react"; +import { Account, AccountInterface } from "starknet"; import { useDojo } from "../context/DojoContext"; import useUIStore from "../store/useUIStore"; import { ArmyInfo, useArmiesByEntityOwner } from "./useArmies"; @@ -13,6 +15,7 @@ import { useGetMyOffers } from "./useTrade"; export interface Quest { id: QuestId; + view: string; name: string; description: string | React.ReactNode; steps: (string | React.ReactNode)[]; @@ -33,7 +36,9 @@ export enum QuestStatus { } export const useQuests = () => { - const questDependencies = useQuestDependencies(); + const { setup, account } = useDojo(); + + const questDependencies = useQuestDependencies(setup, account.account); const createQuest = (questId: QuestId) => { const dependency = questDependencies[questId]; @@ -67,15 +72,22 @@ export const useQuests = () => { return { quests }; }; -const useQuestDependencies = () => { - const { - setup, - account: { account }, - } = useDojo(); - +const useQuestDependencies = (setup: SetupResult, account: Account | AccountInterface) => { const structureEntityId = useUIStore((state) => state.structureEntityId); + const entityUpdate = useEntityQuery([ + HasValue(setup.components.EntityOwner, { entity_owner_id: structureEntityId || 0 }), + ]); + const playerPillages = useEntityQuery([ + HasValue(setup.components.events.BattlePillageData, { pillager: BigInt(account.address) }), + ]); + const buildingQuantities = useBuildingQuantities(structureEntityId); + const { entityArmies } = useArmiesByEntityOwner({ entity_owner_entity_id: structureEntityId || 0 }); + const orders = useGetMyOffers(); + const { playerStructures } = useEntities(); + const structures = playerStructures(); const { getEntityInfo } = useEntitiesUtils(); + const structurePosition = getEntityInfo(structureEntityId)?.position || { x: 0, y: 0 }; const tileManager = new TileManager(setup, { @@ -89,13 +101,6 @@ const useQuestDependencies = () => { [existingBuildings], ); - const entityUpdate = useEntityQuery([ - HasValue(setup.components.EntityOwner, { entity_owner_id: structureEntityId || 0 }), - ]); - - const buildingQuantities = useBuildingQuantities(structureEntityId); - - const { entityArmies } = useArmiesByEntityOwner({ entity_owner_entity_id: structureEntityId || 0 }); const hasDefensiveArmy = useMemo( () => entityArmies.some((army) => army.protectee?.protectee_id === structureEntityId && army.quantity.value > 0n), [entityArmies], @@ -105,20 +110,11 @@ const useQuestDependencies = () => { [entityArmies], ); - const orders = useGetMyOffers(); - const hasTraveled = useMemo( () => armyHasTraveled(entityArmies, structurePosition), [entityArmies, structurePosition], ); - const playerPillages = useEntityQuery([ - HasValue(setup.components.events.BattlePillageData, { pillager: BigInt(account.address) }), - ]); - - const { playerStructures } = useEntities(); - const structures = playerStructures(); - const countStructuresByCategory = useCallback( (category: string) => { return structures.filter((structure) => structure.category === category).length; @@ -274,20 +270,17 @@ const useQuestDependencies = () => { export const useQuestClaimStatus = () => { const { setup: { - components: { HasClaimedStartingResources }, + components: { Quest }, }, } = useDojo(); const structureEntityId = useUIStore((state) => state.structureEntityId); - const prizeUpdate = useEntityQuery([HasValue(HasClaimedStartingResources, { entity_id: structureEntityId || 0 })]); + const prizeUpdate = useEntityQuery([HasValue(Quest, { entity_id: structureEntityId || 0 })]); const checkPrizesClaimed = (prizes: Prize[]) => { return prizes.every((prize) => { - const value = getComponentValue( - HasClaimedStartingResources, - getEntityIdFromKeys([BigInt(structureEntityId || 0), BigInt(prize.id)]), - ); - return value?.claimed; + const value = getComponentValue(Quest, getEntityIdFromKeys([BigInt(structureEntityId || 0), BigInt(prize.id)])); + return value?.completed; }); }; diff --git a/client/src/hooks/helpers/useRealm.tsx b/client/src/hooks/helpers/useRealm.tsx index 0b150a7c8..19afc61c7 100644 --- a/client/src/hooks/helpers/useRealm.tsx +++ b/client/src/hooks/helpers/useRealm.tsx @@ -1,11 +1,6 @@ import { ClientComponents } from "@/dojo/createClientComponents"; -import { - BASE_POPULATION_CAPACITY, - ContractAddress, - ID, - getOrderName, - getQuestResources as getStartingResources, -} from "@bibliothecadao/eternum"; +import { configManager } from "@/dojo/setup"; +import { ContractAddress, ID, getOrderName, getQuestResources as getStartingResources } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; import { ComponentValue, Entity, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs"; import { useMemo } from "react"; @@ -21,12 +16,6 @@ type RealmInfo = { realmId: ID; entityId: ID; name: string; - cities: number; - rivers: number; - wonder: number; - harbors: number; - regions: number; - resourceTypesCount: number; resourceTypesPacked: bigint; order: number; position: ComponentValue; @@ -47,7 +36,7 @@ export function useRealm() { const getQuestResources = () => { const realm = getComponentValue(Realm, getEntityIdFromKeys([BigInt(structureEntityId)])); - const resourcesProduced = realm ? unpackResources(realm.resource_types_packed, realm.resource_types_count) : []; + const resourcesProduced = realm ? unpackResources(realm.produced_resources) : []; return getStartingResources(resourcesProduced); }; @@ -196,19 +185,7 @@ export function useGetRealm(realmEntityId: ID | undefined) { const population = getComponentValue(Population, entityId); if (realm && owner && position) { - const { - realm_id, - entity_id, - cities, - rivers, - wonder, - harbors, - regions, - resource_types_count, - resource_types_packed, - order, - level, - } = realm; + const { realm_id, entity_id, produced_resources, order, level } = realm; const name = getRealmNameById(realm_id); @@ -218,18 +195,13 @@ export function useGetRealm(realmEntityId: ID | undefined) { realmId: realm_id, entityId: entity_id, name, - cities, - rivers, - wonder, - harbors, - regions, level, - resourceTypesCount: resource_types_count, - resourceTypesPacked: resource_types_packed, + resourceTypesPacked: produced_resources, order, position, ...population, - hasCapacity: !population || population.capacity + BASE_POPULATION_CAPACITY > population.population, + hasCapacity: + !population || population.capacity + configManager.getBasePopulationCapacity() > population.population, owner: address, }; } @@ -259,18 +231,7 @@ export function getRealms(): RealmInfo[] { if (!realm || !owner || !position) return; - const { - realm_id, - entity_id, - cities, - rivers, - wonder, - harbors, - regions, - resource_types_count, - resource_types_packed, - order, - } = realm; + const { realm_id, entity_id, produced_resources, order } = realm; const name = getRealmNameById(realm_id); @@ -283,17 +244,12 @@ export function getRealms(): RealmInfo[] { realmId: realm_id, entityId: entity_id, name, - cities, - rivers, - wonder, - harbors, - regions, - resourceTypesCount: resource_types_count, - resourceTypesPacked: resource_types_packed, + resourceTypesPacked: produced_resources, order, position, ...population, - hasCapacity: !population || population.capacity + BASE_POPULATION_CAPACITY > population.population, + hasCapacity: + !population || population.capacity + configManager.getBasePopulationCapacity() > population.population, owner: address, ownerName, }; diff --git a/client/src/hooks/helpers/useStructures.tsx b/client/src/hooks/helpers/useStructures.tsx index 0ad43c96f..7bb9ad47f 100644 --- a/client/src/hooks/helpers/useStructures.tsx +++ b/client/src/hooks/helpers/useStructures.tsx @@ -4,7 +4,7 @@ import { configManager } from "@/dojo/setup"; import { unpackResources } from "@/ui/utils/packedData"; import { getRealm } from "@/ui/utils/realms"; import { calculateDistance, currentTickCount } from "@/ui/utils/utils"; -import { ContractAddress, EternumGlobalConfig, ID, Position } from "@bibliothecadao/eternum"; +import { ContractAddress, DONKEY_ENTITY_TYPE, ID, Position } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; import { ComponentValue, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; @@ -227,11 +227,13 @@ export function useStructuresFromPosition({ position }: { position: Position }) if (!realmData) return undefined; const name = realmData.name; const owner = getComponentValue(Owner, entityId); - const resources = unpackResources(BigInt(realm.resource_types_packed), realm.resource_types_count); + const resources = unpackResources(BigInt(realm.produced_resources)); const distanceFromPosition = calculateDistance(position, realmPosition) ?? 0; - const timeToTravel = Math.floor(((distanceFromPosition / EternumGlobalConfig.speed.donkey) * 3600) / 60 / 60); + const timeToTravel = Math.floor( + ((distanceFromPosition / configManager.getSpeedConfig(DONKEY_ENTITY_TYPE)) * 3600) / 60 / 60, + ); return { ...realm, diff --git a/client/src/hooks/store/_threeStore.tsx b/client/src/hooks/store/_threeStore.tsx index 1e38fae28..c70673b79 100644 --- a/client/src/hooks/store/_threeStore.tsx +++ b/client/src/hooks/store/_threeStore.tsx @@ -1,6 +1,6 @@ import { StructureInfo } from "@/three/components/StructureManager"; import { HexPosition } from "@/types"; -import { BuildingType, ID } from "@bibliothecadao/eternum"; +import { BuildingType, ID, Position } from "@bibliothecadao/eternum"; export interface ThreeStore { navigationTarget: HexPosition | null; @@ -10,12 +10,14 @@ export interface ThreeStore { updateHoveredHex: (hoveredHex: HexPosition | null) => void; updateTravelPaths: (travelPaths: Map) => void; updateSelectedEntityId: (selectedEntityId: ID | null) => void; - selectedHex: HexPosition; - setSelectedHex: (hex: HexPosition) => void; + selectedHex: HexPosition | null; + setSelectedHex: (hex: HexPosition | null) => void; hoveredArmyEntityId: ID | null; setHoveredArmyEntityId: (id: ID | null) => void; hoveredStructure: StructureInfo | null; setHoveredStructure: (structure: StructureInfo | null) => void; + hoveredBattle: Position | null; + setHoveredBattle: (hex: Position | null) => void; selectedBuilding: BuildingType; setSelectedBuilding: (building: BuildingType) => void; selectedBuildingEntityId: ID | null; @@ -56,11 +58,13 @@ export const createThreeStoreSlice = (set: any, get: any) => ({ updateSelectedEntityId: (selectedEntityId: ID | null) => set((state: any) => ({ armyActions: { ...state.armyActions, selectedEntityId } })), selectedHex: { col: 0, row: 0 }, - setSelectedHex: (hex: HexPosition) => set({ selectedHex: hex }), + setSelectedHex: (hex: HexPosition | null) => set({ selectedHex: hex }), hoveredArmyEntityId: null, setHoveredArmyEntityId: (id: ID | null) => set({ hoveredArmyEntityId: id }), hoveredStructure: null, setHoveredStructure: (structure: StructureInfo | null) => set({ hoveredStructure: structure }), + hoveredBattle: null, + setHoveredBattle: (hex: Position | null) => set({ hoveredBattle: hex }), selectedBuilding: BuildingType.Farm, setSelectedBuilding: (building: BuildingType) => set({ selectedBuilding: building }), selectedBuildingEntityId: null, diff --git a/client/src/hooks/store/useBlockchainStore.tsx b/client/src/hooks/store/useBlockchainStore.tsx index 0c5a63f3b..c5cbdc380 100644 --- a/client/src/hooks/store/useBlockchainStore.tsx +++ b/client/src/hooks/store/useBlockchainStore.tsx @@ -33,25 +33,23 @@ export const useFetchBlockchainData = () => { const setNextBlockTimestamp = useUIStore((state) => state.setNextBlockTimestamp); const setCurrentDefaultTick = useUIStore((state) => state.setCurrentDefaultTick); const setCurrentArmiesTick = useUIStore((state) => state.setCurrentArmiesTick); - const currentTimestamp = useUIStore((state) => state.nextBlockTimestamp); // Get the current nextBlockTimestamp from the store - - const tickConfigArmies = getComponentValue( - TickConfig, - getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(TickIds.Armies)]), - ); - const tickConfigDefault = getComponentValue( - TickConfig, - getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(TickIds.Default)]), - ); useEffect(() => { + const tickConfigArmies = getComponentValue( + TickConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(TickIds.Armies)]), + ); + const tickConfigDefault = getComponentValue( + TickConfig, + getEntityIdFromKeys([WORLD_CONFIG_ID, BigInt(TickIds.Default)]), + ); + + console.log(tickConfigArmies, tickConfigDefault); + const fetchBlockchainTimestamp = async () => { - // Perform the necessary logic to fetch the blockchain timestamp - const timestamp = await fetchBlockTimestamp(); // Example: getBlockchainTimestamp is a placeholder for your blockchain timestamp retrieval logic + const timestamp = Math.floor(Date.now() / 1000); - // Update the state with the fetched timestamp - if (timestamp && timestamp !== currentTimestamp) { - // Check if fetched timestamp is different from current state + if (timestamp) { setNextBlockTimestamp(timestamp); setCurrentDefaultTick(Math.floor(timestamp / Number(tickConfigDefault!.tick_interval_in_seconds))); setCurrentArmiesTick(Math.floor(timestamp / Number(tickConfigArmies!.tick_interval_in_seconds))); @@ -60,15 +58,8 @@ export const useFetchBlockchainData = () => { fetchBlockchainTimestamp(); // Initial fetch - const intervalId = setInterval(fetchBlockchainTimestamp, 10000); // Fetch every 10 seconds - return () => { - clearInterval(intervalId); // Clear interval on component unmount + clearInterval(setInterval(fetchBlockchainTimestamp, 10000)); }; }, []); }; - -const fetchBlockTimestamp = async (): Promise => { - const currentTimestamp = Math.floor(Date.now() / 1000); - return currentTimestamp; -}; diff --git a/client/src/hooks/store/useMarketStore.tsx b/client/src/hooks/store/useMarketStore.tsx index 2ecacfe4c..a5ed0e707 100644 --- a/client/src/hooks/store/useMarketStore.tsx +++ b/client/src/hooks/store/useMarketStore.tsx @@ -1,18 +1,8 @@ -import { ID, MarketInterface, ResourcesIds } from "@bibliothecadao/eternum"; import { create } from "zustand"; interface MarketStore { loading: boolean; setLoading: (loading: boolean) => void; - lordsMarket: MarketInterface[]; - generalMarket: MarketInterface[]; - directOffers: MarketInterface[]; - refresh: boolean; - setRefresh: (refresh: boolean) => void; - refreshMarket: () => void; - deleteTrade: (tradeId: ID) => void; - setMarkets: (lordsMarket: MarketInterface[], nonLordsMarket: MarketInterface[]) => void; - setDirectOffers: (directOffers: MarketInterface[]) => void; selectedResource: number; setSelectedResource: (resource: number) => void; } @@ -20,25 +10,7 @@ interface MarketStore { const useMarketStore = create((set, get) => { return { loading: false, - lordsMarket: [], - generalMarket: [], - directOffers: [], - refresh: false, - setRefresh: (refresh: boolean) => set({ refresh }), - refreshMarket: () => { - set({ refresh: true }); - }, - setMarkets: (lordsMarket: MarketInterface[], generalMarket: MarketInterface[]) => - set({ lordsMarket, generalMarket }), - setDirectOffers: (directOffers: MarketInterface[]) => set({ directOffers }), setLoading: (loading) => set({ loading }), - deleteTrade: (tradeId: ID) => { - const lordsMarket = get().lordsMarket.filter((trade) => trade.tradeId !== tradeId); - const generalMarket = get().generalMarket.filter((trade) => trade.tradeId !== tradeId); - const directOffers = get().directOffers.filter((trade) => trade.tradeId !== tradeId); - - set({ lordsMarket: [...lordsMarket], generalMarket: [...generalMarket], directOffers: [...directOffers] }); - }, selectedResource: 1, setSelectedResource: (resource) => { set({ selectedResource: resource }); diff --git a/client/src/index.css b/client/src/index.css index 60f789bb0..ac14248e8 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,4 +1,5 @@ @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap&family=Sedan:ital@0;1&display=swap&Bodoni+Moda:ital,opsz,wght@0,6..96,400..900;1,6..96,400..900&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Bokor&display=swap"); @tailwind base; @tailwind components; @@ -20,7 +21,7 @@ h3, h4, h5, h6 { - font-family: "Sedan", sans-serif; + font-family: "Bokor", system-ui; } .h1, @@ -29,7 +30,7 @@ h6 { .h4, .h5, .h6 { - font-family: "Sedan", sans-serif; + font-family: "Bokor", system-ui; } @keyframes dotAnimation { diff --git a/client/src/three/components/ArmyManager.ts b/client/src/three/components/ArmyManager.ts index eb494ffdf..b5e6e4d04 100644 --- a/client/src/three/components/ArmyManager.ts +++ b/client/src/three/components/ArmyManager.ts @@ -17,21 +17,28 @@ export class ArmyManager { private armies: Map = new Map(); private scale: THREE.Vector3; - private movingArmies: Map = new Map(); + private movingArmies: Map< + ID, + { startPos: THREE.Vector3; endPos: THREE.Vector3; progress: number; matrixIndex: number } + > = new Map(); private labelManager: LabelManager; private labels: Map = new Map(); private movingLabels: Map = new Map(); - private cachedChunks: Map = new Map(); private currentChunkKey: string | null = "190,170"; - private chunkSize: number; private renderChunkSize: { width: number; height: number }; - - constructor(scene: THREE.Scene, chunkSize: number, renderChunkSize: { width: number; height: number }) { + private visibleArmies: Array<{ + entityId: ID; + hexCoords: Position; + isMine: boolean; + color: string; + matrixIndex: number; + }> = []; + + constructor(scene: THREE.Scene, renderChunkSize: { width: number; height: number }) { this.scene = scene; this.armyModel = new ArmyModel(scene); this.scale = new THREE.Vector3(0.3, 0.3, 0.3); this.labelManager = new LabelManager("textures/army_label.png", 1.5); - this.chunkSize = chunkSize; this.renderChunkSize = renderChunkSize; this.onMouseMove = this.onMouseMove.bind(this); @@ -93,7 +100,6 @@ export class ArmyManager { public onRightClick(raycaster: THREE.Raycaster) { if (!this.armyModel.mesh) { - console.log("Mesh not found."); return; } @@ -104,13 +110,11 @@ export class ArmyManager { const clickedObject = intersects[0].object; if (!(clickedObject instanceof THREE.InstancedMesh)) { - console.log("Clicked object is not an instance of THREE.InstancedMesh."); return; } const instanceId = intersects[0].instanceId; if (instanceId === undefined) { - console.log("Instance ID is undefined."); return; } @@ -126,96 +130,60 @@ export class ArmyManager { } async onUpdate(update: ArmySystemUpdate) { - console.debug("onUpdate called with update:", update); await this.armyModel.loadPromise; const { entityId, hexCoords, isMine, battleId, currentHealth, order } = update; if (currentHealth <= 0) { - console.debug(`Army with entityId ${entityId} has currentHealth <= 0`); if (this.armies.has(entityId)) { - console.debug(`Removing army with entityId ${entityId}`); this.removeArmy(entityId); return; } else { - console.debug(`Army with entityId ${entityId} not found`); return; } } if (battleId !== 0) { - console.debug(`Army with entityId ${entityId} is in battle with battleId ${battleId}`); if (this.armies.has(entityId)) { - console.debug(`Removing army with entityId ${entityId} due to battle`); this.removeArmy(entityId); return; } else { - console.debug(`Army with entityId ${entityId} not found`); return; } } const position = new Position({ x: hexCoords.col, y: hexCoords.row }); - console.debug(`Computed position for army with entityId ${entityId}:`, position); if (this.armies.has(entityId)) { - console.debug(`Moving army with entityId ${entityId} to new position`); this.moveArmy(entityId, position); } else { - console.debug(`Adding new army with entityId ${entityId} at position`, position); this.addArmy(entityId, position, isMine, order); } } + async updateChunk(chunkKey: string) { await this.armyModel.loadPromise; - console.debug(`updateChunk called with chunkKey: ${chunkKey}`); + if (this.currentChunkKey === chunkKey) { - console.debug(`Chunk key ${chunkKey} is the same as the current chunk key. No update needed.`); return; } this.currentChunkKey = chunkKey; - if (this.cachedChunks.has(chunkKey)) { - console.debug(`Chunk key ${chunkKey} found in cache. Applying cached chunk.`); - this.computeAndCacheChunk(chunkKey); - } else { - console.debug(`Chunk key ${chunkKey} not found in cache. Computing and caching chunk.`); - this.computeAndCacheChunk(chunkKey); - } + this.renderVisibleArmies(chunkKey); } - private applyCachedChunk(chunkKey: string) { - console.debug(`applyCachedChunk called with chunkKey: ${chunkKey}`); - const cachedData = this.cachedChunks.get(chunkKey); - if (!cachedData) { - console.debug(`No cached data found for chunkKey: ${chunkKey}`); - return; - } + private renderVisibleArmies(chunkKey: string) { + const [startRow, startCol] = chunkKey.split(",").map(Number); - console.debug(`Applying cached data for chunkKey: ${chunkKey}`); - this.armyModel.mesh.instanceMatrix.copy(cachedData.matrices); - this.armyModel.mesh.count = cachedData.count; - this.armyModel.mesh.instanceMatrix.needsUpdate = true; - this.updateLabelsForChunk(chunkKey); - } + this.visibleArmies = this.getVisibleArmiesForChunk(startRow, startCol); - private computeAndCacheChunk(chunkKey: string) { - console.debug(`computeAndCacheChunk called with chunkKey: ${chunkKey}`); - const [startRow, startCol] = chunkKey.split(",").map(Number); - console.debug(`Parsed chunkKey into startRow: ${startRow}, startCol: ${startCol}`); - const visibleArmies = this.getVisibleArmiesForChunk(startRow, startCol); - console.debug(`Found ${visibleArmies.length} visible armies for chunk`); let count = 0; if (!this.armyModel.mesh.userData.entityIdMap) { this.armyModel.mesh.userData.entityIdMap = new Map(); } - visibleArmies.forEach((army, index) => { - console.debug(`Processing army with entityId: ${army.entityId} at index: ${index}`); + this.visibleArmies.forEach((army, index) => { const position = this.getArmyWorldPosition(army.entityId, army.hexCoords); this.armyModel.dummyObject.position.copy(position); this.armyModel.dummyObject.scale.copy(this.scale); - console.debug( - `Position: ${position.x}, ${position.y}, ${position.z}, Scale: ${this.armyModel.dummyObject.scale.x}, ${this.armyModel.dummyObject.scale.y}, ${this.armyModel.dummyObject.scale.z}`, - ); this.armyModel.dummyObject.updateMatrix(); this.armyModel.mesh.setMatrixAt(index, this.armyModel.dummyObject.matrix); this.armyModel.mesh.setColorAt(index, new THREE.Color(army.color)); @@ -224,10 +192,8 @@ export class ArmyManager { this.armies.set(army.entityId, { ...army, matrixIndex: index }); }); - console.debug(`Setting cached chunk with key: ${chunkKey} and count: ${count}`); - this.cachedChunks.set(chunkKey, { matrices: this.armyModel.mesh.instanceMatrix.clone() as any, count }); - this.armyModel.mesh.count = count; - console.debug(`Setting cached chunk with key: ${chunkKey} and count: ${count} and matrices:`); + this.armyModel.mesh.count = this.visibleArmies.length; + this.armyModel.mesh.instanceMatrix.needsUpdate = true; if (this.armyModel.mesh.instanceColor) { this.armyModel.mesh.instanceColor.needsUpdate = true; @@ -236,27 +202,36 @@ export class ArmyManager { this.updateLabelsForChunk(chunkKey); } + private isArmyVisible( + army: { entityId: ID; hexCoords: Position; isMine: boolean; color: string }, + startRow: number, + startCol: number, + ) { + const { x, y } = army.hexCoords.getNormalized(); + const isVisible = + x >= startCol - this.renderChunkSize.width / 2 && + x <= startCol + this.renderChunkSize.width / 2 && + y >= startRow - this.renderChunkSize.height / 2 && + y <= startRow + this.renderChunkSize.height / 2; + return isVisible; + } + private getVisibleArmiesForChunk( startRow: number, startCol: number, - ): Array<{ entityId: ID; hexCoords: Position; isMine: boolean; color: string }> { - console.debug(`getVisibleArmiesForChunk called with startRow: ${startRow}, startCol: ${startCol}`); + ): Array<{ entityId: ID; hexCoords: Position; isMine: boolean; color: string; matrixIndex: number }> { const visibleArmies = Array.from(this.armies.entries()) .filter(([_, army]) => { - const { x, y } = army.hexCoords.getNormalized(); - const isVisible = - x >= startCol - this.renderChunkSize.width / 2 && - x <= startCol + this.renderChunkSize.width / 2 && - y >= startRow - this.renderChunkSize.height / 2 && - y <= startRow + this.renderChunkSize.height / 2; - if (isVisible) { - console.debug(`Army with entityId: ${army.entityId} at hexCoords: (${x}, ${y}) is visible`); - } - - return isVisible; + return this.isArmyVisible(army, startRow, startCol); }) - .map(([entityId, army]) => ({ entityId, hexCoords: army.hexCoords, isMine: army.isMine, color: army.color })); - console.debug(`Found ${visibleArmies.length} visible armies for chunk`); + .map(([entityId, army], index) => ({ + entityId, + hexCoords: army.hexCoords, + isMine: army.isMine, + color: army.color, + matrixIndex: index, + })); + return visibleArmies; } @@ -265,10 +240,7 @@ export class ArmyManager { this.labels.forEach((label) => this.scene.remove(label)); this.labels.clear(); - const [startRow, startCol] = chunkKey.split(",").map(Number); - const visibleArmies = this.getVisibleArmiesForChunk(startRow, startCol); - - visibleArmies.forEach((army) => { + this.visibleArmies.forEach((army) => { const position = this.getArmyWorldPosition(army.entityId, army.hexCoords); const label = this.labelManager.createLabel(position, army.isMine ? myColor : neutralColor); this.labels.set(army.entityId, label); @@ -280,31 +252,28 @@ export class ArmyManager { if (this.armies.has(entityId)) return; const orderColor = orders.find((_order) => _order.orderId === order)?.color || "#000000"; this.armies.set(entityId, { entityId, matrixIndex: this.armies.size - 1, hexCoords, isMine, color: orderColor }); - this.invalidateCache(); - this.computeAndCacheChunk(this.currentChunkKey!); + this.renderVisibleArmies(this.currentChunkKey!); } public moveArmy(entityId: ID, hexCoords: Position) { // @ts-ignore - console.debug(`moveArmy called with entityId: ${entityId}, hexCoords: (${hexCoords.x}, ${hexCoords.y})`); + const armyData = this.armies.get(entityId); if (!armyData) { - console.debug(`No army found with entityId: ${entityId}`); return; } const { x, y } = hexCoords.getNormalized(); const { x: armyDataX, y: armyDataY } = armyData.hexCoords.getNormalized(); if (armyDataX === x && armyDataY === y) { - console.debug(`Army with entityId: ${entityId} is already at the target position: (${x}, ${y})`); return; } this.armies.set(entityId, { ...armyData, hexCoords }); - this.invalidateCache(); - console.debug( - `Army with entityId: ${entityId} moved to new hexCoords: (${x}, ${y}), matrixIndex: ${armyData.matrixIndex}`, - ); + + if (!this.visibleArmies.some((army) => army.entityId === entityId)) { + return; + } const newPosition = this.getArmyWorldPosition(entityId, hexCoords); const currentPosition = new THREE.Vector3(); @@ -315,26 +284,20 @@ export class ArmyManager { startPos: currentPosition, endPos: newPosition, progress: 0, + matrixIndex: armyData.matrixIndex, }); - console.debug( - `Army with entityId: ${entityId} movement started from (${currentPosition.x}, ${currentPosition.y}, ${currentPosition.z}) to (${newPosition.x}, ${newPosition.y}, ${newPosition.z})`, - ); this.movingLabels.set(entityId, { startPos: currentPosition, endPos: newPosition, progress: 0, }); - console.debug( - `Label for army with entityId: ${entityId} movement started from (${currentPosition.x}, ${currentPosition.y}, ${currentPosition.z}) to (${newPosition.x}, ${newPosition.y}, ${newPosition.z})`, - ); } public removeArmy(entityId: ID) { if (!this.armies.delete(entityId)) return; - this.invalidateCache(); - this.computeAndCacheChunk(this.currentChunkKey!); + this.renderVisibleArmies(this.currentChunkKey!); const label = this.labels.get(entityId); if (label) { @@ -343,10 +306,6 @@ export class ArmyManager { } } - private invalidateCache() { - this.cachedChunks.clear(); - } - public getArmies() { return Array.from(this.armies.values()); } @@ -356,8 +315,13 @@ export class ArmyManager { const movementSpeed = 1.25; // Constant movement speed this.movingArmies.forEach((movement, entityId) => { - const armyData = this.armies.get(entityId); - if (!armyData) return; + const armyData = this.visibleArmies.find((army) => army.entityId === entityId); + if (!armyData) { + // delete the movement from the map + this.armyModel.setAnimationState(movement.matrixIndex, false); // Set back to idle animation + this.movingArmies.delete(entityId); + return; + } const { matrixIndex } = armyData; let position: THREE.Vector3; diff --git a/client/src/three/helpers/utils.ts b/client/src/three/helpers/utils.ts index 6a658cce6..8f47c5cf3 100644 --- a/client/src/three/helpers/utils.ts +++ b/client/src/three/helpers/utils.ts @@ -1,6 +1,6 @@ export function createPausedLabel() { const div = document.createElement("div"); - div.classList.add("rounded-md", "bg-black/50", "text-gold", "p-1", "-translate-x-1/2", "text-xs"); + div.classList.add("rounded-md", "bg-brown/50", "text-gold", "p-1", "-translate-x-1/2", "text-xs"); div.textContent = `⚠️ Production paused`; return div; } diff --git a/client/src/three/scenes/Worldmap.ts b/client/src/three/scenes/Worldmap.ts index c54d5d25d..5e852162f 100644 --- a/client/src/three/scenes/Worldmap.ts +++ b/client/src/three/scenes/Worldmap.ts @@ -9,7 +9,6 @@ import { HexPosition, SceneName } from "@/types"; import { Position } from "@/types/Position"; import { FELT_CENTER } from "@/ui/config"; import { UNDEFINED_STRUCTURE_ENTITY_ID } from "@/ui/constants"; -import { View } from "@/ui/modules/navigation/LeftNavigationModule"; import { getWorldPositionForHex } from "@/ui/utils/utils"; import { BiomeType, getNeighborOffsets, ID } from "@bibliothecadao/eternum"; import { throttle } from "lodash"; @@ -95,7 +94,7 @@ export default class WorldmapScene extends HexagonScene { }, ); - this.armyManager = new ArmyManager(this.scene, this.chunkSize, this.renderChunkSize); + this.armyManager = new ArmyManager(this.scene, this.renderChunkSize); this.structureManager = new StructureManager(this.scene, this.renderChunkSize); this.battleManager = new BattleManager(this.scene); @@ -131,10 +130,6 @@ export default class WorldmapScene extends HexagonScene { this.onArmyMouseMove(entityId); }, 10), ); - this.inputManager.addListener("click", (raycaster) => { - const selectedEntityId = this.armyManager.onRightClick(raycaster); - selectedEntityId && this.onArmySelection(selectedEntityId); - }); // add particles this.selectedHexManager = new SelectedHexManager(this.scene); @@ -143,9 +138,7 @@ export default class WorldmapScene extends HexagonScene { useUIStore.subscribe( (state) => state.armyActions.selectedEntityId, (selectedEntityId) => { - if (selectedEntityId) { - this.onArmySelection(selectedEntityId); - } + this.onArmySelection(selectedEntityId); }, ); @@ -165,9 +158,15 @@ export default class WorldmapScene extends HexagonScene { this.closeNavigationViews(); } else { this.clearEntitySelection(); + this.clearHexSelection(); } } }); + + window.addEventListener("urlChanged", () => { + this.clearEntitySelection(); + this.clearHexSelection(); + }); } public moveCameraToURLLocation() { @@ -190,6 +189,8 @@ export default class WorldmapScene extends HexagonScene { protected onHexagonMouseMove(hex: { hexCoords: HexPosition; position: THREE.Vector3 } | null): void { if (hex === null) { this.state.updateHoveredHex(null); + this.state.setHoveredStructure(null); + this.state.setHoveredBattle(null); return; } const { hexCoords } = hex; @@ -213,6 +214,18 @@ export default class WorldmapScene extends HexagonScene { } else if (this.state.hoveredStructure) { this.state.setHoveredStructure(null); } + + const position = new Position({ x: hexCoords.col, y: hexCoords.row }); + const isBattle = this.battleManager.battles.hasByPosition(position); + + if (isBattle) { + const contractPosition = position.getContract(); + if (this.state.hoveredBattle?.x !== contractPosition.x || this.state.hoveredBattle?.y !== contractPosition.y) { + this.state.setHoveredBattle(position.getContract()); + } + } else { + this.state.setHoveredBattle(null); + } } private _canBuildStructure(hexCoords: HexPosition) { @@ -239,15 +252,25 @@ export default class WorldmapScene extends HexagonScene { if (!hexCoords) { return; } - const { selectedEntityId, travelPaths } = this.state.armyActions; - const buildingType = this.structurePreview?.getPreviewStructure(); if (buildingType && this._canBuildStructure(hexCoords)) { const normalizedHexCoords = { col: hexCoords.col + FELT_CENTER, row: hexCoords.row + FELT_CENTER }; this.tileManager.placeStructure(this.structureEntityId, buildingType.type, normalizedHexCoords); this.clearEntitySelection(); - } else if (selectedEntityId && travelPaths.size > 0) { + } else { + const position = getWorldPositionForHex(hexCoords); + this.selectedHexManager.setPosition(position.x, position.z); + this.state.setSelectedHex({ + col: hexCoords.col + FELT_CENTER, + row: hexCoords.row + FELT_CENTER, + }); + } + } + + protected onHexagonRightClick(hexCoords: HexPosition | null): void { + const { selectedEntityId, travelPaths } = this.state.armyActions; + if (selectedEntityId && travelPaths.size > 0 && hexCoords) { const travelPath = travelPaths.get(TravelPaths.posKey(hexCoords, true)); if (travelPath) { const selectedPath = travelPath.path; @@ -255,27 +278,16 @@ export default class WorldmapScene extends HexagonScene { if (selectedPath.length > 0) { const armyMovementManager = new ArmyMovementManager(this.dojo, selectedEntityId); armyMovementManager.moveArmy(selectedPath, isExplored, this.state.currentArmiesTick); - this.clearEntitySelection(); } } - } else { - const position = getWorldPositionForHex(hexCoords); - this.selectedHexManager.setPosition(position.x, position.z); - this.state.setSelectedHex({ - col: hexCoords.col + FELT_CENTER, - row: hexCoords.row + FELT_CENTER, - }); - this.state.setLeftNavigationView(View.EntityView); } } - protected onHexagonRightClick(): void {} - private onArmySelection(selectedEntityId: ID | undefined) { + private onArmySelection(selectedEntityId: ID | null) { if (!selectedEntityId) { this.clearEntitySelection(); return; } - this.state.updateSelectedEntityId(selectedEntityId); const armyMovementManager = new ArmyMovementManager(this.dojo, selectedEntityId); const travelPaths = armyMovementManager.findPaths( this.exploredTiles, @@ -287,12 +299,16 @@ export default class WorldmapScene extends HexagonScene { } private clearEntitySelection() { - this.state.updateSelectedEntityId(null); + // this.state.setSelectedHex(null); this.highlightHexManager.highlightHexes([]); this.state.updateTravelPaths(new Map()); this.structurePreview?.clearPreviewStructure(); } + private clearHexSelection() { + this.state.setSelectedHex(null); + } + setup() { this.controls.maxDistance = 20; this.controls.enablePan = true; diff --git a/client/src/three/systems/SystemManager.ts b/client/src/three/systems/SystemManager.ts index 1c4974406..b46b4ff6e 100644 --- a/client/src/three/systems/SystemManager.ts +++ b/client/src/three/systems/SystemManager.ts @@ -1,23 +1,17 @@ import { ClientComponents } from "@/dojo/createClientComponents"; -import { TOTAL_CONTRIBUTABLE_AMOUNT } from "@/dojo/modelManager/utils/LeaderboardUtils"; -import { SetupResult } from "@/dojo/setup"; +import { configManager, SetupResult } from "@/dojo/setup"; import { HexPosition } from "@/types"; import { Position } from "@/types/Position"; -import { - EternumGlobalConfig, - HYPERSTRUCTURE_RESOURCE_MULTIPLIERS, - HYPERSTRUCTURE_TOTAL_COSTS_SCALED, - ID, - StructureType, -} from "@bibliothecadao/eternum"; +import { divideByPrecision } from "@/ui/utils/utils"; +import { ID, StructureType } from "@bibliothecadao/eternum"; import { Component, ComponentValue, - Has, - HasValue, defineComponentSystem, defineQuery, getComponentValue, + Has, + HasValue, isComponentUpdate, runQuery, } from "@dojoengine/recs"; @@ -81,7 +75,7 @@ export class SystemManager { const protectee = getComponentValue(this.setup.components.Protectee, update.entity); if (protectee) return; - const healthMultiplier = BigInt(EternumGlobalConfig.resources.resourcePrecision); + const healthMultiplier = BigInt(configManager.getResourcePrecision()); const entityOwner = getComponentValue(this.setup.components.EntityOwner, update.entity); if (!entityOwner) return; @@ -174,7 +168,7 @@ export class SystemManager { const position = getComponentValue(this.setup.components.Position, update.entity); if (!position) return; - const healthMultiplier = BigInt(EternumGlobalConfig.resources.resourcePrecision); + const healthMultiplier = BigInt(configManager.getResourcePrecision()); const isEmpty = battle.attack_army_health.current < healthMultiplier && battle.defence_army_health.current < healthMultiplier; @@ -281,28 +275,32 @@ export class SystemManager { progresses: (ComponentValue | undefined)[], hyperstructureEntityId: ID, ) => { + const totalContributableAmount = configManager.getHyperstructureTotalContributableAmount(); let percentage = 0; - const allProgresses = HYPERSTRUCTURE_TOTAL_COSTS_SCALED.map(({ resource, amount: resourceCost }) => { - let foundProgress = progresses.find((progress) => progress!.resource_type === resource); - let progress = { - hyperstructure_entity_id: hyperstructureEntityId, - resource_type: resource, - amount: !foundProgress ? 0 : Number(foundProgress.amount) / EternumGlobalConfig.resources.resourcePrecision, - percentage: !foundProgress - ? 0 - : Math.floor( - (Number(foundProgress.amount) / EternumGlobalConfig.resources.resourcePrecision / resourceCost!) * 100, - ), - costNeeded: resourceCost, - }; - percentage += - (progress.amount * - HYPERSTRUCTURE_RESOURCE_MULTIPLIERS[ - progress.resource_type as keyof typeof HYPERSTRUCTURE_RESOURCE_MULTIPLIERS - ]!) / - TOTAL_CONTRIBUTABLE_AMOUNT; - return progress; - }); + const epsilon = 1e-10; // Small value to account for floating-point precision errors + + const allProgresses = Object.values(configManager.hyperstructureTotalCosts).map( + ({ resource, amount: resourceCost }) => { + let foundProgress = progresses.find((progress) => progress!.resource_type === resource); + let progress = { + hyperstructure_entity_id: hyperstructureEntityId, + resource_type: resource, + amount: !foundProgress ? 0 : divideByPrecision(Number(foundProgress.amount)), + percentage: !foundProgress + ? 0 + : Math.floor((divideByPrecision(Number(foundProgress.amount)) / resourceCost!) * 100), + costNeeded: resourceCost, + }; + percentage += + (progress.amount * configManager.getResourceRarity(progress.resource_type)) / totalContributableAmount; + return progress; + }, + ); + // Adjust percentage to account for floating-point precision issues + if (Math.abs(percentage - 1.0) < epsilon) { + percentage = 1.0; + } + return { allProgresses, percentage }; }; } diff --git a/client/src/ui/components/ModalContainer.tsx b/client/src/ui/components/ModalContainer.tsx index 240ee63ed..78a1fccb4 100644 --- a/client/src/ui/components/ModalContainer.tsx +++ b/client/src/ui/components/ModalContainer.tsx @@ -34,7 +34,7 @@ export const ModalContainer = ({ children, size = "full" }: ModalContainerProps) }, [handleEscapePress]); return ( -
+
@@ -143,7 +146,7 @@ const AddLiquidity = ({ variant="primary" isLoading={false} disabled={!canAdd} - className="text-brown bg-black/90" + className="text-brown bg-brown/90" onClick={() => setOpenConfirmation(true)} > Add Liquidity diff --git a/client/src/ui/components/bank/ConfirmationPopup.tsx b/client/src/ui/components/bank/ConfirmationPopup.tsx index c724f3865..c26dda06c 100644 --- a/client/src/ui/components/bank/ConfirmationPopup.tsx +++ b/client/src/ui/components/bank/ConfirmationPopup.tsx @@ -22,8 +22,8 @@ export const ConfirmationPopup: React.FC = ({ disabled, }) => { return ( -
-
+
+
@@ -210,8 +208,8 @@ export const LiquidityResourceRow = ({ bankEntityId, entityId, resourceId, isFir const InputResourcesPrice = ({ marketManager }: { marketManager: MarketManager }) => { const { setup } = useDojo(); - const inputResources = RESOURCE_INPUTS_SCALED[marketManager.resourceId]; - const outputAmount = RESOURCE_OUTPUTS[marketManager.resourceId]; + const inputResources = configManager.resourceInputs[marketManager.resourceId]; + const outputAmount = configManager.getResourceOutputs(marketManager.resourceId); if (!inputResources?.length) return null; const totalPrice = @@ -225,7 +223,7 @@ const InputResourcesPrice = ({ marketManager }: { marketManager: MarketManager } return sum + Number(price) * resource.amount; }, 0) / outputAmount; return ( -
+
{inputResources.map(({ resource }, index) => ( diff --git a/client/src/ui/components/bank/LiquidityTable.tsx b/client/src/ui/components/bank/LiquidityTable.tsx index c01646c43..dcd781e36 100644 --- a/client/src/ui/components/bank/LiquidityTable.tsx +++ b/client/src/ui/components/bank/LiquidityTable.tsx @@ -27,11 +27,12 @@ export const LiquidityTable = ({ bankEntityId, entity_id }: LiquidityTableProps) const filteredResources = Object.entries(RESOURCE_TIERS).flatMap(([tier, resourceIds]) => { if (tier === "lords") return []; - return resourceIds.filter((resourceId) => - resources - .find((r) => r.id === resourceId) - ?.trait.toLowerCase() - .includes(searchTerm.toLowerCase()), + return resourceIds.filter( + (resourceId) => + resources + .find((r) => r.id === resourceId) + ?.trait.toLowerCase() + .includes(searchTerm.toLowerCase()), ); }); diff --git a/client/src/ui/components/bank/ResourceBar.tsx b/client/src/ui/components/bank/ResourceBar.tsx index 0e1e82f75..28cdc8a8a 100644 --- a/client/src/ui/components/bank/ResourceBar.tsx +++ b/client/src/ui/components/bank/ResourceBar.tsx @@ -19,6 +19,7 @@ export const ResourceBar = ({ disableInput = false, onFocus, onBlur, + max = Infinity, }: { entityId: ID; lordsFee: number; @@ -30,6 +31,7 @@ export const ResourceBar = ({ disableInput?: boolean; onFocus?: () => void; // New prop onBlur?: () => void; // New prop + max?: number; }) => { const { getBalance } = getResourceBalance(); @@ -92,7 +94,7 @@ export const ResourceBar = ({ className="text-2xl border-transparent" value={amount} onChange={handleAmountChange} - max={Infinity} + max={max} arrows={false} allowDecimals onFocus={onFocus} @@ -127,7 +129,7 @@ export const ResourceBar = ({ - + {resources.length > 1 && ( new MarketManager(setup, bankEntityId, ContractAddress(account.address), resourceId), @@ -62,11 +60,14 @@ export const ResourceSwap = ({ } }, [marketManager.resourceId]); + const lordsBalance = useMemo(() => getBalance(entityId, ResourcesIds.Lords).balance, [entityId, getBalance]); + const resourceBalance = useMemo(() => getBalance(entityId, resourceId).balance, [entityId, resourceId, getBalance]); + const hasEnough = useMemo(() => { const amount = isBuyResource ? lordsAmount + ownerFee : resourceAmount; - const balanceId = isBuyResource ? BigInt(ResourcesIds.Lords) : resourceId; - return multiplyByPrecision(amount) <= getBalance(entityId, Number(balanceId)).balance; - }, [isBuyResource, lordsAmount, resourceAmount, getBalance, entityId, resourceId, ownerFee]); + const balance = isBuyResource ? lordsBalance : resourceBalance; + return multiplyByPrecision(amount) <= balance; + }, [isBuyResource, lordsAmount, resourceAmount, resourceBalance, lordsBalance, ownerFee]); const isBankResourcesLocked = useIsResourcesLocked(bankEntityId); const isMyResourcesLocked = useIsResourcesLocked(entityId); @@ -104,15 +105,15 @@ export const ResourceSwap = ({ const calculatedResourceAmount = divideByPrecision( marketManager.calculateResourceOutputForLordsInput( multiplyByPrecision(amount) || 0, - EternumGlobalConfig.banks.lpFeesNumerator, + configManager.getBankConfig().lpFeesNumerator, ), ); setResourceAmount(calculatedResourceAmount); } else { const calculatedResourceAmount = divideByPrecision( marketManager.calculateResourceInputForLordsOutput( - multiplyByPrecision(amount / (1 - OWNER_FEE)) || 0, - EternumGlobalConfig.banks.lpFeesNumerator, + multiplyByPrecision(amount / (1 - configManager.getAdminBankOwnerFee())) || 0, + configManager.getBankConfig().lpFeesNumerator, ), ); setResourceAmount(calculatedResourceAmount); @@ -126,7 +127,7 @@ export const ResourceSwap = ({ const calculatedLordsAmount = divideByPrecision( marketManager.calculateLordsInputForResourceOutput( multiplyByPrecision(amount) || 0, - EternumGlobalConfig.banks.lpFeesNumerator, + configManager.getBankConfig().lpFeesNumerator, ), ); setLordsAmount(calculatedLordsAmount); @@ -134,10 +135,10 @@ export const ResourceSwap = ({ const calculatedLordsAmount = divideByPrecision( marketManager.calculateLordsOutputForResourceInput( multiplyByPrecision(amount) || 0, - EternumGlobalConfig.banks.lpFeesNumerator, + configManager.getBankConfig().lpFeesNumerator, ), ); - const lordsAmountAfterFee = calculatedLordsAmount * (1 - OWNER_FEE); + const lordsAmountAfterFee = calculatedLordsAmount * (1 - configManager.getAdminBankOwnerFee()); setLordsAmount(lordsAmountAfterFee); } }; @@ -159,6 +160,7 @@ export const ResourceSwap = ({ resourceId={isLords ? ResourcesIds.Lords : resourceId} setResourceId={setResourceId} disableInput={disableInput} + max={isLords ? divideByPrecision(lordsBalance) : Infinity} /> ); }, @@ -211,7 +213,12 @@ export const ResourceSwap = ({ @@ -268,8 +275,8 @@ export const ResourceSwap = ({ {( marketManager.slippage( isBuyResource - ? multiplyByPrecision(lordsAmount - lpFee) - : multiplyByPrecision(resourceAmount - lpFee), + ? multiplyByPrecision(Math.abs(lordsAmount - lpFee)) + : multiplyByPrecision(Math.abs(resourceAmount - lpFee)), isBuyResource, ) || 0 ).toFixed(2)}{" "} diff --git a/client/src/ui/components/cityview/realm/SettleRealmComponent.tsx b/client/src/ui/components/cityview/realm/SettleRealmComponent.tsx index 2f59dd566..37f31c6e1 100644 --- a/client/src/ui/components/cityview/realm/SettleRealmComponent.tsx +++ b/client/src/ui/components/cityview/realm/SettleRealmComponent.tsx @@ -2,10 +2,8 @@ import { useState } from "react"; import Button from "../../../elements/Button"; import { MAX_REALMS } from "@/ui/constants"; -import { toValidAscii } from "@/ui/utils/utils"; import { getOrderName, orders } from "@bibliothecadao/eternum"; import clsx from "clsx"; -import { shortString } from "starknet"; import { order_statments } from "../../../../data/orders"; import { useDojo } from "../../../../hooks/context/DojoContext"; import { useRealm } from "../../../../hooks/helpers/useRealm"; @@ -42,26 +40,12 @@ const SettleRealmComponent = () => { // take next realm id let realm = getRealm(new_realm_id); if (!realm) return; - - const realmNameInAscii = toValidAscii(realm.name); - - calldata.push({ - realm_name: shortString.encodeShortString(realmNameInAscii), - realm_id: Number(realm.realmId), - order: realm.order, - wonder: realm.wonder, - regions: realm.regions, - resource_types_count: realm.resourceTypesCount, - resource_types_packed: realm.resourceTypesPacked, - rivers: realm.rivers, - harbors: realm.harbors, - cities: realm.cities, - }); + calldata.push(Number(realm.realmId)); } await create_multiple_realms({ signer: account, - realms: [calldata[0]], + realm_ids: [calldata[0]], }); setIsLoading(false); playSign(); diff --git a/client/src/ui/components/construction/SelectPreviewBuilding.tsx b/client/src/ui/components/construction/SelectPreviewBuilding.tsx index 923eba42f..add4f198d 100644 --- a/client/src/ui/components/construction/SelectPreviewBuilding.tsx +++ b/client/src/ui/components/construction/SelectPreviewBuilding.tsx @@ -1,32 +1,12 @@ -import clsx from "clsx"; - -import useUIStore from "@/hooks/store/useUIStore"; -import { Tabs } from "@/ui/elements/tab"; -import { - BUILDING_CAPACITY, - BUILDING_POPULATION, - BUILDING_RESOURCE_PRODUCED, - BuildingEnumToString, - BuildingType, - EternumGlobalConfig, - findResourceById, - ID, - RESOURCE_BUILDING_COSTS_SCALED, - RESOURCE_INPUTS_SCALED, - RESOURCE_OUTPUTS, - ResourceCost as ResourceCostType, - ResourcesIds, - WORLD_CONFIG_ID, -} from "@bibliothecadao/eternum"; - import { ReactComponent as InfoIcon } from "@/assets/icons/common/info.svg"; +import { ClientComponents } from "@/dojo/createClientComponents"; +import { configManager } from "@/dojo/setup"; +import { DojoResult, useDojo } from "@/hooks/context/DojoContext"; import { useQuestClaimStatus } from "@/hooks/helpers/useQuests"; import { useGetRealm } from "@/hooks/helpers/useRealm"; import { getResourceBalance } from "@/hooks/helpers/useResources"; import { useQuestStore } from "@/hooks/store/useQuestStore"; - -import { ClientComponents } from "@/dojo/createClientComponents"; -import { DojoResult, useDojo } from "@/hooks/context/DojoContext"; +import useUIStore from "@/hooks/store/useUIStore"; import { usePlayResourceSound } from "@/hooks/useUISound"; import { ResourceMiningTypes } from "@/types"; import { QuestId } from "@/ui/components/quest/questDetails"; @@ -35,11 +15,26 @@ import { Headline } from "@/ui/elements/Headline"; import { HintModalButton } from "@/ui/elements/HintModalButton"; import { ResourceCost } from "@/ui/elements/ResourceCost"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; +import { Tabs } from "@/ui/elements/tab"; import { unpackResources } from "@/ui/utils/packedData"; import { hasEnoughPopulationForBuilding } from "@/ui/utils/realms"; -import { getEntityIdFromKeys, isResourceProductionBuilding, ResourceIdToMiningType } from "@/ui/utils/utils"; -import { BUILDING_COSTS_SCALED } from "@bibliothecadao/eternum"; +import { + divideByPrecision, + getEntityIdFromKeys, + isResourceProductionBuilding, + ResourceIdToMiningType, +} from "@/ui/utils/utils"; +import { + BuildingEnumToString, + BuildingType, + findResourceById, + ID, + ResourceCost as ResourceCostType, + ResourcesIds, + WORLD_CONFIG_ID, +} from "@bibliothecadao/eternum"; import { Component, getComponentValue } from "@dojoengine/recs"; +import clsx from "clsx"; import React, { useMemo, useState } from "react"; import { HintSection } from "../hints/HintModal"; @@ -75,7 +70,7 @@ export const SelectPreviewBuildingMenu = ({ className, entityId }: { className?: const realmResourceIds = useMemo(() => { if (realm) { - return unpackResources(BigInt(realm.resourceTypesPacked), realm.resourceTypesCount); + return unpackResources(BigInt(realm.resourceTypesPacked)); } else { return []; } @@ -85,7 +80,7 @@ export const SelectPreviewBuildingMenu = ({ className, entityId }: { className?: Object.keys(cost).every((resourceId) => { const resourceCost = cost[Number(resourceId)]; const balance = getBalance(entityId, resourceCost.resource); - return balance.balance / EternumGlobalConfig.resources.resourcePrecision >= resourceCost.amount; + return divideByPrecision(balance.balance) >= resourceCost.amount; }); const [selectedTab, setSelectedTab] = useState(1); @@ -112,13 +107,13 @@ export const SelectPreviewBuildingMenu = ({ className, entityId }: { className?: const buildingCosts = getResourceBuildingCosts(entityId, dojo, resourceId); if (!buildingCosts) return; - const cost = [...buildingCosts, ...RESOURCE_INPUTS_SCALED[resourceId]]; + const cost = [...buildingCosts, ...configManager.resourceInputs[resourceId]]; const hasBalance = checkBalance(cost); const hasEnoughPopulation = hasEnoughPopulationForBuilding( realm, - BUILDING_POPULATION[BuildingType.Resource], + configManager.getBuildingPopConfig(BuildingType.Resource).population, ); const canBuild = hasBalance && realm?.hasCapacity && hasEnoughPopulation; @@ -347,7 +342,7 @@ const BuildingCard = ({
{(!hasFunds || !hasPopulation) && ( -
+
{!hasFunds && } {!hasPopulation && } @@ -387,7 +382,7 @@ const BuildingCard = ({ />
-
+
{isResourceProductionBuilding(buildingId) && resourceName && ( )} @@ -409,14 +404,15 @@ export const ResourceInfo = ({ hintModal?: boolean; }) => { const dojo = useDojo(); - const cost = RESOURCE_INPUTS_SCALED[resourceId]; + const cost = configManager.resourceInputs[resourceId]; const buildingCost = getResourceBuildingCosts(entityId ?? 0, dojo, resourceId) ?? []; - const population = BUILDING_POPULATION[BuildingType.Resource]; - const capacity = BUILDING_CAPACITY[BuildingType.Resource]; + const buildingPopCapacityConfig = configManager.getBuildingPopConfig(BuildingType.Resource); + const population = buildingPopCapacityConfig.population; + const capacity = buildingPopCapacityConfig.capacity; - const amountProducedPerTick = RESOURCE_OUTPUTS[resourceId]; + const amountProducedPerTick = configManager.getResourceOutputs(resourceId); const { getBalance } = getResourceBalance(); @@ -532,16 +528,14 @@ export const BuildingInfo = ({ const buildingCost = getBuildingCosts(entityId ?? 0, dojo, buildingId) || []; - const population = BUILDING_POPULATION[buildingId as keyof typeof BUILDING_POPULATION] || 0; - const capacity = BUILDING_CAPACITY[buildingId as keyof typeof BUILDING_CAPACITY] || 0; - const resourceProduced = BUILDING_RESOURCE_PRODUCED[buildingId as keyof typeof BUILDING_RESOURCE_PRODUCED]; - const ongoingCost = - resourceProduced !== undefined - ? RESOURCE_INPUTS_SCALED[resourceProduced as keyof typeof RESOURCE_INPUTS_SCALED] || 0 - : 0; + const buildingPopCapacityConfig = configManager.getBuildingPopConfig(buildingId); + const population = buildingPopCapacityConfig.population; + const capacity = buildingPopCapacityConfig.capacity; - const perTick = - resourceProduced !== undefined ? RESOURCE_OUTPUTS[resourceProduced as keyof typeof RESOURCE_OUTPUTS] || 0 : 0; + const resourceProduced = configManager.getResourceBuildingProduced(buildingId); + const ongoingCost = resourceProduced !== undefined ? configManager.resourceInputs[resourceProduced] || 0 : 0; + + const perTick = resourceProduced !== undefined ? configManager.getResourceOutputs(resourceProduced) || 0 : 0; const { getBalance } = getResourceBalance(); @@ -650,7 +644,7 @@ export const BuildingInfo = ({ }; const getConsumedBy = (resourceProduced: ResourcesIds) => { - return Object.entries(RESOURCE_INPUTS_SCALED) + return Object.entries(configManager.resourceInputs) .map(([resourceId, inputs]) => { const resource = inputs.find( (input: { resource: number; amount: number }) => input.resource === resourceProduced, @@ -681,7 +675,7 @@ const getResourceBuildingCosts = (realmEntityId: ID, dojo: DojoResult, resourceI let updatedCosts: ResourceCostType[] = []; - RESOURCE_BUILDING_COSTS_SCALED[Number(resourceId)].forEach((cost) => { + configManager.resourceBuildingCosts[Number(resourceId)].forEach((cost) => { const baseCost = cost.amount; const percentageAdditionalCost = (baseCost * (buildingGeneralConfig.base_cost_percent_increase / 100)) / 100; const scaleFactor = Math.max(0, buildingQuantity ?? 0 - 1); @@ -692,14 +686,7 @@ const getResourceBuildingCosts = (realmEntityId: ID, dojo: DojoResult, resourceI }; const getBuildingCosts = (realmEntityId: ID, dojo: DojoResult, buildingCategory: BuildingType) => { - const buildingGeneralConfig = getComponentValue( - dojo.setup.components.BuildingGeneralConfig, - getEntityIdFromKeys([WORLD_CONFIG_ID]), - ); - - if (!buildingGeneralConfig) { - return; - } + const buildingBaseCostPercentIncrease = configManager.getBuildingBaseCostPercentIncrease(); const buildingQuantity = getBuildingQuantity( realmEntityId, @@ -709,9 +696,9 @@ const getBuildingCosts = (realmEntityId: ID, dojo: DojoResult, buildingCategory: let updatedCosts: ResourceCostType[] = []; - BUILDING_COSTS_SCALED[Number(buildingCategory)].forEach((cost) => { + configManager.buildingCosts[Number(buildingCategory)].forEach((cost) => { const baseCost = cost.amount; - const percentageAdditionalCost = (baseCost * (buildingGeneralConfig.base_cost_percent_increase / 100)) / 100; + const percentageAdditionalCost = (baseCost * (buildingBaseCostPercentIncrease / 100)) / 100; const scaleFactor = Math.max(0, buildingQuantity ?? 0 - 1); const totalCost = baseCost + scaleFactor * scaleFactor * percentageAdditionalCost; updatedCosts.push({ resource: cost.resource, amount: totalCost }); diff --git a/client/src/ui/components/entities/Entity.tsx b/client/src/ui/components/entities/Entity.tsx index c414b9bac..7ccb1b2b5 100644 --- a/client/src/ui/components/entities/Entity.tsx +++ b/client/src/ui/components/entities/Entity.tsx @@ -34,7 +34,7 @@ type EntityProps = { export const Entity = ({ entityId, ...props }: EntityProps) => { const dojo = useDojo(); - const { getEntityInfo } = useEntitiesUtils(); + const { getEntityInfo, getEntityName } = useEntitiesUtils(); const { getResourcesFromBalance } = getResourcesUtils(); const { getOwnedEntityOnPosition } = useOwnedEntitiesOnPosition(); const nextBlockTimestamp = useUIStore.getState().nextBlockTimestamp; @@ -70,7 +70,7 @@ export const Entity = ({ entityId, ...props }: EntityProps) => { case EntityState.Idle: case EntityState.WaitingToOffload: return depositEntityId !== undefined && hasResources ? ( -
Waiting to offload
+
Waiting to offload to {getEntityName(depositEntityId)}
) : (
Idle
); diff --git a/client/src/ui/components/hints/Buildings.tsx b/client/src/ui/components/hints/Buildings.tsx index 2e5c92688..c9c543332 100644 --- a/client/src/ui/components/hints/Buildings.tsx +++ b/client/src/ui/components/hints/Buildings.tsx @@ -1,32 +1,30 @@ +import { configManager } from "@/dojo/setup"; import { BUILDING_IMAGES_PATH } from "@/ui/config"; import { Headline } from "@/ui/elements/Headline"; import { ResourceCost } from "@/ui/elements/ResourceCost"; -import { - BUILDING_CAPACITY, - BUILDING_COSTS_SCALED, - BUILDING_POPULATION, - BUILDING_RESOURCE_PRODUCED, - BuildingEnumToString, - BuildingType, -} from "@bibliothecadao/eternum"; +import { BuildingEnumToString, BuildingType } from "@bibliothecadao/eternum"; import { useMemo } from "react"; export const Buildings = () => { const buildingTable = useMemo(() => { const buildings = []; - for (const buildingId of Object.keys(BUILDING_RESOURCE_PRODUCED) as unknown as BuildingType[]) { - if (BUILDING_COSTS_SCALED[buildingId].length !== 0) { - const population = BUILDING_POPULATION[buildingId]; + for (const buildingId of Object.keys(BuildingType) as unknown as BuildingType[]) { + if (isNaN(Number(buildingId))) continue; - const capacity = BUILDING_CAPACITY[buildingId]; + const buildingCosts = configManager.buildingCosts[buildingId]; + + if (buildingCosts.length !== 0) { + const buildingPopCapacityConfig = configManager.getBuildingPopConfig(buildingId); + const population = buildingPopCapacityConfig.population; + const capacity = buildingPopCapacityConfig.capacity; const calldata = { building_category: buildingId, building_capacity: capacity, building_population: population, - building_resource_type: BUILDING_RESOURCE_PRODUCED[buildingId], - cost_of_building: BUILDING_COSTS_SCALED[buildingId].map((cost) => { + building_resource_type: configManager.getResourceBuildingProduced(buildingId), + cost_of_building: buildingCosts.map((cost) => { return { ...cost, amount: cost.amount, @@ -61,7 +59,7 @@ export const Buildings = () => { {" "}
{BuildingEnumToString[building.building_category]}
diff --git a/client/src/ui/components/hints/Combat.tsx b/client/src/ui/components/hints/Combat.tsx index b8951ebb6..2e6924997 100644 --- a/client/src/ui/components/hints/Combat.tsx +++ b/client/src/ui/components/hints/Combat.tsx @@ -1,9 +1,12 @@ +import { configManager } from "@/dojo/setup"; import { Headline } from "@/ui/elements/Headline"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; -import { EternumGlobalConfig, ResourcesIds, TROOPS_STAMINAS } from "@bibliothecadao/eternum"; +import { ResourcesIds } from "@bibliothecadao/eternum"; import { tableOfContents } from "./utils"; export const Combat = () => { + const troopConfig = configManager.getTroopConfig(); + const chapter = [ { title: "Protecting your Structures", @@ -12,7 +15,7 @@ export const Combat = () => { }, { title: "Exploration", - content: `An offensive army is crucial for exploration, engaging foes, and discovering treasures in Eternum. Your army's stamina fuels these expeditions. You can only have a certain number of attacking armies per Realm. You start at ${EternumGlobalConfig.troop.baseArmyNumberForStructure} and get ${EternumGlobalConfig.troop.armyExtraPerMilitaryBuilding} per military building.`, + content: `An offensive army is crucial for exploration, engaging foes, and discovering treasures in Eternum. Your army's stamina fuels these expeditions. You can only have a certain number of attacking armies per Realm. You start at ${troopConfig.baseArmyNumberForStructure} and get ${troopConfig.armyExtraPerMilitaryBuilding} per military building.`, }, { title: "Battles", @@ -92,7 +95,9 @@ const Battles = () => { }; const Troops = () => { - const troopHealth = EternumGlobalConfig.troop.health; + const troopConfig = configManager.getTroopConfig(); + const advantagePercent = troopConfig.advantagePercent / 100; + const disadvantagePercent = troopConfig.disadvantagePercent / 100; return ( @@ -110,36 +115,42 @@ const Troops = () => { resourceId={ResourcesIds.Knight} strength={ } - health={troopHealth} + health={troopConfig.health} /> } - health={troopHealth} + health={troopConfig.health} /> } - health={troopHealth} + health={troopConfig.health} />
@@ -162,9 +173,7 @@ const TroopRow = ({ - - {TROOPS_STAMINAS[resourceId as keyof typeof TROOPS_STAMINAS]} - + {configManager.getTroopStaminaConfig(resourceId)} {strength} {health} @@ -175,14 +184,15 @@ const Strength = ({ strength, strongAgainst, weakAgainst, + advantagePercent, + disadvantagePercent, }: { strength: number; strongAgainst: string; weakAgainst: string; + advantagePercent: number; + disadvantagePercent: number; }) => { - const advantagePercent = (EternumGlobalConfig.troop.advantagePercent / 10000) * 100; - const disadvantagePercent = (EternumGlobalConfig.troop.disadvantagePercent / 10000) * 100; - return (
{/*
{strength}
*/} diff --git a/client/src/ui/components/hints/GettingStarted.tsx b/client/src/ui/components/hints/GettingStarted.tsx index 31a53cee6..482d51717 100644 --- a/client/src/ui/components/hints/GettingStarted.tsx +++ b/client/src/ui/components/hints/GettingStarted.tsx @@ -1,5 +1,6 @@ +import { configManager } from "@/dojo/setup"; import { Headline } from "@/ui/elements/Headline"; -import { EternumGlobalConfig } from "@bibliothecadao/eternum"; +import { TickIds } from "@bibliothecadao/eternum"; import { tableOfContents } from "./utils"; export const GettingStarted = () => { @@ -7,7 +8,7 @@ export const GettingStarted = () => { { title: "The Time Cycle", content: `Everything in this world revolves around an Eternum Day. A day in Eternum is ${ - EternumGlobalConfig.tick.armiesTickIntervalInSeconds / 60 + configManager.getTick(TickIds.Armies) / 60 } minutes.`, }, { diff --git a/client/src/ui/components/hints/HintModal.tsx b/client/src/ui/components/hints/HintModal.tsx index f90b51163..a96de9454 100644 --- a/client/src/ui/components/hints/HintModal.tsx +++ b/client/src/ui/components/hints/HintModal.tsx @@ -90,7 +90,7 @@ export const HintModal = ({ initialActiveSection }: HintModalProps) => { return ( -
+

The Lordpedia

diff --git a/client/src/ui/components/hints/Realm.tsx b/client/src/ui/components/hints/Realm.tsx index a041d1210..fef78b367 100644 --- a/client/src/ui/components/hints/Realm.tsx +++ b/client/src/ui/components/hints/Realm.tsx @@ -1,7 +1,7 @@ +import { configManager } from "@/dojo/setup"; import { Headline } from "@/ui/elements/Headline"; import { ResourceCost } from "@/ui/elements/ResourceCost"; -import { multiplyByPrecision } from "@/ui/utils/utils"; -import { LEVEL_DESCRIPTIONS, REALM_UPGRADE_COSTS, RealmLevels } from "@bibliothecadao/eternum"; +import { LEVEL_DESCRIPTIONS, RealmLevels } from "@bibliothecadao/eternum"; import { useMemo } from "react"; export const Realm = () => { @@ -26,11 +26,11 @@ export const Realm = () => { const LevelTable = () => { const levelTable = useMemo(() => { - return Object.entries(REALM_UPGRADE_COSTS).map(([level, costs]) => ({ + return Object.entries(configManager.realmUpgradeCosts).map(([level, costs]) => ({ level: RealmLevels[level as keyof typeof RealmLevels], cost: costs.map((cost) => ({ ...cost, - amount: multiplyByPrecision(cost.amount), + amount: cost.amount, })), })); }, []); diff --git a/client/src/ui/components/hints/Resources.tsx b/client/src/ui/components/hints/Resources.tsx index 95c7fd238..9721dcc3f 100644 --- a/client/src/ui/components/hints/Resources.tsx +++ b/client/src/ui/components/hints/Resources.tsx @@ -1,15 +1,9 @@ +import { configManager } from "@/dojo/setup"; import { Headline } from "@/ui/elements/Headline"; import { ResourceCost } from "@/ui/elements/ResourceCost"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; import { currencyFormat, gramToKg, multiplyByPrecision } from "@/ui/utils/utils"; -import { - CapacityConfigCategory, - EternumGlobalConfig, - RESOURCE_INPUTS_SCALED, - RESOURCE_OUTPUTS_SCALED, - ResourcesIds, - findResourceById, -} from "@bibliothecadao/eternum"; +import { CapacityConfigCategory, EternumGlobalConfig, ResourcesIds, findResourceById } from "@bibliothecadao/eternum"; import { useMemo } from "react"; import { tableOfContents } from "./utils"; @@ -40,8 +34,8 @@ export const Resources = () => { Storehouses determine your resource storage capacity. Each storehouse adds {` ${ - gramToKg(Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Storehouse])) / - (EternumGlobalConfig.resources.resourceMultiplier * EternumGlobalConfig.resources.resourcePrecision) + gramToKg(configManager.getCapacityConfig(CapacityConfigCategory.Storehouse)) / + multiplyByPrecision(EternumGlobalConfig.resources.resourceMultiplier) }M capacity per resource type`} . Build more of them to increase storage. @@ -70,13 +64,13 @@ export const Resources = () => { const ResourceTable = () => { const resourceTable = useMemo(() => { const resources = []; - for (const resourceId of Object.keys(RESOURCE_INPUTS_SCALED) as unknown as ResourcesIds[]) { + for (const resourceId of Object.keys(configManager.resourceInputs) as unknown as ResourcesIds[]) { if (resourceId == ResourcesIds.Lords) continue; const calldata = { resource: findResourceById(Number(resourceId)), - amount: RESOURCE_OUTPUTS_SCALED[resourceId], + amount: configManager.getResourceOutputs(resourceId), resource_type: resourceId, - cost: RESOURCE_INPUTS_SCALED[resourceId].map((cost: any) => ({ + cost: configManager.resourceInputs[resourceId].map((cost: any) => ({ ...cost, amount: multiplyByPrecision(cost.amount), })), diff --git a/client/src/ui/components/hints/TheMap.tsx b/client/src/ui/components/hints/TheMap.tsx index 129aa6054..68abf62fd 100644 --- a/client/src/ui/components/hints/TheMap.tsx +++ b/client/src/ui/components/hints/TheMap.tsx @@ -1,8 +1,9 @@ import { ReactComponent as Lightning } from "@/assets/icons/common/lightning.svg"; +import { configManager } from "@/dojo/setup"; import { Headline } from "@/ui/elements/Headline"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; -import { multiplyByPrecision } from "@/ui/utils/utils"; -import { EternumGlobalConfig, ResourcesIds, TROOPS_FOOD_CONSUMPTION } from "@bibliothecadao/eternum"; +import {} from "@/ui/utils/utils"; +import { ResourcesIds } from "@bibliothecadao/eternum"; import { tableOfContents } from "./utils"; export const TheMap = () => { @@ -43,6 +44,9 @@ export const TheMap = () => { }; const ExplorationTable = () => { + const travelStaminaCost = configManager.getTravelStaminaCost(); + const exploreStaminaCost = configManager.getExploreStaminaCost(); + return ( @@ -60,7 +64,7 @@ const ExplorationTable = () => { Travel - +
{EternumGlobalConfig.stamina.travelCost}{travelStaminaCost} @@ -84,13 +88,13 @@ const ExplorationTable = () => { - +
- {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Crossbowman].travel_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Crossbowman).travelFishBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Knight].travel_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Knight).travelFishBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Paladin].travel_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Paladin).travelFishBurnAmount} @@ -98,13 +102,13 @@ const ExplorationTable = () => {
- {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Crossbowman].travel_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Crossbowman).travelWheatBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Knight].travel_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Knight).travelWheatBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Paladin].travel_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Paladin).travelWheatBurnAmount} @@ -121,7 +125,7 @@ const ExplorationTable = () => { Exploration {EternumGlobalConfig.stamina.exploreCost}{exploreStaminaCost} @@ -145,13 +149,13 @@ const ExplorationTable = () => { @@ -109,9 +106,9 @@ const HyperstructureCreationTable = () => { }; const HyperstructureConstructionTable = () => { - const constructionCost = HYPERSTRUCTURE_TOTAL_COSTS_SCALED.filter( - (cost) => cost.resource !== ResourcesIds["AncientFragment"], - ).map((cost) => ({ ...cost })); + const constructionCost = configManager.structureCosts[StructureType.Hyperstructure] + .filter((cost) => cost.resource !== ResourcesIds["AncientFragment"]) + .map((cost) => ({ ...cost })); return (
- {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Crossbowman].explore_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Crossbowman).exploreFishBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Knight].explore_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Knight).exploreFishBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Paladin].explore_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Paladin).exploreFishBurnAmount} @@ -159,13 +163,13 @@ const ExplorationTable = () => {
- {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Crossbowman].explore_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Crossbowman).exploreWheatBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Knight].explore_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Knight).exploreWheatBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Paladin].explore_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Paladin).exploreWheatBurnAmount} diff --git a/client/src/ui/components/hints/Transfers.tsx b/client/src/ui/components/hints/Transfers.tsx index ac4ca297a..222ed06fc 100644 --- a/client/src/ui/components/hints/Transfers.tsx +++ b/client/src/ui/components/hints/Transfers.tsx @@ -1,13 +1,8 @@ +import { configManager } from "@/dojo/setup"; import { BUILDING_IMAGES_PATH } from "@/ui/config"; import { GRAMS_PER_KG } from "@/ui/constants"; import { Headline } from "@/ui/elements/Headline"; -import { - BuildingType, - CapacityConfigCategory, - EternumGlobalConfig, - ResourcesIds, - WEIGHTS_GRAM, -} from "@bibliothecadao/eternum"; +import { BuildingType, CapacityConfigCategory, ResourcesIds } from "@bibliothecadao/eternum"; import { tableOfContents } from "./utils"; export const Transfers = () => { @@ -27,14 +22,16 @@ export const Transfers = () => {

Donkey carry capacity:{" "} - - {Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Donkey]) / GRAMS_PER_KG} kg - + {configManager.getCapacityConfig(CapacityConfigCategory.Donkey) / GRAMS_PER_KG} kg

-
Lords: {`${WEIGHTS_GRAM[ResourcesIds.Lords] / GRAMS_PER_KG} kg/unit`}
-
Food: {`${WEIGHTS_GRAM[ResourcesIds.Wheat] / GRAMS_PER_KG} kg/unit`}
-
Resource: {`${WEIGHTS_GRAM[ResourcesIds.Wood] / GRAMS_PER_KG} kg/unit`}
+
+ Lords: {`${configManager.getResourceWeight(ResourcesIds.Lords) / GRAMS_PER_KG} kg/unit`} +
+
Food: {`${configManager.getResourceWeight(ResourcesIds.Wheat) / GRAMS_PER_KG} kg/unit`}
+
+ Resource: {`${configManager.getResourceWeight(ResourcesIds.Wood) / GRAMS_PER_KG} kg/unit`} +
), diff --git a/client/src/ui/components/hints/WorldStructures.tsx b/client/src/ui/components/hints/WorldStructures.tsx index 23aff6694..30e32f2f5 100644 --- a/client/src/ui/components/hints/WorldStructures.tsx +++ b/client/src/ui/components/hints/WorldStructures.tsx @@ -1,15 +1,8 @@ +import { configManager } from "@/dojo/setup"; import { Headline } from "@/ui/elements/Headline"; import { ResourceCost } from "@/ui/elements/ResourceCost"; import { formatTime } from "@/ui/utils/utils"; -import { - findResourceById, - HYPERSTRUCTURE_POINTS_PER_CYCLE, - HYPERSTRUCTURE_TIME_BETWEEN_SHARES_CHANGE_S, - HYPERSTRUCTURE_TOTAL_COSTS_SCALED, - ResourcesIds, - STRUCTURE_COSTS_SCALED, - StructureType, -} from "@bibliothecadao/eternum"; +import { findResourceById, ResourcesIds, StructureType } from "@bibliothecadao/eternum"; import { useMemo } from "react"; import { STRUCTURE_IMAGE_PATHS } from "../structures/construction/StructureConstructionMenu"; import { tableOfContents } from "./utils"; @@ -60,9 +53,11 @@ export const WorldStructures = () => { const HyperstructureCreationTable = () => { const structureId = StructureType["Hyperstructure"]; - const creationCost = STRUCTURE_COSTS_SCALED[structureId].map((cost) => ({ - ...cost, - })); + const creationCost = configManager.structureCosts[structureId] + .filter((cost) => cost.resource === ResourcesIds["AncientFragment"]) + .map((cost) => ({ + ...cost, + })); return ( <> @@ -93,13 +88,15 @@ const HyperstructureCreationTable = () => {
Hyperstructures are key to victory and can be constructed collaboratively. Once built, Hyperstructures - generate {HYPERSTRUCTURE_POINTS_PER_CYCLE} points per tick. Once completed, the Hyperstructure owner can - distribute shares to others, allowing shareholders to earn a portion of the generated points. + generate {configManager.getHyperstructureConfig().pointsPerCycle} points per tick. Once completed, the + Hyperstructure owner can distribute shares to others, allowing shareholders to earn a portion of the + generated points.

Defending your Hyperstructure is crucial. If captured by another player, they can redistribute the shares, potentially cutting off your point income. -
A new set of shareholers can be set every {formatTime(HYPERSTRUCTURE_TIME_BETWEEN_SHARES_CHANGE_S)}. +
A new set of shareholers can be set every{" "} + {formatTime(configManager.getHyperstructureConfig().timeBetweenSharesChange)}.
diff --git a/client/src/ui/components/hyperstructures/HyperstructurePanel.tsx b/client/src/ui/components/hyperstructures/HyperstructurePanel.tsx index 8eaf1d0ba..ec102496b 100644 --- a/client/src/ui/components/hyperstructures/HyperstructurePanel.tsx +++ b/client/src/ui/components/hyperstructures/HyperstructurePanel.tsx @@ -1,8 +1,10 @@ import { LeaderboardManager } from "@/dojo/modelManager/LeaderboardManager"; import { calculateCompletionPoints } from "@/dojo/modelManager/utils/LeaderboardUtils"; +import { configManager } from "@/dojo/setup"; import { useDojo } from "@/hooks/context/DojoContext"; import { useContributions } from "@/hooks/helpers/useContributions"; import { useEntitiesUtils } from "@/hooks/helpers/useEntities"; +import { useGuilds } from "@/hooks/helpers/useGuilds"; import { ProgressWithPercentage, useHyperstructureProgress, @@ -10,15 +12,10 @@ import { } from "@/hooks/helpers/useHyperstructures"; import useUIStore from "@/hooks/store/useUIStore"; import Button from "@/ui/elements/Button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui/elements/Select"; import TextInput from "@/ui/elements/TextInput"; -import { currencyIntlFormat, getEntityIdFromKeys } from "@/ui/utils/utils"; -import { - ContractAddress, - EternumGlobalConfig, - HYPERSTRUCTURE_POINTS_PER_CYCLE, - HYPERSTRUCTURE_TOTAL_COSTS_SCALED, - MAX_NAME_LENGTH, -} from "@bibliothecadao/eternum"; +import { currencyIntlFormat, getEntityIdFromKeys, multiplyByPrecision, separateCamelCase } from "@/ui/utils/utils"; +import { Access, ContractAddress, MAX_NAME_LENGTH } from "@bibliothecadao/eternum"; import { useComponentValue } from "@dojoengine/react"; import { useMemo, useState } from "react"; import { ContributionSummary } from "./ContributionSummary"; @@ -37,16 +34,19 @@ export const HyperstructurePanel = ({ entity }: any) => { account: { account }, network: { provider }, setup: { - systemCalls: { contribute_to_construction, set_private }, + systemCalls: { contribute_to_construction, set_access }, components: { Hyperstructure }, }, } = useDojo(); + const { getGuildFromPlayerAddress } = useGuilds(); + const [isLoading, setIsLoading] = useState(Loading.None); const [editName, setEditName] = useState(false); const [naming, setNaming] = useState(""); const [resetContributions, setResetContributions] = useState(false); + const setTooltip = useUIStore((state) => state.setTooltip); const structureEntityId = useUIStore((state) => state.structureEntityId); const progresses = useHyperstructureProgress(entity.entity_id); @@ -64,10 +64,12 @@ export const HyperstructurePanel = ({ entity }: any) => { const hyperstructure = useComponentValue(Hyperstructure, getEntityIdFromKeys([BigInt(entity.entity_id)])); + const playerGuild = useMemo(() => getGuildFromPlayerAddress(ContractAddress(account.address)), []); + const contributeToConstruction = async () => { const formattedContributions = Object.entries(newContributions).map(([resourceId, amount]) => ({ resource: Number(resourceId), - amount: amount * EternumGlobalConfig.resources.resourcePrecision, + amount: multiplyByPrecision(amount), })); setIsLoading(Loading.Contribute); @@ -88,7 +90,7 @@ export const HyperstructurePanel = ({ entity }: any) => { const resourceElements = useMemo(() => { if (progresses.percentage === 100) return; - return HYPERSTRUCTURE_TOTAL_COSTS_SCALED.map(({ resource }) => { + return Object.values(configManager.hyperstructureTotalCosts).map(({ resource }) => { const progress = progresses.progresses.find( (progress: ProgressWithPercentage) => progress.resource_type === resource, ); @@ -106,6 +108,20 @@ export const HyperstructurePanel = ({ entity }: any) => { }); }, [progresses, myContributions]); + const canContribute = useMemo(() => { + const hyperstructureOwnerGuild = getGuildFromPlayerAddress(BigInt(entity?.owner || 0)); + return ( + entity.isOwner || + (hyperstructure?.access === Access[Access.GuildOnly] && + playerGuild?.guildEntityId !== undefined && + playerGuild.guildEntityId !== 0 && + hyperstructureOwnerGuild?.guildEntityId !== undefined && + hyperstructureOwnerGuild.guildEntityId !== 0 && + hyperstructureOwnerGuild.guildEntityId === playerGuild.guildEntityId) || + hyperstructure?.access === Access[Access.Public] + ); + }, [newContributions]); + const initialPoints = useMemo(() => { return calculateCompletionPoints(myContributions); }, [myContributions, updates]); @@ -114,13 +130,13 @@ export const HyperstructurePanel = ({ entity }: any) => { return LeaderboardManager.instance().getAddressShares(ContractAddress(account.address), entity.entity_id); }, [myContributions, updates]); - const setPrivate = async () => { + const setAccess = async (access: bigint) => { setIsLoading(Loading.SetPrivate); try { - await set_private({ + await set_access({ signer: account, hyperstructure_entity_id: entity.entity_id, - to_private: !hyperstructure?.private, + access, }); } finally { setIsLoading(Loading.None); @@ -173,9 +189,34 @@ export const HyperstructurePanel = ({ entity }: any) => { - + {hyperstructure && entity.isOwner && ( + + )} )} @@ -206,7 +247,7 @@ export const HyperstructurePanel = ({ entity }: any) => {
Points/cycle
- {currencyIntlFormat((myShares || 0) * HYPERSTRUCTURE_POINTS_PER_CYCLE)} + {currencyIntlFormat((myShares || 0) * configManager.getHyperstructureConfig().pointsPerCycle)}
@@ -219,14 +260,28 @@ export const HyperstructurePanel = ({ entity }: any) => {
{resourceElements}
- + +
)} diff --git a/client/src/ui/components/hyperstructures/StructureCard.tsx b/client/src/ui/components/hyperstructures/StructureCard.tsx index a79d73e0a..23ae6aadb 100644 --- a/client/src/ui/components/hyperstructures/StructureCard.tsx +++ b/client/src/ui/components/hyperstructures/StructureCard.tsx @@ -1,3 +1,4 @@ +import { configManager } from "@/dojo/setup"; import { useDojo } from "@/hooks/context/DojoContext"; import { ArmyInfo, getArmyByEntityId } from "@/hooks/helpers/useArmies"; import { useStructureAtPosition } from "@/hooks/helpers/useStructures"; @@ -17,8 +18,6 @@ import { useMemo, useState } from "react"; import { StructureListItem } from "../worldmap/structures/StructureListItem"; import { ResourceExchange } from "./ResourceExchange"; -const MAX_TROOPS_PER_ARMY = EternumGlobalConfig.troop.maxTroopCount; - export const StructureCard = ({ position, ownArmySelected, @@ -157,6 +156,8 @@ const TroopExchange = ({ const { getArmy } = getArmyByEntityId(); + const maxTroopCountPerArmy = configManager.getTroopConfig().maxTroopCount; + const [loading, setLoading] = useState(false); const [troopsGiven, setTroopsGiven] = useState>({ @@ -185,7 +186,7 @@ const TroopExchange = ({ const remainingTroops = useMemo(() => { const totalTroopsGiven = Object.values(troopsGiven).reduce((a, b) => a + b, 0n); const totalTroops = totalTroopsGiven + totalTroopsReceiver; - return BigInt(MAX_TROOPS_PER_ARMY) > totalTroops ? BigInt(MAX_TROOPS_PER_ARMY) - totalTroops : 0n; + return BigInt(maxTroopCountPerArmy) > totalTroops ? BigInt(maxTroopCountPerArmy) - totalTroops : 0n; }, [troopsGiven, totalTroopsReceiver]); const getMaxTroopCountForAttackingArmy = (amount: bigint, troopId: string) => { @@ -246,7 +247,7 @@ const TroopExchange = ({ {transferDirection === "from" && ( <>
- ⚠️ Maximum troops per attacking army is {formatNumber(MAX_TROOPS_PER_ARMY, 0)} + ⚠️ Maximum troops per attacking army is {formatNumber(maxTroopCountPerArmy, 0)}
Total troops in attacking army: {Number(totalTroopsReceiver)}
diff --git a/client/src/ui/components/military/ArmyChip.tsx b/client/src/ui/components/military/ArmyChip.tsx index e7ebcbbdd..7f047a46b 100644 --- a/client/src/ui/components/military/ArmyChip.tsx +++ b/client/src/ui/components/military/ArmyChip.tsx @@ -81,7 +81,7 @@ export const ArmyChip = ({ const updatedBattle = battleManager.getUpdatedBattle(nextBlockTimestamp!); const updatedArmy = battleManager.getUpdatedArmy(army, updatedBattle); return updatedArmy; - }, [nextBlockTimestamp]); + }, [nextBlockTimestamp, army]); const [location] = useLocation(); const isOnMap = useMemo(() => location.includes("map"), [location]); @@ -260,7 +260,7 @@ const ArmySelector = ({ armies, onSelect }: { armies: ArmyInfo[]; onSelect: (arm
handleArmyClick(army)} > diff --git a/client/src/ui/components/military/ArmyList.tsx b/client/src/ui/components/military/ArmyList.tsx index f38634ee6..ecc87d68a 100644 --- a/client/src/ui/components/military/ArmyList.tsx +++ b/client/src/ui/components/military/ArmyList.tsx @@ -1,4 +1,5 @@ import { TileManager } from "@/dojo/modelManager/TileManager"; +import { configManager } from "@/dojo/setup"; import { useDojo } from "@/hooks/context/DojoContext"; import { useArmiesByEntityOwner } from "@/hooks/helpers/useArmies"; import { type PlayerStructure } from "@/hooks/helpers/useEntities"; @@ -8,7 +9,7 @@ import { QuestId } from "@/ui/components/quest/questDetails"; import Button from "@/ui/elements/Button"; import { Headline } from "@/ui/elements/Headline"; import { HintModalButton } from "@/ui/elements/HintModalButton"; -import { BuildingType, EternumGlobalConfig, StructureType } from "@bibliothecadao/eternum"; +import { BuildingType, StructureType } from "@bibliothecadao/eternum"; import clsx from "clsx"; import { useMemo, useState } from "react"; import { HintSection } from "../hints/HintModal"; @@ -42,20 +43,22 @@ export const EntityArmyList = ({ structure }: { structure: PlayerStructure }) => }, } = useDojo(); + const troopConfig = configManager.getTroopConfig(); + const [loading, setLoading] = useState(Loading.None); const maxAmountOfAttackingArmies = useMemo(() => { const maxWithBuildings = - EternumGlobalConfig.troop.baseArmyNumberForStructure + + troopConfig.baseArmyNumberForStructure + existingBuildings.filter( (building) => building.category === BuildingType[BuildingType.ArcheryRange] || building.category === BuildingType[BuildingType.Barracks] || building.category === BuildingType[BuildingType.Stable], ).length * - EternumGlobalConfig.troop.armyExtraPerMilitaryBuilding; + troopConfig.armyExtraPerMilitaryBuilding; // remove 1 to force to create defensive army first - const hardMax = EternumGlobalConfig.troop.maxArmiesPerStructure - 1; + const hardMax = troopConfig.maxArmiesPerStructure - 1; return Math.min(maxWithBuildings, hardMax); }, [existingBuildings]); @@ -95,7 +98,7 @@ export const EntityArmyList = ({ structure }: { structure: PlayerStructure }) =>
Build military buildings to increase your current max number of attacking armies. Realms can support up to{" "} - {EternumGlobalConfig.troop.maxArmiesPerStructure - 1} attacking armies. + {troopConfig.maxArmiesPerStructure - 1} attacking armies.
{ return ( - Math.max(0, MAX_TROOPS_PER_ARMY - Object.values(troopCounts).reduce((a, b) => a + b, 0)) - + Math.max(0, maxTroopCountPerArmy - Object.values(troopCounts).reduce((a, b) => a + b, 0)) - Number(army?.quantity.value) ); }, [troopCounts]); const getMaxTroopCount = useCallback( (balance: number, troopName: number) => { - const balanceFloor = Math.floor(balance / EternumGlobalConfig.resources.resourcePrecision); + const balanceFloor = Math.floor(divideByPrecision(balance)); if (!balance) return 0; - const maxFromBalance = Math.min(balanceFloor, U32_MAX / EternumGlobalConfig.resources.resourcePrecision); + const maxFromBalance = Math.min(balanceFloor, divideByPrecision(U32_MAX)); if (isDefendingArmy) { return maxFromBalance; @@ -133,9 +141,9 @@ export const ArmyManagementCard = ({ owner_entity, army, setSelectedEntity }: Ar army_id: army?.entity_id || 0n, payer_id: owner_entity, troops: { - knight_count: troopCounts[ResourcesIds.Knight] * EternumGlobalConfig.resources.resourcePrecision || 0, - paladin_count: troopCounts[ResourcesIds.Paladin] * EternumGlobalConfig.resources.resourcePrecision || 0, - crossbowman_count: troopCounts[ResourcesIds.Crossbowman] * EternumGlobalConfig.resources.resourcePrecision || 0, + knight_count: multiplyByPrecision(troopCounts[ResourcesIds.Knight]), + paladin_count: multiplyByPrecision(troopCounts[ResourcesIds.Paladin]), + crossbowman_count: multiplyByPrecision(troopCounts[ResourcesIds.Crossbowman]), }, }).finally(() => setIsLoading(false)); @@ -297,7 +305,7 @@ export const ArmyManagementCard = ({ owner_entity, army, setSelectedEntity }: Ar {!isDefendingArmy && (
- ⚠️ Maximum troops per attacking army is {formatNumber(MAX_TROOPS_PER_ARMY, 0)} + ⚠️ Maximum troops per attacking army is {formatNumber(maxTroopCountPerArmy, 0)}
)} @@ -429,7 +437,7 @@ const TravelToLocation = ({ }; return ( -
+
Status:
diff --git a/client/src/ui/components/quest/QuestInfo.tsx b/client/src/ui/components/quest/QuestInfo.tsx index 6f6b84c19..c56504051 100644 --- a/client/src/ui/components/quest/QuestInfo.tsx +++ b/client/src/ui/components/quest/QuestInfo.tsx @@ -1,10 +1,10 @@ import { useDojo } from "@/hooks/context/DojoContext"; +import { useQuery } from "@/hooks/helpers/useQuery"; import { Prize, Quest, QuestStatus } from "@/hooks/helpers/useQuests"; import { useRealm } from "@/hooks/helpers/useRealm"; import { useQuestStore } from "@/hooks/store/useQuestStore"; import Button from "@/ui/elements/Button"; import { ResourceCost } from "@/ui/elements/ResourceCost"; -import { multiplyByPrecision } from "@/ui/utils/utils"; import { ID } from "@bibliothecadao/eternum"; import clsx from "clsx"; import { Check, ShieldQuestion } from "lucide-react"; @@ -13,30 +13,23 @@ import { useState } from "react"; export const QuestInfo = ({ quest, entityId }: { quest: Quest; entityId: ID }) => { const { setup: { - systemCalls: { mint_resources_and_claim_quest }, + systemCalls: { claim_quest }, }, account: { account }, } = useDojo(); + const { isMapView } = useQuery(); + const [isLoading, setIsLoading] = useState(false); const setSelectedQuest = useQuestStore((state) => state.setSelectedQuest); - const { getQuestResources } = useRealm(); - const handleAllClaims = async () => { - const questResources = getQuestResources(); - const resourcesToMint = quest.prizes.flatMap((prize) => { - const resources = questResources[prize.id]; - return resources.flatMap((resource) => [resource.resource as number, multiplyByPrecision(resource.amount)]); - }); - setIsLoading(true); try { - await mint_resources_and_claim_quest({ + await claim_quest({ signer: account, - config_ids: quest.prizes.map((prize) => BigInt(prize.id)), + quest_ids: quest.prizes.map((prize) => BigInt(prize.id)), receiver_id: entityId, - resources: resourcesToMint, }); } catch (error) { console.error(`Failed to claim resources for quest ${quest.name}:`, error); @@ -65,6 +58,20 @@ export const QuestInfo = ({ quest, entityId }: { quest: Quest; entityId: ID }) =

Steps
+ {quest.view && ( +
+ Navigate to the{" "} + + {quest.view} + {" "} + view + {(isMapView && quest.view === "WORLD") || (!isMapView && quest.view === "REALM") ? ( + + ) : ( + + )} +
+ )} {quest.steps.map((step: any, index: number) => (
{step}
diff --git a/client/src/ui/components/quest/QuestList.tsx b/client/src/ui/components/quest/QuestList.tsx index 2e8fad15a..043493cc1 100644 --- a/client/src/ui/components/quest/QuestList.tsx +++ b/client/src/ui/components/quest/QuestList.tsx @@ -1,11 +1,10 @@ import { useDojo } from "@/hooks/context/DojoContext"; -import { Prize, Quest, QuestStatus } from "@/hooks/helpers/useQuests"; +import { Quest, QuestStatus } from "@/hooks/helpers/useQuests"; import { useRealm } from "@/hooks/helpers/useRealm"; import { useQuestStore } from "@/hooks/store/useQuestStore"; import Button from "@/ui/elements/Button"; import { Headline } from "@/ui/elements/Headline"; import { ResourceCost } from "@/ui/elements/ResourceCost"; -import { multiplyByPrecision } from "@/ui/utils/utils"; import { ID } from "@bibliothecadao/eternum"; import { useEffect, useMemo, useState } from "react"; import { areAllQuestsClaimed, groupQuestsByDepth } from "./utils"; @@ -124,33 +123,21 @@ const SkipTutorial = ({ }) => { const { setup: { - systemCalls: { mint_resources_and_claim_quest }, + systemCalls: { claim_quest }, }, account: { account }, } = useDojo(); const [isLoading, setIsLoading] = useState(false); - const { getQuestResources } = useRealm(); - - const questResources = getQuestResources(); - - const resourcesToMint = - unclaimedQuests?.flatMap((quest: Quest) => - quest.prizes.flatMap((prize: Prize) => { - const resources = questResources[prize.id]; - return resources.flatMap((resource) => [resource.resource as number, multiplyByPrecision(resource.amount)]); - }), - ) ?? []; const claimAllQuests = async () => { - if (resourcesToMint && unclaimedQuests) { + if (unclaimedQuests) { setIsLoading(true); try { - await mint_resources_and_claim_quest({ + await claim_quest({ signer: account, - config_ids: unclaimedQuests.flatMap((quest) => quest.prizes.map((prize) => BigInt(prize.id))), + quest_ids: unclaimedQuests.flatMap((quest) => quest.prizes.map((prize) => BigInt(prize.id))), receiver_id: entityId, - resources: resourcesToMint, }); } catch (error) { console.error(`Failed to claim resources for quests:`, error); diff --git a/client/src/ui/components/quest/questDetails.tsx b/client/src/ui/components/quest/questDetails.tsx index bcef8200c..bd2a9433e 100644 --- a/client/src/ui/components/quest/questDetails.tsx +++ b/client/src/ui/components/quest/questDetails.tsx @@ -1,22 +1,15 @@ +import { configManager } from "@/dojo/setup"; import { Prize } from "@/hooks/helpers/useQuests"; import { BUILDING_IMAGES_PATH, BuildingThumbs } from "@/ui/config"; import CircleButton from "@/ui/elements/CircleButton"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; -import { multiplyByPrecision } from "@/ui/utils/utils"; -import { - BASE_POPULATION_CAPACITY, - BuildingType, - CapacityConfigCategory, - EternumGlobalConfig, - QuestType, - ResourcesIds, - TROOPS_FOOD_CONSUMPTION, -} from "@bibliothecadao/eternum"; +import { BuildingType, CapacityConfigCategory, QuestType, ResourcesIds } from "@bibliothecadao/eternum"; import clsx from "clsx"; import { ResourceWeight } from "../resources/ResourceWeight"; interface StaticQuestInfo { name: string; + view: string; description: string | React.ReactNode; steps: (string | React.ReactNode)[]; prizes: Prize[]; @@ -41,7 +34,7 @@ const navigationStep = (imgPath: string) => { }; export enum QuestId { - Settle, + Settle = 1, BuildFood, BuildResource, PauseProduction, @@ -62,6 +55,7 @@ export const questDetails = new Map([ QuestId.Settle, { name: "Settle", + view: "", description: (

A gift of food from the gods.

@@ -80,6 +74,7 @@ export const questDetails = new Map([ QuestId.BuildFood, { name: "Build a Farm or a Fishing Village", + view: "REALM", description: "Wheat and Fish are the lifeblood of your people. Go to the construction menu and build a Farm or a Fishing Village", steps: [ @@ -130,6 +125,7 @@ export const questDetails = new Map([ QuestId.BuildResource, { name: "Build a Resource Facility", + view: "REALM", description: (
Eternum thrives on resources. Construct resource facilities to harvest them efficiently.
@@ -144,7 +140,7 @@ export const questDetails = new Map([ "2. Select one of the Resource Buildings in the 'Resources' tab", "3. Left click on a hex to build it, or right click to cancel", ], - prizes: [{ id: QuestType.Trade, title: "Donkeys and Lords" }], + prizes: [{ id: QuestType.Trade, title: "Donkeys" }], depth: 2, }, ], @@ -152,6 +148,7 @@ export const questDetails = new Map([ QuestId.PauseProduction, { name: "Pause Production", + view: "REALM", description: "Resource facilities will produce resources automatically. Pause production to stop its consumption.", steps: ["1. Left mouse click on the building's model", "2. Pause its production"], @@ -163,6 +160,7 @@ export const questDetails = new Map([ QuestId.CreateTrade, { name: "Create a Trade", + view: "", description: "Trading is the lifeblood of Eternum. Create a trade to start your economy", steps: [navigationStep(BuildingThumbs.scale), "2. Create a 'Buy' or 'Sell' order."], prizes: [{ id: QuestType.Military, title: "Claim Starting Army" }], @@ -173,6 +171,7 @@ export const questDetails = new Map([ QuestId.CreateDefenseArmy, { name: "Create a Defensive Army", + view: "REALM", description: "Your realm is always at risk. Create a defensive army to protect it", steps: [ navigationStep(BuildingThumbs.military), @@ -187,6 +186,7 @@ export const questDetails = new Map([ QuestId.CreateAttackArmy, { name: "Create an attacking Army", + view: "REALM", description: "Conquest is fulfilling. Create an attacking army to conquer your enemies", steps: [ navigationStep(BuildingThumbs.military), @@ -201,6 +201,7 @@ export const questDetails = new Map([ QuestId.Travel, { name: "Move with your Army", + view: "WORLD", description: (

@@ -215,7 +216,7 @@ export const questDetails = new Map([ 🏃‍♂️ Costs{" "} - {EternumGlobalConfig.stamina.travelCost} + {configManager.getTravelStaminaCost()} {" "} stamina per hex @@ -252,15 +253,13 @@ export const questDetails = new Map([

- {multiplyByPrecision( - TROOPS_FOOD_CONSUMPTION[ResourcesIds.Crossbowman].travel_fish_burn_amount, - )} + {configManager.getTravelFoodCostConfig(ResourcesIds.Crossbowman).travelFishBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Knight].travel_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Knight).travelFishBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Paladin].travel_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Paladin).travelFishBurnAmount} @@ -268,15 +267,13 @@ export const questDetails = new Map([
- {multiplyByPrecision( - TROOPS_FOOD_CONSUMPTION[ResourcesIds.Crossbowman].travel_wheat_burn_amount, - )} + {configManager.getTravelFoodCostConfig(ResourcesIds.Crossbowman).travelWheatBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Knight].travel_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Knight).travelWheatBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Paladin].travel_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Paladin).travelWheatBurnAmount} @@ -295,7 +292,7 @@ export const questDetails = new Map([ 🌎 Costs{" "} - {EternumGlobalConfig.stamina.exploreCost} + {configManager.getExploreStaminaCost()} {" "} stamina per hex @@ -332,15 +329,13 @@ export const questDetails = new Map([
- {multiplyByPrecision( - TROOPS_FOOD_CONSUMPTION[ResourcesIds.Crossbowman].explore_fish_burn_amount, - )} + {configManager.getTravelFoodCostConfig(ResourcesIds.Crossbowman).exploreFishBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Knight].explore_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Knight).exploreFishBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Paladin].explore_fish_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Paladin).exploreFishBurnAmount} @@ -348,15 +343,13 @@ export const questDetails = new Map([
- {multiplyByPrecision( - TROOPS_FOOD_CONSUMPTION[ResourcesIds.Crossbowman].explore_wheat_burn_amount, - )} + {configManager.getTravelFoodCostConfig(ResourcesIds.Crossbowman).exploreWheatBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Knight].explore_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Knight).exploreWheatBurnAmount} - {multiplyByPrecision(TROOPS_FOOD_CONSUMPTION[ResourcesIds.Paladin].explore_wheat_burn_amount)} + {configManager.getTravelFoodCostConfig(ResourcesIds.Paladin).exploreWheatBurnAmount} @@ -389,8 +382,11 @@ export const questDetails = new Map([ QuestId.BuildWorkersHut, { name: "Build a workers hut", - description: `Each building takes up population in your realm. You realm starts with a population of ${BASE_POPULATION_CAPACITY}. - Build worker huts to extend your population capacity by ${EternumGlobalConfig.populationCapacity.workerHuts}.`, + view: "REALM", + description: `Each building takes up population in your realm. You realm starts with a population of ${configManager.getBasePopulationCapacity()}. + Build worker huts to extend your population capacity by ${ + configManager.getBuildingPopConfig(BuildingType.WorkersHut).capacity + }.`, steps: [ navigationStep(BuildingThumbs.construction), "2. Select the worker hut building", @@ -404,16 +400,14 @@ export const questDetails = new Map([ QuestId.Market, { name: "Build a market", + view: "REALM", description: (
Build a market to produce donkeys. Donkeys are a resource used to transport goods.
{" "}
-
- {" "} - Donkeys can transport{" "} - {Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Donkey]) / 1000} kg{" "} -
+
Donkeys can transport
+ {configManager.getCapacityConfig(CapacityConfigCategory.Donkey) / 1000} kg{" "}
@@ -431,6 +425,7 @@ export const questDetails = new Map([ QuestId.Pillage, { name: "Pillage a structure", + view: "WORLD", description: "Pillage a realm, hyperstructure or fragment mine. To pillage a structure, travel with your army to your target first, then pillage it.", steps: [ @@ -447,6 +442,7 @@ export const questDetails = new Map([ QuestId.Mine, { name: "Claim a Fragment mine", + view: "WORLD", description: "Explore the world, find Fragment mines and battle bandits for its ownership", steps: [ "1. Go to world view", @@ -462,12 +458,12 @@ export const questDetails = new Map([ QuestId.Contribution, { name: "Contribute to a hyperstructure", + view: "", description: "Contribute to a Hyperstructure", steps: [ - "1. Go to world view", - "2. Right click on your army", - "3. Travel with your army to a Hyperstructure", - "4. Contribute to the Hyperstructure", + navigationStep(BuildingThumbs.worldStructures), + "2. Select a Hyperstructure", + "4. Contribute to the Hyperstructure's construction", ], prizes: [{ id: QuestType.Contribution, title: "Contribution" }], depth: 6, @@ -477,6 +473,7 @@ export const questDetails = new Map([ QuestId.Hyperstructure, { name: "Build a hyperstructure", + view: "WORLD", description: "Build a Hyperstructure", steps: [ navigationStep(BuildingThumbs.construction), diff --git a/client/src/ui/components/resources/EntityResourceTable.tsx b/client/src/ui/components/resources/EntityResourceTable.tsx index 3a1083a30..e5687efa0 100644 --- a/client/src/ui/components/resources/EntityResourceTable.tsx +++ b/client/src/ui/components/resources/EntityResourceTable.tsx @@ -1,6 +1,7 @@ +import { configManager } from "@/dojo/setup"; import { useDojo } from "@/hooks/context/DojoContext"; -import { getEntityIdFromKeys, gramToKg } from "@/ui/utils/utils"; -import { BuildingType, CapacityConfigCategory, EternumGlobalConfig, ID, RESOURCE_TIERS } from "@bibliothecadao/eternum"; +import { getEntityIdFromKeys, gramToKg, multiplyByPrecision } from "@/ui/utils/utils"; +import { BuildingType, CapacityConfigCategory, ID, RESOURCE_TIERS } from "@bibliothecadao/eternum"; import { useComponentValue } from "@dojoengine/react"; import { useMemo } from "react"; import { ResourceChip } from "./ResourceChip"; @@ -15,11 +16,8 @@ export const EntityResourceTable = ({ entityId }: { entityId: ID | undefined }) )?.value || 0; const maxStorehouseCapacityKg = useMemo(() => { - return ( - (quantity * gramToKg(Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Storehouse])) + - gramToKg(Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Storehouse]))) * - EternumGlobalConfig.resources.resourcePrecision - ); + const storehouseCapacityKg = gramToKg(configManager.getCapacityConfig(CapacityConfigCategory.Storehouse)); + return multiplyByPrecision(quantity * storehouseCapacityKg + storehouseCapacityKg); }, [quantity, entityId]); const resourceElements = useMemo(() => { diff --git a/client/src/ui/components/resources/RealmResourcesIO.tsx b/client/src/ui/components/resources/RealmResourcesIO.tsx index 528f67c3b..001684ad8 100644 --- a/client/src/ui/components/resources/RealmResourcesIO.tsx +++ b/client/src/ui/components/resources/RealmResourcesIO.tsx @@ -1,7 +1,8 @@ +import { configManager } from "@/dojo/setup"; import { useGetRealm } from "@/hooks/helpers/useRealm"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; import { unpackResources } from "@/ui/utils/packedData"; -import { ID, RESOURCE_INPUTS_SCALED, ResourcesIds } from "@bibliothecadao/eternum"; +import { ID, ResourcesIds } from "@bibliothecadao/eternum"; export const RealmResourcesIO = ({ realmEntityId, @@ -16,12 +17,13 @@ export const RealmResourcesIO = ({ }) => { const { realm } = useGetRealm(realmEntityId); - const resourcesProduced = realm ? unpackResources(realm.resourceTypesPacked, realm.resourceTypesCount) : []; + const resourcesProduced = realm ? unpackResources(realm.resourceTypesPacked) : []; + const resourcesInputs = configManager.resourceInputs; const resourcesConsumed = [ ...new Set( resourcesProduced.flatMap((resourceId) => { - return RESOURCE_INPUTS_SCALED[resourceId] + return resourcesInputs[resourceId] .filter((input) => input.resource !== ResourcesIds["Wheat"] && input.resource !== ResourcesIds["Fish"]) .map((input) => input.resource); }), diff --git a/client/src/ui/components/resources/ResourceChip.tsx b/client/src/ui/components/resources/ResourceChip.tsx index 534534325..aef11f7b0 100644 --- a/client/src/ui/components/resources/ResourceChip.tsx +++ b/client/src/ui/components/resources/ResourceChip.tsx @@ -1,5 +1,6 @@ -import { findResourceById, getIconResourceId, ID, ResourcesIds, WEIGHTS_GRAM } from "@bibliothecadao/eternum"; +import { findResourceById, getIconResourceId, ID, TickIds } from "@bibliothecadao/eternum"; +import { configManager } from "@/dojo/setup"; import { useProductionManager } from "@/hooks/helpers/useResources"; import useUIStore from "@/hooks/store/useUIStore"; import { useEffect, useMemo, useState } from "react"; @@ -28,16 +29,14 @@ export const ResourceChip = ({ const [displayedNetRate, setDisplayedNetRate] = useState(""); const [isTransitioning, setIsTransitioning] = useState(false); + const [balance, setBalance] = useState(productionManager.balance(currentDefaultTick)); + const production = useMemo(() => { return productionManager.getProduction(); }, [productionManager]); - const balance = useMemo(() => { - return productionManager.balance(currentDefaultTick); - }, [productionManager, production, currentDefaultTick, maxStorehouseCapacityKg]); - const maxAmountStorable = useMemo(() => { - return maxStorehouseCapacityKg / gramToKg(WEIGHTS_GRAM[resourceId as ResourcesIds] || 1000); + return maxStorehouseCapacityKg / gramToKg(configManager.getResourceWeight(resourceId) || 1000); }, [maxStorehouseCapacityKg, resourceId]); const timeUntilValueReached = useMemo(() => { @@ -45,7 +44,7 @@ export const ResourceChip = ({ }, [productionManager, production, currentDefaultTick]); const netRate = useMemo(() => { - let netRate = productionManager.netRate(currentDefaultTick); + let netRate = productionManager.netRate(); if (netRate[1] < 0) { // net rate is negative if (Math.abs(netRate[1]) > productionManager.balance(currentDefaultTick)) { @@ -55,13 +54,54 @@ export const ResourceChip = ({ return netRate[1]; }, [productionManager, production]); + useEffect(() => { + const tickTime = configManager.getTick(TickIds.Default) * 1000; + + let realTick = currentDefaultTick; + + const resource = productionManager.getResource(); + const [sign, rate] = productionManager.netRate(); + const productionDuration = productionManager.productionDuration(realTick); + const depletionDuration = productionManager.depletionDuration(realTick); + + const newBalance = productionManager.balanceFromComponents( + resourceId, + rate, + sign, + resource?.balance, + productionDuration, + depletionDuration, + ); + + setBalance(newBalance); + + if (Math.abs(netRate) > 0) { + const interval = setInterval(() => { + realTick += 1; + const localResource = productionManager.getResource(); + const localProductionDuration = productionManager.productionDuration(realTick); + const localDepletionDuration = productionManager.depletionDuration(realTick); + + const newBalance = productionManager.balanceFromComponents( + resourceId, + rate, + netRate > 0, + localResource?.balance, + localProductionDuration, + localDepletionDuration, + ); + + setBalance(newBalance); + }, tickTime); + return () => clearInterval(interval); + } + }, [currentDefaultTick, setBalance, productionManager, netRate, resourceId]); + const isConsumingInputsWithoutOutput = useMemo(() => { if (!production?.production_rate) return false; return productionManager.isConsumingInputsWithoutOutput(currentDefaultTick); }, [productionManager, production, currentDefaultTick, entityId]); - const [displayBalance, setDisplayBalance] = useState(balance); - const icon = useMemo( () => ( { - setDisplayBalance(balance); + const reachedMaxCap = useMemo(() => { + return maxAmountStorable === balance && Math.abs(netRate) > 0; + }, [maxAmountStorable, balance, netRate]); - const interval = setInterval(() => { - setDisplayBalance((prevDisplayBalance) => { - if (Math.abs(netRate) > 0 && !isConsumingInputsWithoutOutput) { - return Math.min(maxAmountStorable, Math.max(0, prevDisplayBalance + netRate)); - } - return Math.min(maxAmountStorable, prevDisplayBalance); - }); - }, 1000); - return () => clearInterval(interval); - }, [balance, netRate, entityId, maxAmountStorable]); + const timeUntilFinishTick = useMemo(() => { + return productionManager.timeUntilFinishTick(currentDefaultTick); + }, [productionManager, currentDefaultTick]); + + const isProducingOrConsuming = useMemo(() => { + if (netRate > 0 && timeUntilFinishTick <= 0) return false; + return Math.abs(netRate) > 0 && !reachedMaxCap && !isConsumingInputsWithoutOutput && balance > 0; + }, [netRate, reachedMaxCap, isConsumingInputsWithoutOutput, balance, timeUntilFinishTick]); useEffect(() => { const interval = setInterval(() => { @@ -110,8 +149,6 @@ export const ResourceChip = ({ } }, [netRate, showPerHour]); - const reachedMaxCap = maxAmountStorable === displayBalance && Math.abs(netRate) > 0; - return (
{icon}
-
- {currencyFormat(displayBalance ? Number(displayBalance) : 0, 0)} -
+
{currencyFormat(balance ? Number(balance) : 0, 0)}
{timeUntilValueReached !== 0 @@ -137,7 +172,7 @@ export const ResourceChip = ({ : ""}
- {netRate && !reachedMaxCap && !isConsumingInputsWithoutOutput && displayBalance > 0 ? ( + {isProducingOrConsuming ? (
{ return (
-
Lords: {`${WEIGHTS_GRAM[ResourcesIds.Lords] / GRAMS_PER_KG} kg/unit`}
-
Food: {`${WEIGHTS_GRAM[ResourcesIds.Wheat] / GRAMS_PER_KG} kg/unit`}
-
Resource: {`${WEIGHTS_GRAM[ResourcesIds.Wood] / GRAMS_PER_KG} kg/unit`}
+
+ Lords: {`${configManager.getResourceWeight(ResourcesIds.Lords) / GRAMS_PER_KG} kg/unit`} +
+
Food: {`${configManager.getResourceWeight(ResourcesIds.Wheat) / GRAMS_PER_KG} kg/unit`}
+
+ Resource: {`${configManager.getResourceWeight(ResourcesIds.Wood) / GRAMS_PER_KG} kg/unit`} +
); }; diff --git a/client/src/ui/components/structures/construction/StructureCard.tsx b/client/src/ui/components/structures/construction/StructureCard.tsx index cfefcd6c6..3d00e0da7 100644 --- a/client/src/ui/components/structures/construction/StructureCard.tsx +++ b/client/src/ui/components/structures/construction/StructureCard.tsx @@ -28,7 +28,7 @@ export const StructureCard = ({
{!canBuild && ( -
+
@@ -63,7 +63,7 @@ export const StructureCard = ({ />
-
+
diff --git a/client/src/ui/components/structures/construction/StructureConstructionMenu.tsx b/client/src/ui/components/structures/construction/StructureConstructionMenu.tsx index a4f20bfbc..04fac1184 100644 --- a/client/src/ui/components/structures/construction/StructureConstructionMenu.tsx +++ b/client/src/ui/components/structures/construction/StructureConstructionMenu.tsx @@ -1,16 +1,12 @@ +import { configManager } from "@/dojo/setup"; import { getResourceBalance } from "@/hooks/helpers/useResources"; import { useQuestStore } from "@/hooks/store/useQuestStore"; import useUIStore from "@/hooks/store/useUIStore"; import { QuestId } from "@/ui/components/quest/questDetails"; import { Headline } from "@/ui/elements/Headline"; import { ResourceCost } from "@/ui/elements/ResourceCost"; -import { - EternumGlobalConfig, - HYPERSTRUCTURE_POINTS_PER_CYCLE, - ID, - STRUCTURE_COSTS_SCALED, - StructureType, -} from "@bibliothecadao/eternum"; +import { multiplyByPrecision } from "@/ui/utils/utils"; +import { ID, StructureType } from "@bibliothecadao/eternum"; import clsx from "clsx"; import React from "react"; import { StructureCard } from "./StructureCard"; @@ -42,14 +38,14 @@ export const StructureConstructionMenu = ({ className, entityId }: { className?: Object.keys(cost).every((resourceId) => { const resourceCost = cost[Number(resourceId)]; const balance = getBalance(entityId, resourceCost.resource); - return balance.balance >= resourceCost.amount * EternumGlobalConfig.resources.resourcePrecision; + return balance.balance >= multiplyByPrecision(resourceCost.amount); }); return (
{buildingTypes.map((structureType, index) => { const building = StructureType[structureType as keyof typeof StructureType]; - const cost = STRUCTURE_COSTS_SCALED[building]; + const cost = configManager.structureCosts[building]; const hasBalance = checkBalance(cost); const isHyperstructure = building === StructureType["Hyperstructure"]; @@ -89,9 +85,12 @@ const StructureInfo = ({ entityId: ID | undefined; extraButtons?: React.ReactNode[]; }) => { - const cost = STRUCTURE_COSTS_SCALED[structureId]; + const cost = configManager.structureCosts[structureId]; - const perTick = structureId == StructureType.Hyperstructure ? `+${HYPERSTRUCTURE_POINTS_PER_CYCLE} points` : ""; + const perTick = + structureId == StructureType.Hyperstructure + ? `+${configManager.getHyperstructureConfig().pointsPerCycle} points` + : ""; const { getBalance } = getResourceBalance(); diff --git a/client/src/ui/components/trading/MarketModal.tsx b/client/src/ui/components/trading/MarketModal.tsx index b73a825f8..98762c1f6 100644 --- a/client/src/ui/components/trading/MarketModal.tsx +++ b/client/src/ui/components/trading/MarketModal.tsx @@ -1,3 +1,8 @@ +import { ReactComponent as Coins } from "@/assets/icons/Coins.svg"; +import { ReactComponent as Crown } from "@/assets/icons/Crown.svg"; +import { ReactComponent as Scroll } from "@/assets/icons/Scroll.svg"; +import { ReactComponent as Sparkles } from "@/assets/icons/Sparkles.svg"; +import { ReactComponent as Swap } from "@/assets/icons/Swap.svg"; import { useDojo } from "@/hooks/context/DojoContext"; import { useGetBanks } from "@/hooks/helpers/useBanks"; import { useEntities } from "@/hooks/helpers/useEntities"; @@ -72,8 +77,9 @@ export const MarketModal = () => { { key: "all", label: ( -
-
Order Book
+
+ +
Order Book
), component: ( @@ -90,8 +96,9 @@ export const MarketModal = () => { { key: "all", label: ( -
-
AMM
+
+ +
AMM
), component: bank && ( @@ -107,8 +114,9 @@ export const MarketModal = () => { { key: "all", label: ( -
-
History
+
+ +
History
), component: ( @@ -120,8 +128,9 @@ export const MarketModal = () => { { key: "all", label: ( -
-
Transfer
+
+ +
Transfer
), component: ( @@ -133,8 +142,9 @@ export const MarketModal = () => { { key: "resourceProd", label: ( -
-
Realm Production
+
+ +
Realm Production
), component: ( @@ -149,10 +159,10 @@ export const MarketModal = () => { return ( -
-
-
-
+
+
+
+
-
+
{currencyFormat(Number(lordsBalance), 0)}{" "}
-
-

The Lords Market

-
-
- { - toggleModal(null); - toggleModal(); - }} - size={"sm"} - image={BuildingThumbs.question} - /> -
-
- -
}> {
+
+
+

The Lords Market

+
+
+ { + toggleModal(null); + toggleModal(); + }} + size={"sm"} + image={BuildingThumbs.question} + /> +
+
setSelectedTab(index)} className="h-full" > -
- - {tabs.map((tab, index) => ( - {tab.label} - ))} - -
+ + {tabs.map((tab, index) => ( + {tab.label} + ))} + {tabs.map((tab, index) => ( diff --git a/client/src/ui/components/trading/MarketOrderPanel.tsx b/client/src/ui/components/trading/MarketOrderPanel.tsx index 6c1379e2e..ac7ba8e5f 100644 --- a/client/src/ui/components/trading/MarketOrderPanel.tsx +++ b/client/src/ui/components/trading/MarketOrderPanel.tsx @@ -1,4 +1,5 @@ import { ProductionManager } from "@/dojo/modelManager/ProductionManager"; +import { configManager } from "@/dojo/setup"; import { useDojo } from "@/hooks/context/DojoContext"; import { useRealm } from "@/hooks/helpers/useRealm"; import { useProductionManager } from "@/hooks/helpers/useResources"; @@ -11,7 +12,7 @@ import { ResourceIcon } from "@/ui/elements/ResourceIcon"; import { currencyFormat, divideByPrecision, getTotalResourceWeight, multiplyByPrecision } from "@/ui/utils/utils"; import { CapacityConfigCategory, - EternumGlobalConfig, + DONKEY_ENTITY_TYPE, ONE_MONTH, ResourcesIds, findResourceById, @@ -56,7 +57,7 @@ export const MarketResource = ({ onClick={() => { onClick(resource.id); }} - className={`w-full border border-gold/5 h-8 p-1 cursor-pointer grid grid-cols-5 gap-1 hover:bg-gold/10 hover: group ${ + className={`w-full border-gold/5 rounded-xl h-8 p-1 cursor-pointer grid grid-cols-5 gap-1 hover:bg-gold/10 hover: group ${ active ? "bg-gold/10" : "" }`} > @@ -160,20 +161,25 @@ const MarketOrders = ({
{/* Market Price */}
- -
{lowestPrice.toFixed(2)}
+
+
{findResourceById(resourceId)?.trait || ""}
+
+ +
{lowestPrice.toFixed(2)}
+
+
-
+
{offers.length} {isBuy ? "bid" : "ask"}
-
+
))} {isResourcesLocked && ( -
+
Resources locked in battle
)} @@ -254,7 +260,7 @@ const OrderRow = ({ const [confirmOrderModal, setConfirmOrderModal] = useState(false); const travelTime = useMemo( - () => computeTravelTime(entityId, offer.makerId, EternumGlobalConfig.speed.donkey, true), + () => computeTravelTime(entityId, offer.makerId, configManager.getSpeedConfig(DONKEY_ENTITY_TYPE), true), [entityId, offer], ); @@ -296,20 +302,20 @@ const OrderRow = ({ ); const [inputValue, setInputValue] = useState(() => { return isBuy - ? (offer.makerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision) * resourceBalanceRatio - : (offer.takerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision) * lordsBalanceRatio; + ? divideByPrecision(offer.makerGets[0].amount) * resourceBalanceRatio + : divideByPrecision(offer.takerGets[0].amount) * lordsBalanceRatio; }); useEffect(() => { setInputValue( isBuy - ? (offer.makerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision) * resourceBalanceRatio - : (offer.takerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision) * lordsBalanceRatio, + ? divideByPrecision(offer.makerGets[0].amount) * resourceBalanceRatio + : divideByPrecision(offer.takerGets[0].amount) * lordsBalanceRatio, ); }, [resourceBalanceRatio, lordsBalanceRatio]); const calculatedResourceAmount = useMemo(() => { - return inputValue * EternumGlobalConfig.resources.resourcePrecision; + return multiplyByPrecision(inputValue); }, [inputValue, getsDisplay, getTotalLords]); const calculatedLords = useMemo(() => { @@ -327,9 +333,7 @@ const OrderRow = ({ }, [entityId, calculatedResourceAmount, calculatedLords]); const donkeysNeeded = useMemo(() => { - return Math.ceil( - divideByPrecision(orderWeight) / Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Donkey]), - ); + return calculateDonkeysNeeded(orderWeight); }, [orderWeight]); const donkeyProductionManager = useProductionManager(entityId, ResourcesIds.Donkey); @@ -370,18 +374,18 @@ const OrderRow = ({ return (
{isMakerResourcesLocked && ( -
+
Resources locked in battle
)}
- {" "} + {" "} {getsDisplay}
{travelTime && ( @@ -445,16 +449,12 @@ const OrderRow = ({ value={inputValue} className="w-full col-span-3" onChange={setInputValue} - max={ - (getsDisplayNumber / EternumGlobalConfig.resources.resourcePrecision) * - (isBuy ? resourceBalanceRatio : lordsBalanceRatio) - } + max={divideByPrecision(getsDisplayNumber) * (isBuy ? resourceBalanceRatio : lordsBalanceRatio)} /> {notification && !disabled ? (
{props.options.map((option) => ( diff --git a/client/src/ui/elements/NumberInput.tsx b/client/src/ui/elements/NumberInput.tsx index da6f4122b..a0a99c5ba 100644 --- a/client/src/ui/elements/NumberInput.tsx +++ b/client/src/ui/elements/NumberInput.tsx @@ -74,7 +74,7 @@ export const NumberInput = ({ if (allowDecimals) { const match = inputValue.match(/[+-]?([0-9,]+([.][0-9]*)?|[.][0-9]+)/); if (match) { - const parsedNumber = parseNumber(match[0]); + const parsedNumber = Math.min(parseNumber(match[0]), max); setDisplayValue(match[0]); onChange(parsedNumber); } else { @@ -85,8 +85,9 @@ export const NumberInput = ({ const match = inputValue.match(/[+-]?([0-9,]+)/); if (match) { const parsedValue = parseNumber(match[0]); - setDisplayValue(formatNumber(Math.floor(parsedValue))); - onChange(Math.floor(parsedValue)); + const maxValue = Math.min(Math.max(Math.floor(parsedValue), min), max); + setDisplayValue(match[0]); + onChange(maxValue); } else { setDisplayValue(formatNumber(min)); onChange(min); diff --git a/client/src/ui/elements/ResourceIcon.tsx b/client/src/ui/elements/ResourceIcon.tsx index dce484096..6f94037a8 100644 --- a/client/src/ui/elements/ResourceIcon.tsx +++ b/client/src/ui/elements/ResourceIcon.tsx @@ -97,11 +97,11 @@ export const ResourceIcon = ({ isLabor = false, withTooltip = true, tooltipText, )} {withTooltip && ( -
+
{tooltipText || Components[props.resource.replace(" ", "").replace("'", "")]?.name} -
+
)}
diff --git a/client/src/ui/elements/SecondaryPopup.tsx b/client/src/ui/elements/SecondaryPopup.tsx index 5cb3381fe..ce9e9bc2b 100644 --- a/client/src/ui/elements/SecondaryPopup.tsx +++ b/client/src/ui/elements/SecondaryPopup.tsx @@ -112,7 +112,7 @@ SecondaryPopup.Head = ({ }) => (
{ @@ -177,7 +177,7 @@ SecondaryPopup.Body = ({ width ? "" : "min-w-[438px]", height ? "" : "min-h-[438px]", withWrapper ? "p-3" : "", - `relative z-10 flex flex-col bg-black/90 border-gradient border rounded-b overflow-auto bg-hex-bg bg-repeat`, + `relative z-10 flex flex-col bg-brown/90 border-gradient border rounded-b overflow-auto bg-hex-bg bg-repeat`, )} style={{ width: width ? width : "", diff --git a/client/src/ui/elements/Select.tsx b/client/src/ui/elements/Select.tsx index 45979229f..544d3ce1f 100644 --- a/client/src/ui/elements/Select.tsx +++ b/client/src/ui/elements/Select.tsx @@ -66,7 +66,7 @@ const SelectContent = React.forwardRef< (staminaAmount / maxStamina) * 100, [staminaAmount, maxStamina]); const staminaColor = useMemo( - () => (staminaAmount < EternumGlobalConfig.stamina.travelCost ? "bg-red" : "bg-yellow"), + () => (staminaAmount < configManager.getTravelStaminaCost() ? "bg-red" : "bg-yellow"), [staminaAmount], ); diff --git a/client/src/ui/elements/StaminaResourceCost.tsx b/client/src/ui/elements/StaminaResourceCost.tsx index 10475d749..6d81fadeb 100644 --- a/client/src/ui/elements/StaminaResourceCost.tsx +++ b/client/src/ui/elements/StaminaResourceCost.tsx @@ -1,6 +1,7 @@ +import { configManager } from "@/dojo/setup"; import { useStaminaManager } from "@/hooks/helpers/useStamina"; import useUIStore from "@/hooks/store/useUIStore"; -import { EternumGlobalConfig, ID } from "@bibliothecadao/eternum"; +import { ID } from "@bibliothecadao/eternum"; import clsx from "clsx"; import { useMemo } from "react"; @@ -22,7 +23,7 @@ export const StaminaResourceCost = ({ const destinationHex = useMemo(() => { if (!stamina) return; const costs = - travelLength * (isExplored ? -EternumGlobalConfig.stamina.travelCost : -EternumGlobalConfig.stamina.exploreCost); + travelLength * (isExplored ? -configManager.getTravelStaminaCost() : -configManager.getExploreStaminaCost()); const balanceColor = stamina !== undefined && stamina.amount < costs ? "text-red/90" : "text-green/90"; return { isExplored, costs, balanceColor, balance: stamina.amount }; }, [stamina, travelLength]); diff --git a/client/src/ui/elements/Tooltip.tsx b/client/src/ui/elements/Tooltip.tsx index 3b3858717..772589c0d 100644 --- a/client/src/ui/elements/Tooltip.tsx +++ b/client/src/ui/elements/Tooltip.tsx @@ -34,7 +34,7 @@ export const Tooltip = ({ className }: TooltipProps) => {
+ import("../components/worldmap/armies/SelectedArmy").then((module) => ({ default: module.SelectedArmy })), +); + const ActionInfo = lazy(() => import("../components/worldmap/armies/ActionInfo").then((module) => ({ default: module.ActionInfo })), ); const ArmyInfoLabel = lazy(() => import("../components/worldmap/armies/ArmyInfoLabel").then((module) => ({ default: module.ArmyInfoLabel })), ); + +const BattleInfoLabel = lazy(() => + import("../components/worldmap/battles/BattleLabel").then((module) => ({ default: module.BattleInfoLabel })), +); + const BlankOverlayContainer = lazy(() => import("../containers/BlankOverlayContainer").then((module) => ({ default: module.BlankOverlayContainer })), ); @@ -24,7 +34,7 @@ const StructureInfoLabel = lazy(() => const BattleContainer = lazy(() => import("../containers/BattleContainer").then((module) => ({ default: module.BattleContainer })), ); -const BottomMiddleContainer = lazy(() => import("../containers/BottomMiddleContainer")); +const TopCenterContainer = lazy(() => import("../containers/TopCenterContainer")); const BottomRightContainer = lazy(() => import("../containers/BottomRightContainer").then((module) => ({ default: module.BottomRightContainer })), ); @@ -35,17 +45,22 @@ const Tooltip = lazy(() => import("../elements/Tooltip").then((module) => ({ def const BattleView = lazy(() => import("../modules/military/battle-view/BattleView").then((module) => ({ default: module.BattleView })), ); -const BottomNavigation = lazy(() => - import("../modules/navigation/BottomNavigation").then((module) => ({ default: module.BottomNavigation })), +const TopMiddleNavigation = lazy(() => + import("../modules/navigation/TopNavigation").then((module) => ({ default: module.TopMiddleNavigation })), +); + +const BottomMiddleContainer = lazy(() => + import("../containers/BottomMiddleContainer").then((module) => ({ default: module.BottomMiddleContainer })), ); + const LeftNavigationModule = lazy(() => import("../modules/navigation/LeftNavigationModule").then((module) => ({ default: module.LeftNavigationModule })), ); const RightNavigationModule = lazy(() => import("../modules/navigation/RightNavigationModule").then((module) => ({ default: module.RightNavigationModule })), ); -const TopMiddleNavigation = lazy(() => - import("../modules/navigation/TopMiddleNavigation").then((module) => ({ default: module.TopMiddleNavigation })), +const TopLeftNavigation = lazy(() => + import("../modules/navigation/TopLeftNavigation").then((module) => ({ default: module.TopLeftNavigation })), ); const PlayerId = lazy(() => import("../modules/social/PlayerId").then((module) => ({ default: module.PlayerId }))); const EventStream = lazy(() => @@ -84,7 +99,7 @@ export const World = () => {
@@ -98,6 +113,7 @@ export const World = () => { + @@ -108,8 +124,12 @@ export const World = () => { + + + + - + @@ -121,7 +141,7 @@ export const World = () => { - +
diff --git a/client/src/ui/modules/LoadingScreen.tsx b/client/src/ui/modules/LoadingScreen.tsx index de4509341..cdb812a4a 100644 --- a/client/src/ui/modules/LoadingScreen.tsx +++ b/client/src/ui/modules/LoadingScreen.tsx @@ -35,9 +35,9 @@ export const LoadingScreen = () => { }, []); return ( -
+
-
+
{ }, } = useDojo(); + const { getGuildFromPlayerAddress } = useGuilds(); + const guildName = getGuildFromPlayerAddress(ContractAddress(account.address))?.guildName; + const guildKey = guildName ? shortString.encodeShortString(guildName) : undefined; + const bottomChatRef = useRef(null); const [hideChat, setHideChat] = useState(false); @@ -33,17 +39,157 @@ export const Chat = () => { const currentTab = useChatStore((state) => state.currentTab); const setCurrentTab = useChatStore((state) => state.setCurrentTab); const tabs = useChatStore((state) => state.tabs); - const setTabs = useChatStore((state) => state.setTabs); const addTab = useChatStore((state) => state.addTab); - const allMessageEntities = useEntityQuery([Has(Message)]); const getPlayers = useGetOtherPlayers(); const players = useMemo(() => { return getPlayers(); }, []); + useEffect(() => { + scrollToElement(bottomChatRef); + }, [currentTab]); + + useEffect(() => { + const selfMessageEntities = runQuery([Has(Message), HasValue(Message, { identity: BigInt(account.address) })]); + + const latestSalt = Array.from(selfMessageEntities).reduce((maxSalt, entity) => { + const currentSalt = getComponentValue(Message, entity)?.salt ?? 0n; + return currentSalt > maxSalt ? currentSalt : maxSalt; + }, 0n); + + setSalt(latestSalt + 1n); + }, [account.address]); + + const changeTabs = useCallback( + (tab: string | undefined, address: string, fromSelector: boolean = false) => { + if (address === GLOBAL_CHANNEL_KEY) { + setCurrentTab(DEFAULT_TAB); + return; + } + + if (guildName && guildKey && address === guildKey) { + addTab({ + name: guildName, + address, + key: guildKey, + displayed: true, + lastSeen: new Date(), + }); + return; + } + + if (ContractAddress(address) === ContractAddress(account.address)) { + return; + } + + addTab({ + name: fromSelector + ? shortString.decodeShortString( + getComponentValue(AddressName, getEntityIdFromKeys([BigInt(address)]))?.name.toString() || "", + ) + : tab!, + address, + displayed: true, + lastSeen: new Date(), + key: getMessageKey(account.address, BigInt(address)), + }); + }, + [guildName, guildKey, account.address, addTab, setCurrentTab], + ); + + const renderTabs = useMemo(() => { + return tabs + .filter( + (tab) => + (tab.name === GLOBAL_CHANNEL_KEY || ContractAddress(tab.address) !== ContractAddress(account.address)) && + tab.displayed, + ) + .map((tab) => ); + }, [tabs, account.address, currentTab.name]); + + return ( +
+
+
+ {renderTabs} +
+
{ + setHideChat(!hideChat); + }} + > +
+ +
+
+
+
+ +
+ + +
+
+
+ ); +}; + +const Messages = ({ + account, + currentTab, + guildName, + guildKey, + hideChat, + bottomChatRef, + changeTabs, +}: { + account: { address: string }; + currentTab: Tab; + guildName: string | undefined; + guildKey: string | undefined; + hideChat: boolean; + bottomChatRef: React.RefObject; + changeTabs: (tab: string | undefined, address: string) => void; +}) => { + const { + setup: { + components: { Message, AddressName }, + }, + } = useDojo(); + + const allMessageEntities = useEntityQuery([Has(Message)]); + const messages = useMemo(() => { const messageMap = new Map(); @@ -56,10 +202,10 @@ export const Chat = () => { if (!addressName) return; const fromSelf = message.identity === BigInt(account.address); - const isRelevantMessage = - fromSelf || - BigInt(message.channel) === BigInt(account.address) || - BigInt(message.channel) === BigInt(GLOBAL_CHANNEL); + const toSelf = message.channel === BigInt(account.address); + const isGlobalMessage = BigInt(message.channel) === BigInt(GLOBAL_CHANNEL); + const isGuildMessage = guildKey && BigInt(message.channel) === BigInt(guildKey); + const isRelevantMessage = fromSelf || toSelf || isGlobalMessage || isGuildMessage; if (!isRelevantMessage) return; @@ -70,9 +216,10 @@ export const Chat = () => { const identity = message.identity; const channel = message.channel; - const key = - BigInt(message.channel) === BigInt(GLOBAL_CHANNEL) - ? GLOBAL_CHANNEL + const key = isGlobalMessage + ? GLOBAL_CHANNEL + : isGuildMessage + ? guildKey : getMessageKey(identity, BigInt(message.channel)); if (!messageMap.has(ContractAddress(key))) { @@ -82,8 +229,6 @@ export const Chat = () => { channel: BigInt(message.channel), fromName: name, address, - isChannel: - BigInt(message.channel) !== BigInt(account.address) && BigInt(message.channel) !== BigInt(GLOBAL_CHANNEL), }); } @@ -104,160 +249,52 @@ export const Chat = () => { } }); - // Sort messages within each chat - messageMap.forEach((metadata, key) => { - metadata.messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); - - const tabKey = key.toString().toLowerCase(); // Normalize to lowercase for consistent comparison - - const existingTab = tabs.find((t) => t.address === metadata.address); - - if (existingTab?.name === GLOBAL_CHANNEL_KEY || metadata.address == "0x0") return; - - if (!existingTab) { - const newTab: Tab = { - name: metadata.fromName, - address: metadata.address, - key: getMessageKey(account.address, BigInt(metadata.address)), - displayed: true, - lastSeen: new Date(), - }; - setTabs([...tabs, newTab]); - } - }); - return messageMap; - }, [allMessageEntities, account.address]); + }, [allMessageEntities, account.address, guildKey]); const messagesToDisplay = useMemo(() => { if (currentTab.name === GLOBAL_CHANNEL_KEY) { return messages.get(BigInt(GLOBAL_CHANNEL)); } - return messages.get(ContractAddress(getMessageKey(currentTab.address, account.address))); - }, [messages, currentTab.address]); - - useEffect(() => { - scrollToElement(bottomChatRef); - }, [messagesToDisplay]); - - useEffect(() => { - const selfMessageEntities = runQuery([Has(Message), HasValue(Message, { identity: BigInt(account.address) })]); - - const latestSalt = Array.from(selfMessageEntities).reduce((maxSalt, entity) => { - const currentSalt = getComponentValue(Message, entity)?.salt ?? 0n; - return currentSalt > maxSalt ? currentSalt : maxSalt; - }, 0n); - - setSalt(latestSalt + 1n); - }, [account.address, messages]); - - const changeTabs = (tab: string | undefined, address: string, fromSelector: boolean = false) => { - if (address === GLOBAL_CHANNEL_KEY) { - setCurrentTab(DEFAULT_TAB); - return; - } - - if (ContractAddress(address) === ContractAddress(account.address)) { - return; + if (currentTab.name === guildName && guildKey) { + return messages.get(ContractAddress(guildKey)); } - - addTab({ - name: fromSelector - ? shortString.decodeShortString( - getComponentValue(AddressName, getEntityIdFromKeys([BigInt(address)]))?.name.toString() || "", - ) - : tab!, - address, - displayed: true, - lastSeen: new Date(), - key: getMessageKey(account.address, BigInt(address)), - }); - }; - - const renderTabs = useMemo(() => { - return tabs - .filter((tab) => ContractAddress(tab.address) !== ContractAddress(account.address)) - .map((tab) => ); - }, [tabs, account.address]); + return messages.get(ContractAddress(getMessageKey(currentTab.address, account.address))); + }, [messages, currentTab.address, currentTab.name, guildName, guildKey, account.address]); return ( -
-
-
- {renderTabs} -
+
+ {messagesToDisplay?.messages.map((message, index) => (
{ - setHideChat(!hideChat); + style={{ + color: + currentTab.name === GLOBAL_CHANNEL_KEY + ? CHAT_COLORS.GLOBAL + : currentTab.name === guildName + ? CHAT_COLORS.GUILD + : CHAT_COLORS.PRIVATE, }} + className={`flex gap-2 mb-1`} + key={index} > -
- -
-
-
-
-
- {messagesToDisplay?.messages.map((message, index) => ( -
+ changeTabs(message.name, message.address)}> + {message.fromSelf ? "you" : message.name}: + + -
- changeTabs(message.name, message.address)}> - {message.fromSelf ? "you" : message.name}: - - - {`${message.content}`} - -
-
- ))} - -
-
- - + {`${message.content}`} + +
-
+ ))} +
); }; @@ -269,3 +306,104 @@ const scrollToElement = (ref: React.RefObject) => { } }, 1); }; + +const ChatSelect = ({ + selectedChannel, + changeTabs, + guildName, + players, +}: { + selectedChannel: string; + changeTabs: (tab: string | undefined, address: string, fromSelector?: boolean) => void; + guildName?: string; + players: Player[]; +}) => { + const [searchInput, setSearchInput] = useState(""); + const [open, setOpen] = useState(false); + + const inputRef = useRef(null); + + const guildKey = guildName ? shortString.encodeShortString(guildName) : undefined; + + const handleTabChange = (channel: string) => { + if (channel === GLOBAL_CHANNEL_KEY) { + changeTabs(undefined, GLOBAL_CHANNEL_KEY); + } else if (channel === guildKey) { + changeTabs(undefined, guildKey, true); + } else { + const player = players.find((p) => p.addressName === channel); + if (player) { + changeTabs(undefined, toHexString(player.address), true); + } + } + }; + + const filteredPlayers = players.filter( + (player) => + player.addressName.toLowerCase().startsWith(searchInput.toLowerCase()) || player.addressName === selectedChannel, + ); + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + if (newOpen && inputRef.current) { + setSearchInput(""); + setTimeout(() => { + inputRef.current?.focus(); + }, 0); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + if (filteredPlayers.length > 0) { + const selectedPlayer = filteredPlayers.find((player) => player.addressName !== selectedChannel); + if (selectedPlayer) { + handleTabChange(selectedPlayer.addressName); + setOpen(false); + } + } + setSearchInput(""); + } else { + e.stopPropagation(); + } + }; + + return ( + + ); +}; diff --git a/client/src/ui/modules/chat/ChatState.tsx b/client/src/ui/modules/chat/ChatState.tsx index 86a8f6761..5406727eb 100644 --- a/client/src/ui/modules/chat/ChatState.tsx +++ b/client/src/ui/modules/chat/ChatState.tsx @@ -18,7 +18,7 @@ interface ChatState { export const useChatStore = create()( persist( (set, get) => ({ - tabs: [], + tabs: [DEFAULT_TAB], currentTab: DEFAULT_TAB, setCurrentTab: (tab: Tab) => { set({ currentTab: tab }); @@ -49,7 +49,7 @@ export const useChatStore = create()( }), hideTab: (tab: Tab) => set((state) => ({ - tabs: state.tabs.map((t) => (t.address === tab.address ? { ...t, visible: false } : t)), + tabs: state.tabs.map((t) => (t.address === tab.address ? { ...t, displayed: false } : t)), currentTab: DEFAULT_TAB, })), deleteTab: (address: string) => diff --git a/client/src/ui/modules/chat/ChatTab.tsx b/client/src/ui/modules/chat/ChatTab.tsx index 42e990fb4..b008cd6f5 100644 --- a/client/src/ui/modules/chat/ChatTab.tsx +++ b/client/src/ui/modules/chat/ChatTab.tsx @@ -23,7 +23,6 @@ export interface Tab { export const ChatTab = ({ tab, selected }: { tab: Tab; selected: boolean }) => { const setCurrentTab = useChatStore((state) => state.setCurrentTab); - const hideTab = useChatStore((state) => state.hideTab); const userName = useMemo(() => { @@ -42,24 +41,24 @@ export const ChatTab = ({ tab, selected }: { tab: Tab; selected: boolean }) => { transition={{ duration: 0.3 }} >
setCurrentTab({ ...tab, displayed: true })} > {userName} -
+
{tab.name !== GLOBAL_CHANNEL_KEY && ( )}
diff --git a/client/src/ui/modules/chat/constants.tsx b/client/src/ui/modules/chat/constants.tsx index bc54832da..9ddd334ad 100644 --- a/client/src/ui/modules/chat/constants.tsx +++ b/client/src/ui/modules/chat/constants.tsx @@ -1,7 +1,13 @@ import { shortString } from "starknet"; -export const PASTEL_PINK = "#FFB3BA"; -export const PASTEL_BLUE = "#BAE1FF"; +const PASTEL_PINK = "#FFB3BA"; +const PASTEL_BLUE = "#BAE1FF"; +const PASTEL_GREEN = "#B5EAD7"; +export const CHAT_COLORS = { + GLOBAL: PASTEL_PINK, + GUILD: PASTEL_GREEN, + PRIVATE: PASTEL_BLUE, +}; export const GLOBAL_CHANNEL = shortString.encodeShortString("global"); export const CHAT_STORAGE_KEY = "chat_tabs"; export const GLOBAL_CHANNEL_KEY = "global"; diff --git a/client/src/ui/modules/chat/types.tsx b/client/src/ui/modules/chat/types.tsx index 637655ae3..7c127b6ee 100644 --- a/client/src/ui/modules/chat/types.tsx +++ b/client/src/ui/modules/chat/types.tsx @@ -11,7 +11,6 @@ interface ChatMessage { export interface ChatMetadata { messages: ChatMessage[]; lastMessageReceived: Date; - isChannel: boolean; channel: bigint; fromName: string; address: string; diff --git a/client/src/ui/modules/entity-details/BuildingEntityDetails.tsx b/client/src/ui/modules/entity-details/BuildingEntityDetails.tsx index 36bc46351..2725567fc 100644 --- a/client/src/ui/modules/entity-details/BuildingEntityDetails.tsx +++ b/client/src/ui/modules/entity-details/BuildingEntityDetails.tsx @@ -19,20 +19,20 @@ import { ResourceIdToMiningType, copyPlayerAddressToClipboard, displayAddress, + divideByPrecision, formatTime, getEntityIdFromKeys, toHexString, } from "@/ui/utils/utils"; import { BuildingType, - EternumGlobalConfig, ID, LEVEL_DESCRIPTIONS, - REALM_UPGRADE_COSTS, + REALM_MAX_LEVEL, RealmLevels, ResourcesIds, StructureType, - scaleResources, + TickIds, } from "@bibliothecadao/eternum"; import { useComponentValue } from "@dojoengine/react"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -197,8 +197,7 @@ const CastleDetails = () => { const immunityEndTimestamp = useMemo(() => { return ( - Number(structure.created_at) + - configManager.getBattleGraceTickCount() * EternumGlobalConfig.tick.armiesTickIntervalInSeconds + Number(structure.created_at) + configManager.getBattleGraceTickCount() * configManager.getTick(TickIds.Armies) ); }, [structure.created_at, configManager]); @@ -211,21 +210,17 @@ const CastleDetails = () => { const getNextRealmLevel = useMemo(() => { const nextLevel = realm.level + 1; - return nextLevel <= RealmLevels.Empire ? nextLevel : null; + return nextLevel <= REALM_MAX_LEVEL ? nextLevel : null; }, [realm.level]); const checkBalance = useMemo(() => { if (!getNextRealmLevel) return false; - const cost = scaleResources( - REALM_UPGRADE_COSTS[getNextRealmLevel as keyof typeof REALM_UPGRADE_COSTS], - EternumGlobalConfig.resources.resourceMultiplier, - ); - + const cost = configManager.realmUpgradeCosts[getNextRealmLevel]; return Object.keys(cost).every((resourceId) => { const resourceCost = cost[Number(resourceId)]; const balance = getBalance(structureEntityId, resourceCost.resource); - return balance.balance / EternumGlobalConfig.resources.resourcePrecision >= resourceCost.amount; + return divideByPrecision(balance.balance) >= resourceCost.amount; }); }, [getBalance, structureEntityId]); @@ -304,10 +299,7 @@ const CastleDetails = () => {

Upgrade Cost to {RealmLevels[realm.level + 1]}
- {scaleResources( - REALM_UPGRADE_COSTS[(realm.level + 1) as keyof typeof REALM_UPGRADE_COSTS], - EternumGlobalConfig.resources.resourceMultiplier, - )?.map((a) => { + {configManager.realmUpgradeCosts[getNextRealmLevel]?.map((a) => { return ( { - const dojo = useDojo(); - const selectedHex = useUIStore((state) => state.selectedHex); - const updateSelectedEntityId = useUIStore((state) => state.updateSelectedEntityId); + const selectedEntityId = useUIStore((state) => state.armyActions.selectedEntityId); + const setSelectedEntityId = useUIStore((state) => state.updateSelectedEntityId); - const getStructures = getPlayerStructures(); - const hexPosition = useMemo(() => new Position({ x: selectedHex.col, y: selectedHex.row }), [selectedHex]); + const hexPosition = useMemo( + () => new Position({ x: selectedHex?.col || 0, y: selectedHex?.row || 0 }), + [selectedHex], + ); + const { playerStructures } = useEntities(); const ownArmiesAtPosition = useOwnArmiesByPosition({ position: hexPosition.getContract(), inBattle: false, - playerStructures: getStructures(ContractAddress(dojo.account.account.address)), + playerStructures: playerStructures(), }); const userArmies = useMemo( @@ -36,19 +37,9 @@ export const CombatEntityDetails = () => { [ownArmiesAtPosition], ); - const [selectedArmyEntityId, setSelectedArmyEntityId] = useState(userArmies?.[0]?.entity_id || 0); - - useEffect(() => { - setSelectedArmyEntityId(userArmies?.[0]?.entity_id || 0); - }, [userArmies]); - - useEffect(() => { - updateSelectedEntityId(selectedArmyEntityId); - }, [selectedArmyEntityId, updateSelectedEntityId]); - const ownArmy = useMemo(() => { - return ownArmiesAtPosition.find((army) => army.entity_id === selectedArmyEntityId); - }, [ownArmiesAtPosition, selectedArmyEntityId]); + return ownArmiesAtPosition.find((army) => army.entity_id === selectedEntityId); + }, [ownArmiesAtPosition, selectedEntityId]); const structure = useStructureAtPosition(hexPosition.getContract()); const battles = useBattlesByPosition(hexPosition.getContract()); @@ -112,8 +103,8 @@ export const CombatEntityDetails = () => { {selectedTab !== 2 && userArmies.length > 0 && ( )} diff --git a/client/src/ui/modules/entity-details/Entities.tsx b/client/src/ui/modules/entity-details/Entities.tsx index 0ef4364bf..0b201dcb2 100644 --- a/client/src/ui/modules/entity-details/Entities.tsx +++ b/client/src/ui/modules/entity-details/Entities.tsx @@ -1,18 +1,17 @@ import { useDojo } from "@/hooks/context/DojoContext"; import { ArmyInfo, useEnemyArmiesByPosition } from "@/hooks/helpers/useArmies"; -import { getPlayerStructures } from "@/hooks/helpers/useEntities"; +import { useEntities } from "@/hooks/helpers/useEntities"; import { Position } from "@/types/Position"; import { StructureCard } from "@/ui/components/hyperstructures/StructureCard"; -import { ContractAddress } from "@bibliothecadao/eternum"; import { EnemyArmies } from "./EnemyArmies"; export const Entities = ({ position, ownArmy }: { position: Position; ownArmy: ArmyInfo | undefined }) => { const dojo = useDojo(); - const getStructures = getPlayerStructures(); + const { playerStructures } = useEntities(); const enemyArmies = useEnemyArmiesByPosition({ position: position.getContract(), - playerStructures: getStructures(ContractAddress(dojo.account.account.address)), + playerStructures: playerStructures(), }); return ( diff --git a/client/src/ui/modules/military/Military.tsx b/client/src/ui/modules/military/Military.tsx index 76241edcd..baa418380 100644 --- a/client/src/ui/modules/military/Military.tsx +++ b/client/src/ui/modules/military/Military.tsx @@ -1,22 +1,15 @@ -import { useDojo } from "@/hooks/context/DojoContext"; -import { getPlayerStructures } from "@/hooks/helpers/useEntities"; +import { useEntities } from "@/hooks/helpers/useEntities"; import { useQuery } from "@/hooks/helpers/useQuery"; import { EntityArmyList } from "@/ui/components/military/ArmyList"; import { EntitiesArmyTable } from "@/ui/components/military/EntitiesArmyTable"; -import { ContractAddress, ID } from "@bibliothecadao/eternum"; -import { useMemo } from "react"; +import { ID } from "@bibliothecadao/eternum"; export const Military = ({ entityId, className }: { entityId: ID | undefined; className?: string }) => { - const { - account: { account }, - } = useDojo(); - const { isMapView } = useQuery(); - const getStructures = getPlayerStructures(); - const selectedStructure = useMemo(() => { - return getStructures(ContractAddress(account.address)).find((structure) => structure.entity_id === entityId); - }, [getStructures, entityId]); + const { playerStructures } = useEntities(); + + const selectedStructure = playerStructures().find((structure) => structure.entity_id === entityId); return (
diff --git a/client/src/ui/modules/military/battle-view/Battle.tsx b/client/src/ui/modules/military/battle-view/Battle.tsx index be0895d2e..556b5b375 100644 --- a/client/src/ui/modules/military/battle-view/Battle.tsx +++ b/client/src/ui/modules/military/battle-view/Battle.tsx @@ -11,7 +11,7 @@ import { ComponentValue } from "@dojoengine/recs"; import { motion } from "framer-motion"; import { useState } from "react"; import { BattleActions } from "./BattleActions"; -import { BattleProgressBar } from "./BattleProgressBar"; +import { BattleProgress } from "./BattleProgress"; import { BattleSideView } from "./BattleSideView"; import { LockedResources } from "./LockedResources"; import { TopScreenView } from "./TopScreenView"; @@ -67,16 +67,17 @@ export const Battle = ({ )}
- -
+ +
+
state.setTooltip); const currentTimestamp = useUIStore((state) => state.nextBlockTimestamp); - const currentArmiesTick = useUIStore((state) => state.currentArmiesTick); const setBattleView = useUIStore((state) => state.setBattleView); const setView = useUIStore((state) => state.setLeftNavigationView); const [loading, setLoading] = useState(Loading.None); const [raidWarning, setRaidWarning] = useState(false); - const [localSelectedUnit, setLocalSelectedUnit] = useState(ownArmyEntityId || 0); + const [localSelectedUnit, setLocalSelectedUnit] = useState(ownArmyEntityId ?? 0); useEffect(() => { if (localSelectedUnit === 0 || !userArmiesInBattle.some((army) => army.entity_id === localSelectedUnit)) { - const newLocalSelectedUnit = userArmiesInBattle?.[0]?.entity_id ?? 0; + const newLocalSelectedUnit = userArmiesInBattle?.[0]?.entity_id ?? ownArmyEntityId; setLocalSelectedUnit(newLocalSelectedUnit); } - }, [userArmiesInBattle]); + }, [userArmiesInBattle, ownArmyEntityId]); const isActive = useMemo(() => battleManager.isBattleOngoing(currentTimestamp!), [battleManager, currentTimestamp]); const selectedArmy = useMemo(() => { return getAliveArmy(localSelectedUnit || 0); - }, [localSelectedUnit, isActive]); + }, [localSelectedUnit, isActive, userArmiesInBattle]); const defenderArmy = useMemo(() => { - const defender = structure?.protector ? structure.protector : defenderArmies[0]; - const battleManager = new BattleManager(defender?.battle_id || 0, dojo); + const defender = structure?.protector ?? defenderArmies[0]; + + const battleManager = new BattleManager(defender?.battle_id || battleAdjusted?.entity_id || 0, dojo); return battleManager.getUpdatedArmy(defender, battleManager.getUpdatedBattle(currentTimestamp!)); - }, [defenderArmies, localSelectedUnit, isActive]); + }, [defenderArmies, localSelectedUnit, isActive, currentTimestamp, battleAdjusted]); const handleRaid = async () => { if (selectedArmy?.battle_id !== 0 && !raidWarning) { @@ -99,59 +99,70 @@ export const BattleActions = ({ setLoading(Loading.Raid); setRaidWarning(false); - - await battleManager.pillageStructure(selectedArmy!, structure!.entity_id); + try { + await battleManager.pillageStructure(selectedArmy!, structure!.entity_id); + toggleModal( + + Pillage History + + , + ); + setBattleView(null); + setView(View.None); + } catch (error) { + console.error("Error during pillage:", error); + } setLoading(Loading.None); - setBattleView(null); - setView(View.None); - toggleModal( - - Pillage History - - , - ); }; const handleBattleStart = async () => { setLoading(Loading.Start); - if (battleStartStatus === BattleStartStatus.ForceStart) { - await battle_force_start({ - signer: account, - battle_id: battleManager?.battleEntityId || 0, - defending_army_id: defenderArmy!.entity_id, - }); - } else { - await battle_start({ - signer: account, - attacking_army_id: selectedArmy!.entity_id, - defending_army_id: defenderArmy!.entity_id, + try { + if (battleStartStatus === BattleStartStatus.ForceStart) { + await battle_force_start({ + signer: account, + battle_id: battleManager?.battleEntityId || 0, + defending_army_id: defenderArmy!.entity_id, + }); + } else { + await battle_start({ + signer: account, + attacking_army_id: selectedArmy!.entity_id, + defending_army_id: defenderArmy!.entity_id, + }); + } + setBattleView({ + engage: false, + battleEntityId: undefined, + ownArmyEntityId: undefined, + targetArmy: defenderArmy?.entity_id, }); + } catch (error) { + console.error("Error during battle start:", error); } - setBattleView({ - engage: false, - battleEntityId: undefined, - ownArmyEntityId: undefined, - targetArmy: defenderArmy?.entity_id, - }); setLoading(Loading.None); }; const handleBattleClaim = async () => { setLoading(Loading.Claim); - if (battleAdjusted?.entity_id! !== 0 && battleAdjusted?.entity_id === selectedArmy!.battle_id) { - await battle_leave_and_claim({ - signer: account, - army_id: selectedArmy!.entity_id, - battle_id: battleManager?.battleEntityId || 0, - structure_id: structure!.entity_id, - }); - } else { - await battle_claim({ - signer: account, - army_id: selectedArmy!.entity_id, - structure_id: structure!.entity_id, - }); + try { + if (battleAdjusted?.entity_id! !== 0 && battleAdjusted?.entity_id === selectedArmy!.battle_id) { + await battle_leave_and_claim({ + signer: account, + army_id: selectedArmy!.entity_id, + battle_id: battleManager?.battleEntityId || 0, + structure_id: structure!.entity_id, + }); + } else { + await battle_claim({ + signer: account, + army_id: selectedArmy!.entity_id, + structure_id: structure!.entity_id, + }); + } + } catch (error) { + console.error("Error during claim:", error); } setBattleView(null); setView(View.None); @@ -166,10 +177,6 @@ export const BattleActions = ({ army_ids: [selectedArmy!.entity_id], battle_id: battleManager?.battleEntityId || 0, }).then(() => { - setLoading(Loading.None); - setBattleView(null); - setView(View.None); - const attackerArmiesLength = attackerArmies.some((army) => army?.entity_id === selectedArmy?.entity_id) ? attackerArmies.length - 1 : attackerArmies.length; @@ -180,26 +187,29 @@ export const BattleActions = ({ battleManager.deleteBattle(); } }); + setLoading(Loading.None); + setBattleView(null); + setView(View.None); }; const claimStatus = useMemo( () => battleManager.isClaimable(currentTimestamp!, selectedArmy, structure, defenderArmy), - [battleManager, currentTimestamp, selectedArmy], + [battleManager, currentTimestamp, selectedArmy, structure, defenderArmy], ); const raidStatus = useMemo( - () => battleManager.isRaidable(currentTimestamp!, currentArmiesTick, selectedArmy, structure), - [battleManager, currentTimestamp, selectedArmy], + () => battleManager.isRaidable(currentTimestamp!, currentTimestamp!, selectedArmy, structure), + [battleManager, currentTimestamp, selectedArmy, structure, currentTimestamp], ); const battleStartStatus = useMemo( () => battleManager.isAttackable(selectedArmy, defenderArmy, currentTimestamp!), - [battleManager, defenderArmy, currentTimestamp], + [battleManager, defenderArmy, currentTimestamp, selectedArmy], ); const leaveStatus = useMemo( () => battleManager.isLeavable(currentTimestamp!, selectedArmy), - [battleManager, selectedArmy], + [currentTimestamp, battleManager, selectedArmy], ); const mouseEnterRaid = useCallback(() => { @@ -235,7 +245,7 @@ export const BattleActions = ({
+
{eventClone.event_id !== EventType.BattleStart && eventClone.event_id !== EventType.BattleAttacked diff --git a/client/src/ui/modules/military/battle-view/BattleProgressBar.tsx b/client/src/ui/modules/military/battle-view/BattleProgress.tsx similarity index 73% rename from client/src/ui/modules/military/battle-view/BattleProgressBar.tsx rename to client/src/ui/modules/military/battle-view/BattleProgress.tsx index 0e31c7afa..5312deaab 100644 --- a/client/src/ui/modules/military/battle-view/BattleProgressBar.tsx +++ b/client/src/ui/modules/military/battle-view/BattleProgress.tsx @@ -17,48 +17,62 @@ function formatTimeDifference(time: Date) { } } -export const BattleProgressBar = ({ +export const DurationLeft = ({ battleManager, - ownArmySide, - attackingHealth, - attackerArmies, - defendingHealth, - defenderArmies, + currentTimestamp, structure, }: { battleManager: BattleManager | undefined; - ownArmySide: string; - attackingHealth: Health | undefined; - attackerArmies: ArmyInfo[]; - defendingHealth: Health | undefined; - defenderArmies: (ArmyInfo | undefined)[]; + currentTimestamp: number; structure: Structure | undefined; }) => { - const currentTimestamp = useUIStore((state) => state.nextBlockTimestamp); - - const playUnitSelectedOne = useUiSounds(soundSelector.unitSelected1).play; - const playUnitSelectedTwo = useUiSounds(soundSelector.unitSelected2).play; - const playUnitSelectedThree = useUiSounds(soundSelector.unitSelected3).play; - const playBattleVictory = useUiSounds(soundSelector.battleVictory).play; - const playBattleDefeat = useUiSounds(soundSelector.battleDefeat).play; - const durationLeft = useMemo(() => { if (!battleManager) return undefined; - if (battleManager.isSiege(currentTimestamp!)) return battleManager.getSiegeTimeLeft(currentTimestamp!); - return battleManager!.getTimeLeft(currentTimestamp!); - }, [attackingHealth, currentTimestamp, defendingHealth]); + if (battleManager.isSiege(currentTimestamp)) return battleManager.getSiegeTimeLeft(currentTimestamp); + return battleManager.getTimeLeft(currentTimestamp); + }, [battleManager, currentTimestamp]); - const [time, setTime] = useState(durationLeft); + const [timeLeft, setTimeLeft] = useState(durationLeft); - const attackerName = `${attackerArmies.length > 0 ? "Attackers" : "Empty"} ${ownArmySide === "Attack" ? "" : ""}`; - const defenderName = structure - ? structure.isMercenary - ? "Bandits" - : `${structure!.name} ${ownArmySide === "Defence" ? "(⚔️)" : ""}` - : defenderArmies?.length > 0 - ? `Defenders ${ownArmySide === "Defence" ? "(⚔️)" : ""}` - : "Empty"; + useEffect(() => { + if (!timeLeft) return; + if (timeLeft.getTime() === 0) return; + const timer = setInterval(() => { + const date = new Date(0); + date.setTime(timeLeft.getTime() - 1000); + setTimeLeft(date); + }, 1000); + return () => clearInterval(timer); + }, [timeLeft]); + useEffect(() => { + setTimeLeft(durationLeft); + }, [durationLeft]); + + const battleType = useMemo(() => { + return battleManager?.getBattleType(structure); + }, [battleManager, structure]); + + if (timeLeft) { + if (battleManager?.isSiege(currentTimestamp) && battleType === BattleType.Structure) { + return `Siege ongoing: ${formatTimeDifference(timeLeft)} left`; + } else if (battleManager?.isSiege(currentTimestamp)) { + return "Loading..."; + } else { + return `${formatTimeDifference(timeLeft)} left`; + } + } +}; + +export const ProgressBar = ({ + className, + attackingHealth, + defendingHealth, +}: { + className?: string; + attackingHealth: { current: bigint } | undefined; + defendingHealth: { current: bigint } | undefined; +}) => { const totalHealth = useMemo( () => (attackingHealth?.current || 0n) + (defendingHealth?.current || 0n), [attackingHealth, defendingHealth], @@ -75,21 +89,6 @@ export const BattleProgressBar = ({ [defendingHealth, totalHealth], ); - useEffect(() => { - if (!time) return; - if (time.getTime() === 0) return; - const timer = setInterval(() => { - const date = new Date(0); - date.setTime(time.getTime() - 1000); - setTime(date); - }, 1000); - return () => clearInterval(timer); - }, [time]); - - useEffect(() => { - setTime(durationLeft); - }, [durationLeft]); - const gradient = useMemo(() => { const attackPercentage = parseFloat(attackingHealthPercentage); const defendPercentage = parseFloat(defendingHealthPercentage); @@ -97,14 +96,65 @@ export const BattleProgressBar = ({ attackPercentage + defendPercentage }%)`; }, [attackingHealthPercentage, defendingHealthPercentage]); + return ( + <> + {!isNaN(Number(attackingHealthPercentage)) && !isNaN(Number(defendingHealthPercentage)) && ( +
+
+ {Number(attackingHealthPercentage) > 0 && ( +
+

{attackingHealthPercentage}%

+
+ )} + {Number(defendingHealthPercentage) > 0 && ( +
+

{defendingHealthPercentage}%

+
+ )} +
+
+ )} + + ); +}; + +export const BattleProgress = ({ + battleManager, + ownArmySide, + attackingHealth, + attackerArmies, + defendingHealth, + defenderArmies, + structure, +}: { + battleManager: BattleManager | undefined; + ownArmySide: string; + attackingHealth: Health | undefined; + attackerArmies: ArmyInfo[]; + defendingHealth: Health | undefined; + defenderArmies: (ArmyInfo | undefined)[]; + structure: Structure | undefined; +}) => { + const currentTimestamp = useUIStore((state) => state.nextBlockTimestamp); + + const playUnitSelectedOne = useUiSounds(soundSelector.unitSelected1).play; + const playUnitSelectedTwo = useUiSounds(soundSelector.unitSelected2).play; + const playUnitSelectedThree = useUiSounds(soundSelector.unitSelected3).play; + const playBattleVictory = useUiSounds(soundSelector.battleVictory).play; + const playBattleDefeat = useUiSounds(soundSelector.battleDefeat).play; + + const attackerName = `${attackerArmies.length > 0 ? "Attackers" : "Empty"} ${ownArmySide === "Attack" ? "" : ""}`; + const defenderName = structure + ? structure.isMercenary + ? "Bandits" + : `${structure!.name} ${ownArmySide === "Defence" ? "(⚔️)" : ""}` + : defenderArmies?.length > 0 + ? `Defenders ${ownArmySide === "Defence" ? "(⚔️)" : ""}` + : "Empty"; const battleStatus = useMemo(() => { if (battleManager) return battleManager.getWinner(currentTimestamp!, ownArmySide); - }, [time, battleManager, currentTimestamp, ownArmySide]); - - const battleType = useMemo(() => { - return battleManager?.getBattleType(structure); - }, [battleManager, structure]); + }, [battleManager, currentTimestamp, ownArmySide]); useEffect(() => { if (battleStatus === BattleStatus.BattleStart) { @@ -136,44 +186,22 @@ export const BattleProgressBar = ({ - {!isNaN(Number(attackingHealthPercentage)) && !isNaN(Number(defendingHealthPercentage)) && ( -
-
- {Number(attackingHealthPercentage) > 0 && ( -
-

{attackingHealthPercentage}%

-
- )} - {Number(defendingHealthPercentage) > 0 && ( -
-

{defendingHealthPercentage}%

-
- )} -
-
- )} -
+ +
-

- {attackerName} {} -

+

{attackerName}

- {time - ? `${ - battleManager?.isSiege(currentTimestamp!) && battleType === BattleType.Structure - ? `Siege ongoing: ${formatTimeDifference(time)} left` - : battleManager?.isSiege(currentTimestamp!) - ? "Loading..." - : `${formatTimeDifference(time)} left` - }` - : battleStatus} + {currentTimestamp && ( + + )} + {battleStatus}

{defenderName}

diff --git a/client/src/ui/modules/military/battle-view/BattleSideView.tsx b/client/src/ui/modules/military/battle-view/BattleSideView.tsx index c3a560a9e..c3f20416e 100644 --- a/client/src/ui/modules/military/battle-view/BattleSideView.tsx +++ b/client/src/ui/modules/military/battle-view/BattleSideView.tsx @@ -83,11 +83,11 @@ export const BattleSideView = ({ return (
-
+
+
{addressName} {army?.name} {army?.isMine && ( @@ -139,7 +139,7 @@ export const BattleSideView = ({ {showBattleDetails && battleEntityId ? ( ) : ( - + )}
); diff --git a/client/src/ui/modules/military/battle-view/BattleView.tsx b/client/src/ui/modules/military/battle-view/BattleView.tsx index 457167997..0c95b363a 100644 --- a/client/src/ui/modules/military/battle-view/BattleView.tsx +++ b/client/src/ui/modules/military/battle-view/BattleView.tsx @@ -22,8 +22,8 @@ export const BattleView = () => { const updatedTarget = useArmyByArmyEntityId(battleView?.targetArmy || 0); const battlePosition = useMemo( - () => ({ x: selectedHex.col, y: selectedHex.row }), - [selectedHex.col, selectedHex.row], + () => ({ x: selectedHex?.col || 0, y: selectedHex?.row || 0 }), + [selectedHex?.col, selectedHex?.row], ); const targetArmy = useMemo(() => { diff --git a/client/src/ui/modules/military/battle-view/EntityAvatar.tsx b/client/src/ui/modules/military/battle-view/EntityAvatar.tsx index 25e69cb76..38b09ce95 100644 --- a/client/src/ui/modules/military/battle-view/EntityAvatar.tsx +++ b/client/src/ui/modules/military/battle-view/EntityAvatar.tsx @@ -10,10 +10,13 @@ export const EntityAvatar = ({ show?: boolean; address?: string; }) => { - const isRealm = Boolean(structure) && String(structure?.category) === "Realm"; - const isHyperstructure = Boolean(structure) && String((structure as Structure).category) === "Hyperstructure"; - const isFragmentMine = Boolean(structure) && String((structure as Structure).category) === "FragmentMine"; - const isMercenary = Boolean(structure) && structure!.isMercenary; + const getStructureCategory = (s?: Structure) => s?.category?.toString(); + const isCategory = (category: string, s?: Structure) => getStructureCategory(s) === category; + + const isRealm = isCategory("Realm", structure); + const isHyperstructure = isCategory("Hyperstructure", structure); + const isFragmentMine = isCategory("FragmentMine", structure); + const isMercenary = structure?.isMercenary; const randomAvatarIndex = (parseInt(address.slice(0, 8), 16) % 7) + 1; let imgSource = `./images/avatars/${randomAvatarIndex}.png`; @@ -29,18 +32,18 @@ export const EntityAvatar = ({ } const displayImg = structure || show; - const slideUp = { - hidden: { y: "100%" }, - visible: { y: "0%", transition: { duration: 0.6 } }, - }; + return ( -
+
{displayImg && ( diff --git a/client/src/ui/modules/military/battle-view/LockedResources.tsx b/client/src/ui/modules/military/battle-view/LockedResources.tsx index 402238a7e..2e344fc5d 100644 --- a/client/src/ui/modules/military/battle-view/LockedResources.tsx +++ b/client/src/ui/modules/military/battle-view/LockedResources.tsx @@ -9,7 +9,7 @@ export const LockedResources = ({ defendersResourcesEscrowEntityId: ID; }) => { return ( -
+
Locked Resources
{ animate="visible" exit="hidden" > -
-
Battle!
-
diff --git a/client/src/ui/modules/military/battle-view/Troops.tsx b/client/src/ui/modules/military/battle-view/Troops.tsx index 7cdb130ef..f3ef3d3ef 100644 --- a/client/src/ui/modules/military/battle-view/Troops.tsx +++ b/client/src/ui/modules/military/battle-view/Troops.tsx @@ -14,16 +14,21 @@ export const TroopRow = ({
- +
); @@ -41,16 +46,13 @@ const TroopCard = ({ defending?: boolean; }) => { return ( -
+
{ResourcesIds[id]} -
{ResourcesIds[id]}
+

{ResourcesIds[id]}

x {currencyFormat(count, 0)}
); diff --git a/client/src/ui/modules/navigation/SecondaryMenuItems.tsx b/client/src/ui/modules/navigation/SecondaryMenuItems.tsx index 36cd0992f..52bab1062 100644 --- a/client/src/ui/modules/navigation/SecondaryMenuItems.tsx +++ b/client/src/ui/modules/navigation/SecondaryMenuItems.tsx @@ -61,7 +61,7 @@ export const SecondaryMenuItems = () => { /> {completedQuests.length < 8 && !isMapView && realmSelected && ( -
+
Complete quests to master the game mechanics diff --git a/client/src/ui/modules/navigation/TopMiddleNavigation.tsx b/client/src/ui/modules/navigation/TopLeftNavigation.tsx similarity index 89% rename from client/src/ui/modules/navigation/TopMiddleNavigation.tsx rename to client/src/ui/modules/navigation/TopLeftNavigation.tsx index e160a40fc..fa3283385 100644 --- a/client/src/ui/modules/navigation/TopMiddleNavigation.tsx +++ b/client/src/ui/modules/navigation/TopLeftNavigation.tsx @@ -1,3 +1,4 @@ +import { configManager } from "@/dojo/setup"; import { useDojo } from "@/hooks/context/DojoContext"; import { useEntities, useEntitiesUtils } from "@/hooks/helpers/useEntities"; import { useQuery } from "@/hooks/helpers/useQuery"; @@ -12,15 +13,7 @@ import Button from "@/ui/elements/Button"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui/elements/Select"; import { formatTime, gramToKg, kgToGram } from "@/ui/utils/utils"; -import { - BASE_POPULATION_CAPACITY, - BuildingType, - CapacityConfigCategory, - EternumGlobalConfig, - ID, - ResourcesIds, - WEIGHTS_GRAM, -} from "@bibliothecadao/eternum"; +import { BuildingType, CapacityConfigCategory, ID, ResourcesIds, TickIds } from "@bibliothecadao/eternum"; import { useComponentValue } from "@dojoengine/react"; import { getComponentValue } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; @@ -52,15 +45,15 @@ const StorehouseTooltipContent = ({ storehouseCapacity }: { storehouseCapacity:
  • - {(capacity / WEIGHTS_GRAM[ResourcesIds.Lords]).toLocaleString()} Lords + {(capacity / configManager.getResourceWeight(ResourcesIds.Lords)).toLocaleString()} Lords
  • - {(capacity / WEIGHTS_GRAM[ResourcesIds.Wheat]).toLocaleString()} Food + {(capacity / configManager.getResourceWeight(ResourcesIds.Wheat)).toLocaleString()} Food
  • - {(capacity / WEIGHTS_GRAM[ResourcesIds.Wood]).toLocaleString()} Other + {(capacity / configManager.getResourceWeight(ResourcesIds.Wood)).toLocaleString()} Other

Build Storehouses to increase capacity.

@@ -69,12 +62,12 @@ const StorehouseTooltipContent = ({ storehouseCapacity }: { storehouseCapacity: }; const WorkersHutTooltipContent = () => { - const capacity = EternumGlobalConfig.populationCapacity.workerHuts; + const capacity = configManager.getBuildingPopConfig(BuildingType.WorkersHut).capacity; return (

Population Capacity

    -
  • {BASE_POPULATION_CAPACITY} Base Capacity
  • +
  • {configManager.getBasePopulationCapacity()} Base Capacity
  • +{capacity} per Workers Hut

Build Workers Huts to increase population capacity.

@@ -82,7 +75,7 @@ const WorkersHutTooltipContent = () => { ); }; -export const TopMiddleNavigation = () => { +export const TopLeftNavigation = () => { const { setup } = useDojo(); const { isMapView, handleUrlChange, hexPosition } = useQuery(); @@ -143,22 +136,21 @@ export const TopMiddleNavigation = () => { getEntityIdFromKeys([BigInt(structureEntityId || 0), BigInt(BuildingType.Storehouse)]), )?.value || 0; - return ( - quantity * gramToKg(Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Storehouse])) + - gramToKg(Number(EternumGlobalConfig.carryCapacityGram[CapacityConfigCategory.Storehouse])) - ); + const storehouseCapacity = configManager.getCapacityConfig(CapacityConfigCategory.Storehouse); + + return quantity * gramToKg(storehouseCapacity) + gramToKg(storehouseCapacity); }, [structureEntityId, nextBlockTimestamp]); const { timeLeftBeforeNextTick, progress } = useMemo(() => { - const timeLeft = nextBlockTimestamp % EternumGlobalConfig.tick.armiesTickIntervalInSeconds; - const progressValue = (timeLeft / EternumGlobalConfig.tick.armiesTickIntervalInSeconds) * 100; + const timeLeft = nextBlockTimestamp % configManager.getTick(TickIds.Armies); + const progressValue = (timeLeft / configManager.getTick(TickIds.Armies)) * 100; return { timeLeftBeforeNextTick: timeLeft, progress: progressValue }; }, [nextBlockTimestamp]); return (
-
+
{structure.isMine ? (