From 73dd618568852a0c590a935b83f428741ce902b0 Mon Sep 17 00:00:00 2001 From: Credence Date: Fri, 27 Sep 2024 01:57:42 +0100 Subject: [PATCH 01/15] contracts: realm leveling --- client/src/dojo/contractComponents.ts | 3 +- contracts/src/models/config.cairo | 10 ++++ contracts/src/models/owner.cairo | 1 + contracts/src/models/realm.cairo | 11 ++-- .../src/systems/buildings/contracts.cairo | 33 +++++++---- contracts/src/systems/config/contracts.cairo | 57 ++++++++++++++++++- contracts/src/systems/realm/contracts.cairo | 45 ++++++++++++++- .../tests/travel_systems_tests.cairo | 2 + 8 files changed, 141 insertions(+), 21 deletions(-) diff --git a/client/src/dojo/contractComponents.ts b/client/src/dojo/contractComponents.ts index cba53477d..84a35e472 100644 --- a/client/src/dojo/contractComponents.ts +++ b/client/src/dojo/contractComponents.ts @@ -909,12 +909,13 @@ export function defineContractComponents(world: World) { regions: RecsType.Number, wonder: RecsType.Number, order: RecsType.Number, + level: RecsType.Number, }, { metadata: { namespace: "eternum", name: "Realm", - types: ["u32", "u32", "u128", "u8", "u8", "u8", "u8", "u8", "u8", "u8"], + types: ["u32", "u32", "u128", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8"], customTypes: [], }, }, diff --git a/contracts/src/models/config.cairo b/contracts/src/models/config.cairo index 9f5bff3c3..f919b6040 100644 --- a/contracts/src/models/config.cairo +++ b/contracts/src/models/config.cairo @@ -474,3 +474,13 @@ pub struct HasClaimedStartingResources { config_id: ID, claimed: bool, } + + +#[derive(IntrospectPacked, Copy, Drop, Serde)] +#[dojo::model] +struct RealmLevelConfig { + #[key] + level: u8, + detached_resource_id: ID, + detached_resource_count: u8, +} diff --git a/contracts/src/models/owner.cairo b/contracts/src/models/owner.cairo index a5d9bd4f6..f97ea4674 100644 --- a/contracts/src/models/owner.cairo +++ b/contracts/src/models/owner.cairo @@ -71,6 +71,7 @@ mod tests { regions: 0, wonder: 0, order: 0, + level: 0 } ); diff --git a/contracts/src/models/realm.cairo b/contracts/src/models/realm.cairo index 1acbb6f85..766588302 100644 --- a/contracts/src/models/realm.cairo +++ b/contracts/src/models/realm.cairo @@ -21,15 +21,16 @@ pub struct Realm { regions: u8, wonder: u8, order: u8, + level: u8, } -trait RealmCustomTrait { - fn has_resource(self: Realm, resource_type: u8) -> bool; - fn assert_is_set(self: Realm); -} - +#[generate_trait] impl RealmCustomImpl of RealmCustomTrait { + fn max_level(self: Realm) -> u8 { + 3 + } + fn has_resource(self: Realm, resource_type: u8) -> bool { let mut resource_types: Span = unpack_resource_types(self.resource_types_packed, self.resource_types_count); let mut has_resource = false; diff --git a/contracts/src/systems/buildings/contracts.cairo b/contracts/src/systems/buildings/contracts.cairo index 40eebd3fc..08ae24be0 100644 --- a/contracts/src/systems/buildings/contracts.cairo +++ b/contracts/src/systems/buildings/contracts.cairo @@ -33,26 +33,37 @@ mod building_systems { building_category: BuildingCategory, produce_resource_type: Option, ) { - assert!(directions.len() <= 4, "cannot build on selected tile"); - let mut building_coord: Coord = BuildingCustomImpl::center(); - loop { - match directions.pop_front() { - Option::Some(direction) => { building_coord = building_coord.neighbor(*direction); }, - Option::None => { break; } - } - }; - + // ensure only realms can make buildings let realm: Realm = get!(world, entity_id, Realm); assert!(realm.realm_id != 0, "entity is not a realm"); + + // ensure caller owns the realm + get!(world, entity_id, EntityOwner).assert_caller_owner(world); + + // ensure buildings can't be made outside + // the range of what the realm level allows + let directions_count = directions.len(); + assert!(directions_count > 0, "building cant be made at the center"); + assert!(directions_count <= realm.max_level().into() + 1, "building outside of max bound"); + assert!(directions_count <= realm.level.into() + 1, "building outside of what realm level allows"); + + // ensure that the realm produces the resource if produce_resource_type.is_some() { let resource_type: u8 = produce_resource_type.unwrap(); let realm_produces_resource = realm.has_resource(resource_type); assert!(realm_produces_resource, "realm does not produce specified resource"); } - get!(world, entity_id, EntityOwner).assert_caller_owner(world); + // get the location of the building + let mut building_coord: Coord = BuildingCustomImpl::center(); + loop { + match directions.pop_front() { + Option::Some(direction) => { building_coord = building_coord.neighbor(*direction); }, + Option::None => { break; } + } + }; - // todo: check that entity is a realm + // create the building let building: Building = BuildingCustomImpl::create( world, entity_id, building_category, produce_resource_type, building_coord ); diff --git a/contracts/src/systems/config/contracts.cairo b/contracts/src/systems/config/contracts.cairo index 2111f6f37..501390c41 100644 --- a/contracts/src/systems/config/contracts.cairo +++ b/contracts/src/systems/config/contracts.cairo @@ -19,6 +19,11 @@ trait IRealmFreeMintConfig { fn set_mint_config(ref world: IWorldDispatcher, config_id: ID, resources: Span<(u8, u128)>); } +#[dojo::interface] +trait IRealmLevelConfig { + fn set_realm_level_config(ref world: IWorldDispatcher, level: u8, resources: Span<(u8, u128)>); +} + #[dojo::interface] trait IWeightConfig { fn set_weight_config(ref world: IWorldDispatcher, entity_type: ID, weight_gram: u128); @@ -149,7 +154,7 @@ mod config_systems { CapacityConfig, SpeedConfig, WeightConfig, WorldConfig, LevelingConfig, RealmFreeMintConfig, MapConfig, TickConfig, ProductionConfig, BankConfig, TroopConfig, BuildingConfig, BuildingCategoryPopConfig, PopulationConfig, HyperstructureResourceConfig, HyperstructureConfig, StaminaConfig, StaminaRefillConfig, - MercenariesConfig, BattleConfig, TravelStaminaCostConfig + MercenariesConfig, BattleConfig, TravelStaminaCostConfig, RealmLevelConfig }; use eternum::models::position::{Position, PositionCustomTrait, Coord}; @@ -570,4 +575,54 @@ mod config_systems { set!(world, (MercenariesConfig { config_id: WORLD_CONFIG_ID, troops, rewards })); } } + + #[abi(embed_v0)] + impl RealmLevelConfigCustomImpl of super::IRealmLevelConfig { + fn set_realm_level_config(ref world: IWorldDispatcher, level: u8, resources: Span<(u8, u128)>) { + // ensure only admin can set this + assert_caller_is_admin(world); + + // ensure level is between 1 and 3 + assert(level > 0 && level < 4, 'level must be between 1 and 3'); + + let detached_resource_id = world.uuid(); + let detached_resource_count = resources.len(); + let mut resources = resources; + let mut index = 0; + loop { + match resources.pop_front() { + Option::Some(( + resource_type, resource_amount + )) => { + let (resource_type, resource_amount) = (*resource_type, *resource_amount); + assert(resource_amount > 0, 'amount must not be 0'); + + set!( + world, + ( + DetachedResource { + entity_id: detached_resource_id, + index, + resource_type, + resource_amount: resource_amount + }, + ) + ); + + index += 1; + }, + Option::None => { break; } + }; + }; + + set!( + world, + (RealmLevelConfig { + level, + detached_resource_id: detached_resource_id.into(), + detached_resource_count: detached_resource_count.try_into().unwrap() + }) + ); + } + } } diff --git a/contracts/src/systems/realm/contracts.cairo b/contracts/src/systems/realm/contracts.cairo index 9298b7ac3..b480e7d77 100644 --- a/contracts/src/systems/realm/contracts.cairo +++ b/contracts/src/systems/realm/contracts.cairo @@ -16,6 +16,7 @@ trait IRealmSystems { order: u8, position: eternum::models::position::Position ) -> ID; + fn upgrade_level(ref world: IWorldDispatcher, realm_id: ID); fn mint_starting_resources(ref world: IWorldDispatcher, config_id: ID, entity_id: ID) -> ID; } @@ -30,17 +31,17 @@ mod realm_systems { use eternum::constants::REALM_ENTITY_TYPE; use eternum::constants::{WORLD_CONFIG_ID, REALM_FREE_MINT_CONFIG_ID, MAX_REALMS_PER_ADDRESS}; use eternum::models::capacity::{CapacityCategory}; - use eternum::models::config::{CapacityConfigCategory}; + use eternum::models::config::{CapacityConfigCategory, RealmLevelConfig}; use eternum::models::config::{RealmFreeMintConfig, HasClaimedStartingResources}; use eternum::models::event::{SettleRealmData, EventType}; use eternum::models::map::Tile; use eternum::models::metadata::EntityMetadata; use eternum::models::movable::Movable; use eternum::models::name::{AddressName}; - use eternum::models::owner::{Owner, EntityOwner}; + use eternum::models::owner::{Owner, EntityOwner, EntityOwnerCustomTrait}; use eternum::models::position::{Position, Coord}; use eternum::models::quantity::QuantityTracker; - use eternum::models::realm::{Realm, RealmCustomTrait}; + use eternum::models::realm::{Realm, RealmCustomTrait, RealmCustomImpl}; use eternum::models::resources::{DetachedResource, Resource, ResourceCustomImpl, ResourceCustomTrait}; use eternum::models::structure::{Structure, StructureCategory, StructureCount, StructureCountCustomTrait}; use eternum::systems::map::contracts::map_systems::InternalMapSystemsImpl; @@ -142,6 +143,7 @@ mod realm_systems { regions, wonder, order, + level: 0 }, Position { entity_id: entity_id.into(), x: position.x, y: position.y, }, EntityMetadata { entity_id: entity_id.into(), entity_type: REALM_ENTITY_TYPE, } @@ -178,5 +180,42 @@ mod realm_systems { entity_id.into() } + + + fn upgrade_level(ref world: IWorldDispatcher, realm_id: ID) { + // ensure caller owns the realm + get!(world, realm_id, EntityOwner).assert_caller_owner(world); + + // ensure entity is a realm + let mut realm = get!(world, realm_id, Realm); + realm.assert_is_set(); + + // ensure realm is not already at max level + assert(realm.level < realm.max_level(), 'realm is already at max level'); + + // make payment to upgrade to next level + let next_level = realm.level + 1; + let realm_level_config = get!(world, next_level, RealmLevelConfig); + let detached_resource_id = realm_level_config.detached_resource_id; + let detached_resource_count = realm_level_config.detached_resource_count; + let mut index = 0; + loop { + if index == detached_resource_count { + break; + } + + let mut detached_resource = get!(world, (detached_resource_id, index), DetachedResource); + + // burn resource from realm + let mut realm_resource = ResourceCustomImpl::get(world, (realm_id, detached_resource.resource_type)); + realm_resource.burn(detached_resource.resource_amount); + realm_resource.save(world); + index += 1; + }; + + // set new level + realm.level = next_level; + set!(world, (realm)); + } } } diff --git a/contracts/src/systems/transport/tests/travel_systems_tests.cairo b/contracts/src/systems/transport/tests/travel_systems_tests.cairo index 4f07ac431..44eb8ce8f 100644 --- a/contracts/src/systems/transport/tests/travel_systems_tests.cairo +++ b/contracts/src/systems/transport/tests/travel_systems_tests.cairo @@ -134,6 +134,7 @@ fn test_travel_with_realm_bonus() { regions: 0, wonder: 0, order: 0, + level: 0 }, Level { entity_id: realm_entity_id, level: LevelIndex::TRAVEL.into() + 4, valid_until: 10000000, }, LevelingConfig { @@ -208,6 +209,7 @@ fn test_travel_with_realm_and_order_bonus() { regions: 0, wonder: 0, order: realm_order_id.into(), + level: 0 }, Level { entity_id: realm_entity_id, level: LevelIndex::TRAVEL.into() + 4, valid_until: 10000000, }, LevelingConfig { From b8ccd2a32ef0144b386730ecb84c5140524b739b Mon Sep 17 00:00:00 2001 From: Credence Date: Fri, 27 Sep 2024 02:01:10 +0100 Subject: [PATCH 02/15] contracts: realm leveling --- contracts/src/models/config.cairo | 4 ++-- contracts/src/systems/config/contracts.cairo | 4 ++-- contracts/src/systems/realm/contracts.cairo | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/src/models/config.cairo b/contracts/src/models/config.cairo index f919b6040..045cec281 100644 --- a/contracts/src/models/config.cairo +++ b/contracts/src/models/config.cairo @@ -481,6 +481,6 @@ pub struct HasClaimedStartingResources { struct RealmLevelConfig { #[key] level: u8, - detached_resource_id: ID, - detached_resource_count: u8, + required_resources_id: ID, + required_resource_count: u8, } diff --git a/contracts/src/systems/config/contracts.cairo b/contracts/src/systems/config/contracts.cairo index 501390c41..63fbc74b2 100644 --- a/contracts/src/systems/config/contracts.cairo +++ b/contracts/src/systems/config/contracts.cairo @@ -619,8 +619,8 @@ mod config_systems { world, (RealmLevelConfig { level, - detached_resource_id: detached_resource_id.into(), - detached_resource_count: detached_resource_count.try_into().unwrap() + required_resources_id: detached_resource_id.into(), + required_resource_count: detached_resource_count.try_into().unwrap() }) ); } diff --git a/contracts/src/systems/realm/contracts.cairo b/contracts/src/systems/realm/contracts.cairo index b480e7d77..426b20125 100644 --- a/contracts/src/systems/realm/contracts.cairo +++ b/contracts/src/systems/realm/contracts.cairo @@ -196,18 +196,18 @@ mod realm_systems { // make payment to upgrade to next level let next_level = realm.level + 1; let realm_level_config = get!(world, next_level, RealmLevelConfig); - let detached_resource_id = realm_level_config.detached_resource_id; - let detached_resource_count = realm_level_config.detached_resource_count; + let required_resources_id = realm_level_config.required_resources_id; + let required_resource_count = realm_level_config.required_resource_count; let mut index = 0; loop { - if index == detached_resource_count { + if index == required_resource_count { break; } - let mut detached_resource = get!(world, (detached_resource_id, index), DetachedResource); + let mut required_resource = get!(world, (required_resources_id, index), DetachedResource); // burn resource from realm - let mut realm_resource = ResourceCustomImpl::get(world, (realm_id, detached_resource.resource_type)); + let mut realm_resource = ResourceCustomImpl::get(world, (realm_id, required_resource.resource_type)); realm_resource.burn(detached_resource.resource_amount); realm_resource.save(world); index += 1; From b9e473213ebda08a3c4e39d946e2e40ffbb456e5 Mon Sep 17 00:00:00 2001 From: Credence Date: Fri, 27 Sep 2024 02:03:09 +0100 Subject: [PATCH 03/15] contracts: realm leveling --- contracts/src/systems/config/contracts.cairo | 3 --- contracts/src/systems/realm/contracts.cairo | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/src/systems/config/contracts.cairo b/contracts/src/systems/config/contracts.cairo index 63fbc74b2..a6cef599c 100644 --- a/contracts/src/systems/config/contracts.cairo +++ b/contracts/src/systems/config/contracts.cairo @@ -582,9 +582,6 @@ mod config_systems { // ensure only admin can set this assert_caller_is_admin(world); - // ensure level is between 1 and 3 - assert(level > 0 && level < 4, 'level must be between 1 and 3'); - let detached_resource_id = world.uuid(); let detached_resource_count = resources.len(); let mut resources = resources; diff --git a/contracts/src/systems/realm/contracts.cairo b/contracts/src/systems/realm/contracts.cairo index 426b20125..89571b8f5 100644 --- a/contracts/src/systems/realm/contracts.cairo +++ b/contracts/src/systems/realm/contracts.cairo @@ -208,7 +208,7 @@ mod realm_systems { // burn resource from realm let mut realm_resource = ResourceCustomImpl::get(world, (realm_id, required_resource.resource_type)); - realm_resource.burn(detached_resource.resource_amount); + realm_resource.burn(required_resource.resource_amount); realm_resource.save(world); index += 1; }; From 210f327abb3b64a9f4595e93c54899b2675d9740 Mon Sep 17 00:00:00 2001 From: Credence Date: Fri, 27 Sep 2024 02:45:44 +0100 Subject: [PATCH 04/15] contracts: realm leveling --- contracts/src/systems/config/contracts.cairo | 39 ++++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/contracts/src/systems/config/contracts.cairo b/contracts/src/systems/config/contracts.cairo index a6cef599c..7291d73e9 100644 --- a/contracts/src/systems/config/contracts.cairo +++ b/contracts/src/systems/config/contracts.cairo @@ -578,38 +578,29 @@ mod config_systems { #[abi(embed_v0)] impl RealmLevelConfigCustomImpl of super::IRealmLevelConfig { - fn set_realm_level_config(ref world: IWorldDispatcher, level: u8, resources: Span<(u8, u128)>) { + fn set_realm_level_config(ref world: IWorldDispatcher, level: u8, mut resources: Span<(u8, u128)>) { // ensure only admin can set this assert_caller_is_admin(world); let detached_resource_id = world.uuid(); let detached_resource_count = resources.len(); - let mut resources = resources; let mut index = 0; - loop { - match resources.pop_front() { - Option::Some(( - resource_type, resource_amount - )) => { - let (resource_type, resource_amount) = (*resource_type, *resource_amount); - assert(resource_amount > 0, 'amount must not be 0'); + for ( + resource_type, resource_amount + ) in resources { + let (resource_type, resource_amount) = (*resource_type, *resource_amount); + assert(resource_amount > 0, 'amount must not be 0'); - set!( - world, - ( - DetachedResource { - entity_id: detached_resource_id, - index, - resource_type, - resource_amount: resource_amount - }, - ) - ); + set!( + world, + ( + DetachedResource { + entity_id: detached_resource_id, index, resource_type, resource_amount: resource_amount + }, + ) + ); - index += 1; - }, - Option::None => { break; } - }; + index += 1; }; set!( From 53a26e7b9c6b59ee182f3d2cd1aeac13b2c9dcdf Mon Sep 17 00:00:00 2001 From: Credence Date: Fri, 27 Sep 2024 02:45:52 +0100 Subject: [PATCH 05/15] contracts: realm leveling tests --- .../combat/tests/battle_pillage_test.cairo | 8 +- contracts/src/systems/realm/tests.cairo | 154 +++++++++++++++++- contracts/src/utils/testing/config.cairo | 13 +- contracts/src/utils/testing/world.cairo | 4 +- 4 files changed, 171 insertions(+), 8 deletions(-) diff --git a/contracts/src/systems/combat/tests/battle_pillage_test.cairo b/contracts/src/systems/combat/tests/battle_pillage_test.cairo index d7f23e30e..7f98044bc 100644 --- a/contracts/src/systems/combat/tests/battle_pillage_test.cairo +++ b/contracts/src/systems/combat/tests/battle_pillage_test.cairo @@ -125,9 +125,9 @@ fn test_battle_pillage__near_max_capacity() { starknet::testing::set_block_timestamp(DEFAULT_BLOCK_TIMESTAMP * 2); - let army_quantity = get!(world, attacker_realm_army_unit_id, Quantity); + let _army_quantity = get!(world, attacker_realm_army_unit_id, Quantity); - let capacity_config = get!(world, 3, CapacityConfig); + let _capacity_config = get!(world, 3, CapacityConfig); combat_system_dispatcher.battle_pillage(attacker_realm_army_unit_id, defender_realm_entity_id); @@ -152,9 +152,9 @@ fn test_simple_battle_pillage() { starknet::testing::set_block_timestamp(DEFAULT_BLOCK_TIMESTAMP * 2); - let army_quantity = get!(world, attacker_realm_army_unit_id, Quantity); + let _army_quantity = get!(world, attacker_realm_army_unit_id, Quantity); - let capacity_config = get!(world, 3, CapacityConfig); + let _capacity_config = get!(world, 3, CapacityConfig); combat_system_dispatcher.battle_pillage(attacker_realm_army_unit_id, defender_realm_entity_id); diff --git a/contracts/src/systems/realm/tests.cairo b/contracts/src/systems/realm/tests.cairo index ed2bfee08..a351b1a7f 100644 --- a/contracts/src/systems/realm/tests.cairo +++ b/contracts/src/systems/realm/tests.cairo @@ -7,7 +7,8 @@ use eternum::models::map::Tile; use eternum::models::owner::Owner; use eternum::models::position::{Position, Coord}; -use eternum::models::realm::Realm; +use eternum::models::realm::{Realm, RealmCustomTrait}; +use eternum::models::resources::DetachedResource; use eternum::models::resources::Resource; use eternum::systems::config::contracts::{ @@ -23,7 +24,7 @@ use eternum::utils::testing::{ spawn_realm, get_default_realm_pos, generate_realm_positions, spawn_hyperstructure, get_default_hyperstructure_coord }, - config::{set_combat_config, set_capacity_config} + config::{set_combat_config, set_capacity_config, set_realm_level_config} }; use starknet::contract_address_const; @@ -58,6 +59,8 @@ fn setup() -> (IWorldDispatcher, IRealmSystemsDispatcher) { set_capacity_config(config_systems_address); + set_realm_level_config(config_systems_address); + (world, realm_systems_dispatcher) } @@ -203,3 +206,150 @@ fn test_mint_starting_resources_as_not_realm() { realm_systems_dispatcher.mint_starting_resources(REALM_FREE_MINT_CONFIG_ID, hyperstructure_entity_id); } + +#[test] +#[available_gas(3000000000000)] +fn test_upgrade_level_success() { + let (world, realm_systems_dispatcher) = setup(); + + // Spawn a realm + let realm_entity_id = spawn_realm(world, realm_systems_dispatcher, get_default_realm_pos()); + + // Add required resources for upgrade + let required_resources = array![(ResourceTypes::WHEAT, 100), (ResourceTypes::WOOD, 100),]; + for (resource_type, amount) in required_resources + .span() { + let mut resource = get!(world, (realm_entity_id, *resource_type), Resource); + resource.balance += *amount; + set!(world, (resource)); + }; + + // Upgrade level + realm_systems_dispatcher.upgrade_level(realm_entity_id); + + // Check if level increased + let realm = get!(world, realm_entity_id, Realm); + assert(realm.level == 1, 'Realm level should be 1'); + + // Check if resources were consumed + for (resource_type, _amount) in required_resources + .span() { + let resource = get!(world, (realm_entity_id, *resource_type), Resource); + assert(resource.balance == 0, 'Resource should be consumed'); + } +} + +#[test] +#[available_gas(3000000000000)] +#[should_panic(expected: ('Caller is not the owner', 'ENTRYPOINT_FAILED'))] +fn test_upgrade_level_not_owner() { + let (world, realm_systems_dispatcher) = setup(); + + // Spawn a realm + let realm_entity_id = spawn_realm(world, realm_systems_dispatcher, get_default_realm_pos()); + + // Set a different caller + starknet::testing::set_contract_address(contract_address_const::<'not_owner'>()); + + // Attempt to upgrade level + realm_systems_dispatcher.upgrade_level(realm_entity_id); +} + +#[test] +#[available_gas(3000000000000)] +#[should_panic(expected: ('Entity is not a realm', 'ENTRYPOINT_FAILED'))] +fn test_upgrade_level_not_realm() { + let (_world, realm_systems_dispatcher) = setup(); + + // Use a non-existent entity ID + let non_realm_entity_id = 12345; + + // Attempt to upgrade level + realm_systems_dispatcher.upgrade_level(non_realm_entity_id); +} + +#[test] +#[available_gas(3000000000000)] +#[should_panic(expected: ('realm is already at max level', 'ENTRYPOINT_FAILED'))] +fn test_upgrade_level_max_level() { + let (world, realm_systems_dispatcher) = setup(); + + // Spawn a realm + let realm_entity_id = spawn_realm(world, realm_systems_dispatcher, get_default_realm_pos()); + + // Set realm to max level + let mut realm = get!(world, realm_entity_id, Realm); + realm.level = realm.max_level(); + set!(world, (realm)); + + // Attempt to upgrade level + realm_systems_dispatcher.upgrade_level(realm_entity_id); +} + +#[test] +#[available_gas(3000000000000)] +#[should_panic( + expected: ( + "not enough resources, Resource (entity id: 5, resource type: WHEAT, balance: 0). deduction: 100", + 'ENTRYPOINT_FAILED' + ) +)] +fn test_upgrade_level_insufficient_resources() { + let (world, realm_systems_dispatcher) = setup(); + + // Spawn a realm + let realm_entity_id = spawn_realm(world, realm_systems_dispatcher, get_default_realm_pos()); + + // Attempt to upgrade level without adding resources + realm_systems_dispatcher.upgrade_level(realm_entity_id); +} + +#[test] +#[available_gas(3000000000000)] +fn test_upgrade_level_multiple_times() { + let (world, realm_systems_dispatcher) = setup(); + + // Spawn a realm + let realm_entity_id = spawn_realm(world, realm_systems_dispatcher, get_default_realm_pos()); + + // Add more than enough resources for multiple upgrades + let required_resources = array![ + (ResourceTypes::WHEAT, 1000), + (ResourceTypes::WOOD, 1000), + (ResourceTypes::STONE, 1000), + (ResourceTypes::FISH, 1000), + (ResourceTypes::COAL, 1000), + (ResourceTypes::IRONWOOD, 1000), + ]; + for (resource_type, amount) in required_resources + .span() { + let mut resource = get!(world, (realm_entity_id, *resource_type), Resource); + resource.balance += *amount; + set!(world, (resource)); + }; + + // Upgrade level multiple times + realm_systems_dispatcher.upgrade_level(realm_entity_id); + realm_systems_dispatcher.upgrade_level(realm_entity_id); + realm_systems_dispatcher.upgrade_level(realm_entity_id); + + // Check if level increased correctly + let realm = get!(world, realm_entity_id, Realm); + assert(realm.level == 3, 'Realm level should be 3'); + + // Check if resources were consumed correctly + let resource_types = array![ + ResourceTypes::WHEAT, + ResourceTypes::WOOD, + ResourceTypes::STONE, + ResourceTypes::FISH, + ResourceTypes::COAL, + ResourceTypes::IRONWOOD + ]; + for resource_type in resource_types + .span() { + let resource = get!(world, (realm_entity_id, *resource_type), Resource); + assert!(resource.balance < 1000, "Resource should be partially consumed"); + assert!(resource.balance > 0, "Resource should not be fully consumed"); + } +} diff --git a/contracts/src/utils/testing/config.cairo b/contracts/src/utils/testing/config.cairo index 7dc82b89f..db6586d99 100644 --- a/contracts/src/utils/testing/config.cairo +++ b/contracts/src/utils/testing/config.cairo @@ -17,7 +17,7 @@ use eternum::systems::config::contracts::{ ITickConfigDispatcher, ITickConfigDispatcherTrait, IMapConfigDispatcher, IMapConfigDispatcherTrait, IWeightConfigDispatcher, IWeightConfigDispatcherTrait, IProductionConfigDispatcher, IProductionConfigDispatcherTrait, ITravelStaminaCostConfigDispatcher, ITravelStaminaCostConfigDispatcherTrait, - IBattleConfigDispatcher, IBattleConfigDispatcherTrait + IBattleConfigDispatcher, IBattleConfigDispatcherTrait, IRealmLevelConfigDispatcher, IRealmLevelConfigDispatcherTrait }; use eternum::utils::testing::constants::{ @@ -157,3 +157,14 @@ fn set_weight_config(config_systems_address: ContractAddress) { } } +fn set_realm_level_config(config_systems_address: ContractAddress) { + IRealmLevelConfigDispatcher { contract_address: config_systems_address } + .set_realm_level_config(1, array![(ResourceTypes::WHEAT, 100), (ResourceTypes::WOOD, 100),].span()); + + IRealmLevelConfigDispatcher { contract_address: config_systems_address } + .set_realm_level_config(2, array![(ResourceTypes::STONE, 200), (ResourceTypes::FISH, 200),].span()); + + IRealmLevelConfigDispatcher { contract_address: config_systems_address } + .set_realm_level_config(3, array![(ResourceTypes::COAL, 300), (ResourceTypes::IRONWOOD, 300),].span()); +} + diff --git a/contracts/src/utils/testing/world.cairo b/contracts/src/utils/testing/world.cairo index b257d4e43..af3706705 100644 --- a/contracts/src/utils/testing/world.cairo +++ b/contracts/src/utils/testing/world.cairo @@ -16,7 +16,8 @@ use eternum::models::config::{ world_config, speed_config, capacity_config, weight_config, hyperstructure_resource_config, stamina_config, stamina_refill_config, tick_config, map_config, realm_free_mint_config, mercenaries_config, leveling_config, production_config, bank_config, building_config, troop_config, battle_config, building_category_pop_config, - population_config, has_claimed_starting_resources, hyperstructure_config, travel_stamina_cost_config + population_config, has_claimed_starting_resources, hyperstructure_config, travel_stamina_cost_config, + realm_level_config }; use eternum::models::guild::{guild, guild_member, guild_whitelist}; use eternum::models::hyperstructure::{ @@ -124,6 +125,7 @@ fn spawn_eternum() -> IWorldDispatcher { capacity_category::TEST_CLASS_HASH, production_deadline::TEST_CLASS_HASH, travel_stamina_cost_config::TEST_CLASS_HASH, + realm_level_config::TEST_CLASS_HASH, ]; let world = spawn_test_world(["eternum"].span(), models.span()); From 5fff80f298db9509584d8804a91e27307cfde4a0 Mon Sep 17 00:00:00 2001 From: Credence Date: Fri, 27 Sep 2024 02:54:22 +0100 Subject: [PATCH 06/15] contracts: realm leveling update --- contracts/scripts/system_models.json | 3 ++- contracts/src/systems/realm/tests.cairo | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/scripts/system_models.json b/contracts/scripts/system_models.json index 79ce9ac95..c12efc243 100644 --- a/contracts/scripts/system_models.json +++ b/contracts/scripts/system_models.json @@ -25,7 +25,8 @@ "ProductionOutput", "DetachedResource", "ResourceCost", - "TravelStaminaCostConfig" + "TravelStaminaCostConfig", + "RealmLevelConfig" ], "DEV_RESOURCE_SYSTEMS": ["Resource", "OwnedResourcesTracker", "Production"], "DEV_BANK_SYSTEMS": ["Tile", "Position", "Owner", "Bank", "Structure", "StructureCount", "CapacityCategory"], diff --git a/contracts/src/systems/realm/tests.cairo b/contracts/src/systems/realm/tests.cairo index a351b1a7f..295801de4 100644 --- a/contracts/src/systems/realm/tests.cairo +++ b/contracts/src/systems/realm/tests.cairo @@ -241,7 +241,7 @@ fn test_upgrade_level_success() { #[test] #[available_gas(3000000000000)] -#[should_panic(expected: ('Caller is not the owner', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('Not Owner', 'ENTRYPOINT_FAILED'))] fn test_upgrade_level_not_owner() { let (world, realm_systems_dispatcher) = setup(); From 50bff2b65d98eeb075d6468bcc3ef76d6d053e83 Mon Sep 17 00:00:00 2001 From: Credence Date: Fri, 27 Sep 2024 03:17:58 +0100 Subject: [PATCH 07/15] client config: realm leveling --- client/src/dojo/createSystemCalls.ts | 5 ++++ .../__tests__/__BattleManagerMock__.ts | 1 + config/index.ts | 2 ++ sdk/packages/eternum/src/config/index.ts | 24 +++++++++++++++ .../eternum/src/constants/resources.ts | 15 ++++++++++ sdk/packages/eternum/src/provider/index.ts | 29 +++++++++++++++++++ sdk/packages/eternum/src/types/provider.ts | 10 +++++++ sdk/packages/eternum/src/utils/index.ts | 6 ++++ 8 files changed, 92 insertions(+) diff --git a/client/src/dojo/createSystemCalls.ts b/client/src/dojo/createSystemCalls.ts index febe84818..037393c06 100644 --- a/client/src/dojo/createSystemCalls.ts +++ b/client/src/dojo/createSystemCalls.ts @@ -108,6 +108,10 @@ export function createSystemCalls({ provider }: SetupNetworkResult) { await provider.create_realm(props); }; + const upgrade_realm = async (props: SystemProps.UpgradeRealmProps) => { + await provider.upgrade_realm(props); + }; + const create_multiple_realms = async (props: SystemProps.CreateMultipleRealmsProps) => { await provider.create_multiple_realms(props); }; @@ -321,6 +325,7 @@ export function createSystemCalls({ provider }: SetupNetworkResult) { cancel_order: withQueueing(withErrorHandling(cancel_order)), accept_partial_order: withQueueing(withErrorHandling(accept_partial_order)), create_realm: withQueueing(withErrorHandling(create_realm)), + upgrade_realm: withQueueing(withErrorHandling(upgrade_realm)), create_multiple_realms: withQueueing(withErrorHandling(create_multiple_realms)), transfer_resources: withQueueing(withErrorHandling(transfer_resources)), travel: withQueueing(withErrorHandling(travel)), diff --git a/client/src/dojo/modelManager/__tests__/__BattleManagerMock__.ts b/client/src/dojo/modelManager/__tests__/__BattleManagerMock__.ts index 1ada96054..51d05ca1c 100644 --- a/client/src/dojo/modelManager/__tests__/__BattleManagerMock__.ts +++ b/client/src/dojo/modelManager/__tests__/__BattleManagerMock__.ts @@ -124,6 +124,7 @@ export const generateMockArmyInfo = ( regions: 1, wonder: 1, order: 1, + level: 1, }, homePosition: { entity_id: ARMY_ENTITY_ID, x: 0, y: 0 }, }; diff --git a/config/index.ts b/config/index.ts index 6f6013e1a..d31a060c7 100644 --- a/config/index.ts +++ b/config/index.ts @@ -12,6 +12,7 @@ import { setMercenariesConfig, setPopulationConfig, setProductionConfig, + setRealmLevelConfig, setResourceBuildingConfig, setSpeedConfig, setStaminaConfig, @@ -56,6 +57,7 @@ console.log("Account set up"); await setBuildingCategoryPopConfig(account, provider); await setPopulationConfig(account, provider); await setBuildingConfig(account, provider); +await setRealmLevelConfig(account, provider); await setResourceBuildingConfig(account, provider); await setWeightConfig(account, provider); await setCombatConfig(account, provider); diff --git a/sdk/packages/eternum/src/config/index.ts b/sdk/packages/eternum/src/config/index.ts index 7441d528e..38b0b178b 100644 --- a/sdk/packages/eternum/src/config/index.ts +++ b/sdk/packages/eternum/src/config/index.ts @@ -19,6 +19,7 @@ import { TickIds, TravelTypes } from "../types"; import { BUILDING_COSTS_SCALED, HYPERSTRUCTURE_TOTAL_COSTS_SCALED, + REALM_LEVEL_COSTS_SCALED, RESOURCE_BUILDING_COSTS_SCALED, RESOURCE_INPUTS_SCALED, RESOURCE_OUTPUTS_SCALED, @@ -106,6 +107,29 @@ export const setBuildingConfig = async (account: Account, provider: EternumProvi console.log(`Configuring building cost config ${tx.statusReceipt}...`); }; +export const setRealmLevelConfig = async (account: Account, provider: EternumProvider) => { + const calldataArray = []; + + for (const level of Object.keys(REALM_LEVEL_COSTS_SCALED) as unknown as number[]) { + if (REALM_LEVEL_COSTS_SCALED[level].length !== 0) { + const calldata = { + level, + cost_of_level: REALM_LEVEL_COSTS_SCALED[level].map((cost) => { + return { + ...cost, + amount: cost.amount * EternumGlobalConfig.resources.resourcePrecision, + }; + }), + }; + + calldataArray.push(calldata); + } + } + + const tx = await provider.set_realm_level_config({ signer: account, calls: calldataArray }); + console.log(`Configuring realm level cost config ${tx.statusReceipt}...`); +}; + export const setResourceBuildingConfig = async (account: Account, provider: EternumProvider) => { const calldataArray = []; diff --git a/sdk/packages/eternum/src/constants/resources.ts b/sdk/packages/eternum/src/constants/resources.ts index 70389b1a5..4687baedf 100644 --- a/sdk/packages/eternum/src/constants/resources.ts +++ b/sdk/packages/eternum/src/constants/resources.ts @@ -642,6 +642,21 @@ export const RESOURCE_INPUTS: ResourceInputs = { [ResourcesIds.Earthenshard]: [], }; +export const REALM_LEVEL_COSTS: ResourceInputs = { + [1]: [ + { resource: ResourcesIds.Wheat, amount: 500 }, + { resource: ResourcesIds.Fish, amount: 500 }, + ], + [2]: [ + { resource: ResourcesIds.Wheat, amount: 1000 }, + { resource: ResourcesIds.Fish, amount: 1000 }, + ], + [3]: [ + { resource: ResourcesIds.Wheat, amount: 2000 }, + { resource: ResourcesIds.Fish, amount: 2000 }, + ], +}; + export const BUILDING_COSTS: ResourceInputs = { [BuildingType.Castle]: [], [BuildingType.Bank]: [], diff --git a/sdk/packages/eternum/src/provider/index.ts b/sdk/packages/eternum/src/provider/index.ts index 133755bca..95a7708d1 100644 --- a/sdk/packages/eternum/src/provider/index.ts +++ b/sdk/packages/eternum/src/provider/index.ts @@ -211,6 +211,16 @@ export class EternumProvider extends EnhancedDojoProvider { return await this.waitForTransactionWithCheck(tx.transaction_hash); } + public async upgrade_realm(props: SystemProps.UpgradeRealmProps) { + const { realm_entity_id, signer } = props; + + return await this.executeAndCheckTransaction(signer, { + contractAddress: getContractByName(this.manifest, `${NAMESPACE}-realm_systems`), + entrypoint: "upgrade_level", + calldata: [realm_entity_id], + }); + } + create_multiple_realms = async (props: SystemProps.CreateMultipleRealmsProps) => { let { realms, signer } = props; @@ -888,6 +898,25 @@ export class EternumProvider extends EnhancedDojoProvider { }); } + public async set_realm_level_config(props: SystemProps.SetRealmLevelConfigProps) { + const { calls, signer } = props; + + return await this.executeAndCheckTransaction( + signer, + calls.map((call) => { + return { + contractAddress: getContractByName(this.manifest, `${NAMESPACE}-config_systems`), + entrypoint: "set_realm_level_config", + calldata: [ + call.level, + call.cost_of_level.length, + ...call.cost_of_level.flatMap(({ resource, amount }) => [resource, amount]), + ], + }; + }), + ); + } + public async set_building_config(props: SystemProps.SetBuildingConfigProps) { const { calls, signer } = props; diff --git a/sdk/packages/eternum/src/types/provider.ts b/sdk/packages/eternum/src/types/provider.ts index dcede8bd6..85f61cdca 100644 --- a/sdk/packages/eternum/src/types/provider.ts +++ b/sdk/packages/eternum/src/types/provider.ts @@ -161,6 +161,9 @@ export interface CreateMultipleRealmsProps extends SystemSigner { } export interface CreateRealmProps extends Realm, SystemSigner {} +export interface UpgradeRealmProps extends SystemSigner { + realm_entity_id: num.BigNumberish; +} export interface TransferItemsProps extends SystemSigner { sender_id: num.BigNumberish; @@ -456,6 +459,13 @@ export interface SetBuildingConfigProps extends SystemSigner { }[]; } +export interface SetRealmLevelConfigProps extends SystemSigner { + calls: { + level: num.BigNumberish; + cost_of_level: ResourceCosts[]; + }[]; +} + export interface SetWorldConfigProps extends SystemSigner { admin_address: num.BigNumberish; realm_l2_contract: num.BigNumberish; diff --git a/sdk/packages/eternum/src/utils/index.ts b/sdk/packages/eternum/src/utils/index.ts index 3c0bffb28..8257d26c9 100644 --- a/sdk/packages/eternum/src/utils/index.ts +++ b/sdk/packages/eternum/src/utils/index.ts @@ -6,6 +6,7 @@ import { HYPERSTRUCTURE_CREATION_COSTS, HYPERSTRUCTURE_TOTAL_COSTS, QUEST_RESOURCES, + REALM_LEVEL_COSTS, RESOURCE_BUILDING_COSTS, RESOURCE_INPUTS, RESOURCE_OUTPUTS, @@ -104,6 +105,11 @@ export const BUILDING_COSTS_SCALED: ResourceInputs = scaleResourceInputs( EternumGlobalConfig.resources.resourceMultiplier, ); +export const REALM_LEVEL_COSTS_SCALED: ResourceInputs = scaleResourceInputs( + REALM_LEVEL_COSTS, + EternumGlobalConfig.resources.resourceMultiplier, +); + export const RESOURCE_INPUTS_SCALED: ResourceInputs = scaleResourceInputs( RESOURCE_INPUTS, EternumGlobalConfig.resources.resourceMultiplier, From 4fb9af116d8337b6336996ebf56b9bec68972de1 Mon Sep 17 00:00:00 2001 From: Credence Date: Tue, 8 Oct 2024 07:45:02 +0100 Subject: [PATCH 08/15] Merge branch 'rc' of https://github.com/BibliothecaDAO/eternum into realm-level --- .github/workflows/discord-bot.yml | 2 + .github/workflows/knip.yml | 2 + .github/workflows/lint.yml | 2 + .github/workflows/test-client.yml | 2 + .github/workflows/test-contracts.yml | 2 + .knip.json | 3 +- balancing/.gitignore | 24 + balancing/README.md | 53 + balancing/components.json | 20 + balancing/eslint.config.js | 25 + balancing/index.html | 13 + balancing/package.json | 42 + balancing/postcss.config.js | 6 + balancing/public/vite.svg | 1 + balancing/src/App.css | 0 balancing/src/App.tsx | 30 + balancing/src/assets/react.svg | 1 + .../src/components/modules/building-table.tsx | 91 + .../src/components/modules/resource-table.tsx | 122 + balancing/src/components/ui/badge.tsx | 29 + balancing/src/components/ui/table.tsx | 76 + balancing/src/components/ui/tabs.tsx | 53 + balancing/src/index.css | 66 + balancing/src/lib/utils.ts | 6 + balancing/src/main.tsx | 10 + balancing/src/vite-env.d.ts | 1 + balancing/tailwind.config.js | 57 + balancing/tsconfig.app.json | 24 + balancing/tsconfig.json | 10 + balancing/tsconfig.node.json | 26 + balancing/vite.config.ts | 13 + client/package.json | 1 + client/src/assets/icons/Collapse Left.svg | 3 + client/src/assets/icons/Combat.svg | 3 + client/src/assets/icons/Compass.svg | 4 + client/src/assets/icons/Controller.svg | 5 + client/src/assets/icons/Crown.svg | 3 + client/src/assets/icons/Helmet.svg | 3 + client/src/assets/icons/Info.svg | 3 + client/src/assets/icons/Swap.svg | 3 + client/src/assets/icons/Times.svg | 3 + client/src/assets/icons/Wedge Down.svg | 3 + client/src/assets/icons/common/cross.svg | 5 +- client/src/assets/icons/common/message.svg | 1 + client/src/assets/icons/common/minimize.svg | 1 + client/src/dojo/contractComponents.ts | 70 +- client/src/dojo/createSystemCalls.ts | 32 +- .../dojo/modelManager/ArmyMovementManager.ts | 10 +- client/src/dojo/modelManager/BattleManager.ts | 11 +- .../dojo/modelManager/LeaderboardManager.ts | 9 - .../src/dojo/modelManager/StaminaManager.ts | 6 +- .../__tests__/BattleManager.test.ts | 10 +- .../__tests__/LeaderboardManager.test.ts | 719 +- .../utils/LeaderboardUtils.test.ts | 41 +- .../modelManager/utils/LeaderboardUtils.ts | 6 +- client/src/hooks/helpers/useContributions.tsx | 20 + client/src/hooks/helpers/useEntities.tsx | 254 +- client/src/hooks/helpers/useGetAllPlayers.tsx | 52 + client/src/hooks/helpers/useGuilds.tsx | 31 +- .../src/hooks/helpers/useHyperstructures.tsx | 70 +- client/src/hooks/helpers/useRealm.tsx | 83 +- client/src/hooks/helpers/useResources.tsx | 7 +- .../hooks/helpers/useStructureEntityId.tsx | 2 +- client/src/hooks/helpers/useStructures.tsx | 3 +- client/src/hooks/store/useUIStore.tsx | 24 +- client/src/index.css | 2 +- client/src/three/GameRenderer.ts | 2 +- client/src/three/SceneManager.ts | 4 + client/src/three/components/ArmyManager.ts | 4 + client/src/three/components/Minimap.ts | 384 + client/src/three/scenes/HexagonScene.ts | 1 + client/src/three/scenes/Hexception.ts | 2 + client/src/three/scenes/Worldmap.ts | 24 +- client/src/three/systems/SystemManager.ts | 10 +- client/src/ui/components/bank/ResourceBar.tsx | 117 +- client/src/ui/components/bank/Swap.tsx | 81 +- .../construction/SelectPreviewBuilding.tsx | 229 +- client/src/ui/components/entities/Entity.tsx | 25 +- .../entities/SelectLocationPanel.tsx | 1 - client/src/ui/components/hints/Buildings.tsx | 12 +- client/src/ui/components/hints/Resources.tsx | 46 +- client/src/ui/components/hints/TheMap.tsx | 57 +- .../components/hyperstructures/CoOwners.tsx | 18 +- .../hyperstructures/HyperstructurePanel.tsx | 44 +- .../hyperstructures/Leaderboard.tsx | 13 +- .../hyperstructures/ResourceExchange.tsx | 190 + .../hyperstructures/StructureCard.tsx | 47 +- .../src/ui/components/military/ArmyChip.tsx | 6 +- .../src/ui/components/military/ArmyList.tsx | 2 +- .../military/ArmyManagementCard.tsx | 15 +- .../components/military/EntitiesArmyTable.tsx | 2 +- .../src/ui/components/navigation/Config.tsx | 5 +- client/src/ui/components/quest/QuestList.tsx | 7 +- .../components/resources/DepositResources.tsx | 2 +- .../resources/EntityResourceTable.tsx | 23 +- .../components/resources/RealmResourcesIO.tsx | 36 +- .../ui/components/resources/ResourceChip.tsx | 25 +- .../components/shardMines/ShardMinePanel.tsx | 2 - .../ui/components/toggle/ToggleComponent.tsx | 50 +- .../src/ui/components/trading/MarketModal.tsx | 39 +- .../components/trading/MarketOrderPanel.tsx | 115 +- .../ui/components/trading/RealmProduction.tsx | 70 + .../trading/SelectEntityFromList.tsx | 47 + .../ui/components/trading/SelectResources.tsx | 104 + .../trading/TransferBetweenEntities.tsx | 254 +- .../ui/components/trading/TransferView.tsx | 55 + .../ui/components/worldmap/guilds/Guilds.tsx | 2 - .../worldmap/guilds/GuildsPanel.tsx | 2 +- .../ui/components/worldmap/guilds/MyGuild.tsx | 8 +- .../components/worldmap/guilds/Whitelist.tsx | 1 - .../worldmap/players/PlayersPanel.tsx | 82 + .../worldmap/structures/StructureListItem.tsx | 2 +- client/src/ui/config.tsx | 43 +- client/src/ui/containers/BaseContainer.tsx | 2 +- .../src/ui/containers/BottomLeftContainer.tsx | 9 - .../ui/containers/BottomRightContainer.tsx | 2 +- .../src/ui/containers/TopMiddleContainer.tsx | 5 - client/src/ui/elements/AddressSelect.tsx | 3 +- client/src/ui/elements/CircleButton.tsx | 76 +- client/src/ui/elements/DojoHtml.tsx | 2 +- client/src/ui/elements/Headline.tsx | 7 +- client/src/ui/elements/ListSelect.tsx | 4 +- client/src/ui/elements/NumberInput.tsx | 113 +- client/src/ui/elements/SelectResource.tsx | 102 +- client/src/ui/elements/Tabs.tsx | 52 + client/src/ui/elements/TextArea.tsx | 21 - client/src/ui/elements/TextInput.tsx | 52 +- client/src/ui/elements/Tooltip.tsx | 2 +- client/src/ui/elements/tab/tabs.tsx | 6 +- client/src/ui/layouts/World.tsx | 24 +- client/src/ui/modules/assistant/Assistant.tsx | 115 - client/src/ui/modules/chat/Chat.tsx | 372 +- client/src/ui/modules/chat/InputField.tsx | 160 + .../entity-details/BuildingEntityDetails.tsx | 34 +- client/src/ui/modules/guilds/Guilds.tsx | 15 - .../modules/military/battle-view/Battle.tsx | 8 +- .../military/battle-view/BattleActions.tsx | 8 +- .../military/battle-view/BattleSideView.tsx | 43 +- .../modules/navigation/BottomNavigation.tsx | 123 +- .../navigation/LeftNavigationModule.tsx | 140 +- .../navigation/RightNavigationModule.tsx | 95 +- .../modules/navigation/SecondaryMenuItems.tsx | 128 + .../modules/navigation/TopLeftNavigation.tsx | 37 - .../navigation/TopMiddleNavigation.tsx | 229 +- .../modules/navigation/TopRightNavigation.tsx | 67 + client/src/ui/modules/onboarding/Steps.tsx | 20 +- client/src/ui/modules/social/PlayerId.tsx | 167 + client/src/ui/modules/social/Social.tsx | 52 + client/src/ui/modules/stream/EventStream.tsx | 70 +- client/src/ui/utils/realms.tsx | 14 +- config/bank/index.ts | 25 +- config/index.ts | 41 +- contracts/.gitignore | 5 +- .../manifests/dev/deployment/manifest.json | 6268 +++++++++++------ .../manifests/dev/deployment/manifest.toml | 246 +- contracts/scripts/system_models.json | 8 +- contracts/src/constants.cairo | 29 + contracts/src/models/buildings.cairo | 53 +- contracts/src/models/config.cairo | 20 + contracts/src/models/event.cairo | 1 + contracts/src/models/hyperstructure.cairo | 55 +- contracts/src/models/realm.cairo | 7 +- .../src/systems/bank/contracts/bank.cairo | 5 + .../systems/bank/contracts/liquidity.cairo | 5 + .../src/systems/bank/contracts/swap.cairo | 5 + .../src/systems/buildings/contracts.cairo | 26 +- contracts/src/systems/combat/contracts.cairo | 24 + contracts/src/systems/config/contracts.cairo | 45 +- contracts/src/systems/guild/contracts.cairo | 16 + .../systems/hyperstructure/contracts.cairo | 290 +- .../src/systems/hyperstructure/tests.cairo | 218 +- .../src/systems/leveling/contracts.cairo | 5 + contracts/src/systems/map/contracts.cairo | 5 + contracts/src/systems/name/contracts.cairo | 6 + contracts/src/systems/realm/contracts.cairo | 13 +- contracts/src/systems/realm/tests.cairo | 2 +- .../src/systems/resources/contracts.cairo | 8 + .../trade/contracts/trade_systems.cairo | 10 + .../transport/contracts/travel_systems.cairo | 7 + contracts/src/utils/testing/world.cairo | 8 +- pnpm-lock.yaml | 671 +- pnpm-workspace.yaml | 1 + sdk/packages/eternum/src/config/index.ts | 344 +- .../eternum/src/constants/buildings.ts | 56 +- sdk/packages/eternum/src/constants/global.ts | 10 +- sdk/packages/eternum/src/constants/hex.ts | 6 +- .../eternum/src/constants/hyperstructure.ts | 65 + sdk/packages/eternum/src/constants/index.ts | 37 + sdk/packages/eternum/src/constants/orders.ts | 2 +- sdk/packages/eternum/src/constants/quests.ts | 83 + .../eternum/src/constants/realmLevels.ts | 37 + .../eternum/src/constants/resources.ts | 491 +- .../eternum/src/constants/structures.ts | 63 +- sdk/packages/eternum/src/constants/troops.ts | 4 +- sdk/packages/eternum/src/constants/utils.ts | 15 + sdk/packages/eternum/src/provider/index.ts | 72 +- sdk/packages/eternum/src/types/common.ts | 98 +- sdk/packages/eternum/src/types/provider.ts | 29 +- sdk/packages/eternum/src/utils/index.ts | 21 +- 199 files changed, 11417 insertions(+), 5148 deletions(-) create mode 100644 balancing/.gitignore create mode 100644 balancing/README.md create mode 100644 balancing/components.json create mode 100644 balancing/eslint.config.js create mode 100644 balancing/index.html create mode 100644 balancing/package.json create mode 100644 balancing/postcss.config.js create mode 100644 balancing/public/vite.svg create mode 100644 balancing/src/App.css create mode 100644 balancing/src/App.tsx create mode 100644 balancing/src/assets/react.svg create mode 100644 balancing/src/components/modules/building-table.tsx create mode 100644 balancing/src/components/modules/resource-table.tsx create mode 100644 balancing/src/components/ui/badge.tsx create mode 100644 balancing/src/components/ui/table.tsx create mode 100644 balancing/src/components/ui/tabs.tsx create mode 100644 balancing/src/index.css create mode 100644 balancing/src/lib/utils.ts create mode 100644 balancing/src/main.tsx create mode 100644 balancing/src/vite-env.d.ts create mode 100644 balancing/tailwind.config.js create mode 100644 balancing/tsconfig.app.json create mode 100644 balancing/tsconfig.json create mode 100644 balancing/tsconfig.node.json create mode 100644 balancing/vite.config.ts create mode 100644 client/src/assets/icons/Collapse Left.svg create mode 100644 client/src/assets/icons/Combat.svg create mode 100644 client/src/assets/icons/Compass.svg create mode 100644 client/src/assets/icons/Controller.svg create mode 100644 client/src/assets/icons/Crown.svg create mode 100644 client/src/assets/icons/Helmet.svg create mode 100644 client/src/assets/icons/Info.svg create mode 100644 client/src/assets/icons/Swap.svg create mode 100644 client/src/assets/icons/Times.svg create mode 100644 client/src/assets/icons/Wedge Down.svg create mode 100644 client/src/assets/icons/common/message.svg create mode 100644 client/src/assets/icons/common/minimize.svg create mode 100644 client/src/hooks/helpers/useGetAllPlayers.tsx create mode 100644 client/src/three/components/Minimap.ts create mode 100644 client/src/ui/components/hyperstructures/ResourceExchange.tsx create mode 100644 client/src/ui/components/trading/RealmProduction.tsx create mode 100644 client/src/ui/components/trading/SelectEntityFromList.tsx create mode 100644 client/src/ui/components/trading/SelectResources.tsx create mode 100644 client/src/ui/components/trading/TransferView.tsx create mode 100644 client/src/ui/components/worldmap/players/PlayersPanel.tsx delete mode 100644 client/src/ui/containers/BottomLeftContainer.tsx delete mode 100644 client/src/ui/containers/TopMiddleContainer.tsx create mode 100644 client/src/ui/elements/Tabs.tsx delete mode 100644 client/src/ui/elements/TextArea.tsx delete mode 100644 client/src/ui/modules/assistant/Assistant.tsx create mode 100644 client/src/ui/modules/chat/InputField.tsx delete mode 100644 client/src/ui/modules/guilds/Guilds.tsx create mode 100644 client/src/ui/modules/navigation/SecondaryMenuItems.tsx delete mode 100644 client/src/ui/modules/navigation/TopLeftNavigation.tsx create mode 100644 client/src/ui/modules/navigation/TopRightNavigation.tsx create mode 100644 client/src/ui/modules/social/PlayerId.tsx create mode 100644 client/src/ui/modules/social/Social.tsx create mode 100644 sdk/packages/eternum/src/constants/hyperstructure.ts create mode 100644 sdk/packages/eternum/src/constants/quests.ts create mode 100644 sdk/packages/eternum/src/constants/realmLevels.ts create mode 100644 sdk/packages/eternum/src/constants/utils.ts diff --git a/.github/workflows/discord-bot.yml b/.github/workflows/discord-bot.yml index 801313cf1..d732cda5c 100644 --- a/.github/workflows/discord-bot.yml +++ b/.github/workflows/discord-bot.yml @@ -4,6 +4,8 @@ on: pull_request: paths-ignore: - "contracts/**" + - "config/**" + - "client/**" - "**/manifest.json" - ".github/**" - "pnpm-lock.yaml" diff --git a/.github/workflows/knip.yml b/.github/workflows/knip.yml index a280f388a..10ff090a4 100644 --- a/.github/workflows/knip.yml +++ b/.github/workflows/knip.yml @@ -5,6 +5,8 @@ on: paths-ignore: - "contracts/**" - "**/manifest.json" + - "discord-bot/**" + - "config/**" - ".github/**" - "pnpm-lock.yaml" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2070d500f..ff631612b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,6 +5,8 @@ on: paths-ignore: - "contracts/**" - "**/manifest.json" + - "discord-bot/**" + - "config/**" - ".github/**" - "pnpm-lock.yaml" diff --git a/.github/workflows/test-client.yml b/.github/workflows/test-client.yml index e07dd5f50..ce4e0e45b 100644 --- a/.github/workflows/test-client.yml +++ b/.github/workflows/test-client.yml @@ -5,6 +5,8 @@ on: paths-ignore: - "contracts/**" - "**/manifest.json" + - "discord-bot/**" + - "config/**" - ".github/**" - "pnpm-lock.yaml" diff --git a/.github/workflows/test-contracts.yml b/.github/workflows/test-contracts.yml index 30bb56246..a83880ca6 100644 --- a/.github/workflows/test-contracts.yml +++ b/.github/workflows/test-contracts.yml @@ -6,6 +6,8 @@ on: - "client/**" - "sdk/**" - "**/manifest.json" + - "discord-bot/**" + - "config/**" - ".github/**" - "pnpm-lock.yaml" diff --git a/.knip.json b/.knip.json index d5fded339..2251dc7db 100644 --- a/.knip.json +++ b/.knip.json @@ -12,6 +12,7 @@ "**/**__tests__**/**", "client/src/three/components/FogManager.ts", - "client/src/hooks/useUISound.tsx" + "client/src/hooks/useUISound.tsx", + "balancing/**" ] } diff --git a/balancing/.gitignore b/balancing/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/balancing/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/balancing/README.md b/balancing/README.md new file mode 100644 index 000000000..fe2f688b5 --- /dev/null +++ b/balancing/README.md @@ -0,0 +1,53 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses + [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast + Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ["./tsconfig.node.json", "./tsconfig.app.json"], + tsconfigRootDir: import.meta.dirname, + }, + }, +}); +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or + `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from "eslint-plugin-react"; + +export default tseslint.config({ + // Set the react version + settings: { react: { version: "18.3" } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs["jsx-runtime"].rules, + }, +}); +``` diff --git a/balancing/components.json b/balancing/components.json new file mode 100644 index 000000000..aa38200a5 --- /dev/null +++ b/balancing/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/balancing/eslint.config.js b/balancing/eslint.config.js new file mode 100644 index 000000000..5b0cd9c72 --- /dev/null +++ b/balancing/eslint.config.js @@ -0,0 +1,25 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + }, + }, +); diff --git a/balancing/index.html b/balancing/index.html new file mode 100644 index 000000000..e4b78eae1 --- /dev/null +++ b/balancing/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/balancing/package.json b/balancing/package.json new file mode 100644 index 000000000..23c27989a --- /dev/null +++ b/balancing/package.json @@ -0,0 +1,42 @@ +{ + "name": "balancing", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@bibliothecadao/eternum": "workspace:^", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-tabs": "^1.1.0", + "@tanstack/react-table": "^8.20.5", + "class-variance-authority": "^0.7.0", + "clsx": "^1.2.1", + "lucide-react": "^0.365.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@types/node": "^20.11.10", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.18", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1" + } +} diff --git a/balancing/postcss.config.js b/balancing/postcss.config.js new file mode 100644 index 000000000..2aa7205d4 --- /dev/null +++ b/balancing/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/balancing/public/vite.svg b/balancing/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/balancing/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/balancing/src/App.css b/balancing/src/App.css new file mode 100644 index 000000000..e69de29bb diff --git a/balancing/src/App.tsx b/balancing/src/App.tsx new file mode 100644 index 000000000..e826bc641 --- /dev/null +++ b/balancing/src/App.tsx @@ -0,0 +1,30 @@ +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { resources } from "@bibliothecadao/eternum"; +import "./App.css"; +import { BuildingTable } from "./components/modules/building-table"; +import { ResourceTable } from "./components/modules/resource-table"; + +function App() { + return ( + <> +
+ + + Production + Buildings + + + + + + + + + + +
+ + ); +} + +export default App; diff --git a/balancing/src/assets/react.svg b/balancing/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/balancing/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/balancing/src/components/modules/building-table.tsx b/balancing/src/components/modules/building-table.tsx new file mode 100644 index 000000000..9ec3fb2b5 --- /dev/null +++ b/balancing/src/components/modules/building-table.tsx @@ -0,0 +1,91 @@ +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + BUILDING_CAPACITY, + BUILDING_COSTS, + BUILDING_COSTS_SCALED, + BUILDING_POPULATION, + BUILDING_RESOURCE_PRODUCED, + BuildingType, + RESOURCE_RARITY, + ResourcesIds, + resources, +} from "@bibliothecadao/eternum"; +import { Badge } from "../ui/badge"; + +export const BuildingTable = () => { + // Assuming BuildingType is an enum, we extract its keys for iteration + const buildingTypes = Object.keys(BuildingType).filter((key) => isNaN(Number(key))) as Array< + keyof typeof BuildingType + >; + + const resourceName = (id: number) => { + return resources.find((resource) => resource.id === id)?.trait; + }; + + const resourceColor = (id: number) => { + return resources.find((resource) => resource.id === id)?.colour; + }; + + const getAdjustedResourceInputs = (buildingId: number) => { + // const multiplier = HYPERSTRUCTURE_RESOURCE_MULTIPLIERS[buildingId] || 1; // Default multiplier is 1 + + return ( + BUILDING_COSTS_SCALED[buildingId]?.map((input) => ({ + ...input, + adjustedAmount: input.amount * RESOURCE_RARITY[input.resource], + })) || [] + ); + }; + + // Function to sum adjusted inputs + const sumAdjustedInputs = (buildingId: number) => { + const adjustedInputs = getAdjustedResourceInputs(buildingId); + return adjustedInputs.reduce((sum, input) => sum + input.adjustedAmount, 0).toFixed(2); + }; + + const getResourceImage = (id: number) => { + return resources.find((resource) => resource.id === id)?.img || ""; + }; + + return ( + + + + Building + {/* Description */} + Capacity + Population + Resource Produced + Cost + Cost Scaled + + + + {buildingTypes.map((typeKey) => { + const type = BuildingType[typeKey]; + const resourceProduced = BUILDING_RESOURCE_PRODUCED[type] + ? ResourcesIds[BUILDING_RESOURCE_PRODUCED[type] as unknown as keyof typeof ResourcesIds] + : "None"; + + return ( + + {typeKey} + {/* {BUILDING_INFORMATION[type]} */} + {BUILDING_CAPACITY[type]} + {BUILDING_POPULATION[type]} + {resourceProduced} + + {BUILDING_COSTS[type]?.map((input, idx) => ( + + x {input.amount} + + ))} + + {sumAdjustedInputs(type)} + + ); + })} + +
+ ); +}; diff --git a/balancing/src/components/modules/resource-table.tsx b/balancing/src/components/modules/resource-table.tsx new file mode 100644 index 000000000..7df71e3bf --- /dev/null +++ b/balancing/src/components/modules/resource-table.tsx @@ -0,0 +1,122 @@ +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + RESOURCE_BUILDING_COSTS, + RESOURCE_INPUTS_SCALED, + RESOURCE_OUTPUTS, + RESOURCE_RARITY, + RESOURCE_TIERS, + Resources, + WEIGHTS_GRAM, +} from "@bibliothecadao/eternum"; +import { Badge } from "../ui/badge"; + +export const ResourceTable = ({ resources }: { resources: Resources[] }) => { + const resourceName = (id: number) => { + return resources.find((resource) => resource.id === id)?.trait; + }; + + const resourceColor = (id: number) => { + return resources.find((resource) => resource.id === id)?.colour; + }; + + const getResourceDescription = (id: number) => { + return resources.find((resource) => resource.id === id)?.description || "No description available."; + }; + + const getResourceImage = (id: number) => { + return resources.find((resource) => resource.id === id)?.img || ""; + }; + + const getResourceWeight = (id: number) => { + return WEIGHTS_GRAM[id] || 0; + }; + + const getResourceTier = (id: number) => { + for (const [tier, ids] of Object.entries(RESOURCE_TIERS)) { + if (ids.includes(id as any)) { + return tier.charAt(0).toUpperCase() + tier.slice(1); + } + } + return "Unknown"; + }; + + // New function to calculate adjusted resource inputs based on rarity + const getAdjustedResourceInputs = (resourceId: number) => { + const multiplier = RESOURCE_RARITY[resourceId] || 1; // Default multiplier is 1 + + return ( + RESOURCE_INPUTS_SCALED[resourceId]?.map((input) => ({ + ...input, + adjustedAmount: input.amount * multiplier, + })) || [] + ); + }; + + // Function to sum adjusted inputs + const sumAdjustedInputs = (resourceId: number) => { + const adjustedInputs = getAdjustedResourceInputs(resourceId); + return adjustedInputs.reduce((sum, input) => sum + input.adjustedAmount, 0).toFixed(2); + }; + + return ( + + + + Image + Trait + ID + Ticker + {/* Description */} + Rarity + Tier + Weight (kg) + Resource Inputs p/s + Adjusted Value + Resource Outputs + Resource Building Fixed Cost + + + + {resources.map((resource, index) => ( + + + {getResourceImage(resource.id) ? ( + {resource.trait} + ) : ( + "N/A" + )} + + {resource.trait} + {resource.id} + {resource.ticker} + {/* {getResourceDescription(resource.id)} */} + + {RESOURCE_RARITY[resource.id as keyof typeof RESOURCE_RARITY]} + + + {getResourceTier(resource.id)} + {getResourceWeight(resource.id)} + + {RESOURCE_INPUTS_SCALED[resource.id]?.map((input, idx) => ( + + {resource.trait} x{" "} + {input.amount} + + ))} + + {sumAdjustedInputs(resource.id)} + {RESOURCE_OUTPUTS[resource.id]} + + {RESOURCE_BUILDING_COSTS[resource.id]?.map((input, idx) => ( + + {resource.trait} x{" "} + {input.amount} + + ))} + + + ))} + +
+ ); +}; diff --git a/balancing/src/components/ui/badge.tsx b/balancing/src/components/ui/badge.tsx new file mode 100644 index 000000000..10c5d3f47 --- /dev/null +++ b/balancing/src/components/ui/badge.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps extends React.HTMLAttributes, VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
; +} + +export { Badge, badgeVariants }; diff --git a/balancing/src/components/ui/table.tsx b/balancing/src/components/ui/table.tsx new file mode 100644 index 000000000..1f70268eb --- /dev/null +++ b/balancing/src/components/ui/table.tsx @@ -0,0 +1,76 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef>( + ({ className, ...props }, ref) => , +); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( + tr]:last:border-b-0", className)} {...props} /> + ), +); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef>( + ({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> + ), +); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef>( + ({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", className)} + {...props} + /> + ), +); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +TableCaption.displayName = "TableCaption"; + +export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }; diff --git a/balancing/src/components/ui/tabs.tsx b/balancing/src/components/ui/tabs.tsx new file mode 100644 index 000000000..9aa22a0b9 --- /dev/null +++ b/balancing/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/balancing/src/index.css b/balancing/src/index.css new file mode 100644 index 000000000..9adcfd4c8 --- /dev/null +++ b/balancing/src/index.css @@ -0,0 +1,66 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/balancing/src/lib/utils.ts b/balancing/src/lib/utils.ts new file mode 100644 index 000000000..a5ef19350 --- /dev/null +++ b/balancing/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/balancing/src/main.tsx b/balancing/src/main.tsx new file mode 100644 index 000000000..ef474bf64 --- /dev/null +++ b/balancing/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.tsx"; +import "./index.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/balancing/src/vite-env.d.ts b/balancing/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/balancing/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/balancing/tailwind.config.js b/balancing/tailwind.config.js new file mode 100644 index 000000000..66dd4b7b0 --- /dev/null +++ b/balancing/tailwind.config.js @@ -0,0 +1,57 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + colors: { + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + chart: { + 1: "hsl(var(--chart-1))", + 2: "hsl(var(--chart-2))", + 3: "hsl(var(--chart-3))", + 4: "hsl(var(--chart-4))", + 5: "hsl(var(--chart-5))", + }, + }, + }, + }, + plugins: [require("tailwindcss-animate")], +}; diff --git a/balancing/tsconfig.app.json b/balancing/tsconfig.app.json new file mode 100644 index 000000000..f0a235055 --- /dev/null +++ b/balancing/tsconfig.app.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/balancing/tsconfig.json b/balancing/tsconfig.json new file mode 100644 index 000000000..2b78387c7 --- /dev/null +++ b/balancing/tsconfig.json @@ -0,0 +1,10 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/balancing/tsconfig.node.json b/balancing/tsconfig.node.json new file mode 100644 index 000000000..29471657e --- /dev/null +++ b/balancing/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["vite.config.ts"] +} diff --git a/balancing/vite.config.ts b/balancing/vite.config.ts new file mode 100644 index 000000000..00564157f --- /dev/null +++ b/balancing/vite.config.ts @@ -0,0 +1,13 @@ +import react from "@vitejs/plugin-react"; +import path from "path"; +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); diff --git a/client/package.json b/client/package.json index 1b93b0558..3c114e500 100644 --- a/client/package.json +++ b/client/package.json @@ -27,6 +27,7 @@ "@headlessui/react": "^1.7.18", "@latticexyz/utils": "^2.0.0-next.12", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-tabs": "^1.1.0", "@react-three/drei": "^9.101.0", "@react-three/fiber": "^8.16.1", "@react-three/postprocessing": "2.16.2", diff --git a/client/src/assets/icons/Collapse Left.svg b/client/src/assets/icons/Collapse Left.svg new file mode 100644 index 000000000..e7e3b631c --- /dev/null +++ b/client/src/assets/icons/Collapse Left.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/Combat.svg b/client/src/assets/icons/Combat.svg new file mode 100644 index 000000000..65f96592f --- /dev/null +++ b/client/src/assets/icons/Combat.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/Compass.svg b/client/src/assets/icons/Compass.svg new file mode 100644 index 000000000..0d1135cce --- /dev/null +++ b/client/src/assets/icons/Compass.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/assets/icons/Controller.svg b/client/src/assets/icons/Controller.svg new file mode 100644 index 000000000..e4f299551 --- /dev/null +++ b/client/src/assets/icons/Controller.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/icons/Crown.svg b/client/src/assets/icons/Crown.svg new file mode 100644 index 000000000..cf235b7cb --- /dev/null +++ b/client/src/assets/icons/Crown.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/Helmet.svg b/client/src/assets/icons/Helmet.svg new file mode 100644 index 000000000..719ce0a44 --- /dev/null +++ b/client/src/assets/icons/Helmet.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/Info.svg b/client/src/assets/icons/Info.svg new file mode 100644 index 000000000..00f41879b --- /dev/null +++ b/client/src/assets/icons/Info.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/Swap.svg b/client/src/assets/icons/Swap.svg new file mode 100644 index 000000000..640193acf --- /dev/null +++ b/client/src/assets/icons/Swap.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/Times.svg b/client/src/assets/icons/Times.svg new file mode 100644 index 000000000..ea8ac6bda --- /dev/null +++ b/client/src/assets/icons/Times.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/Wedge Down.svg b/client/src/assets/icons/Wedge Down.svg new file mode 100644 index 000000000..65865e250 --- /dev/null +++ b/client/src/assets/icons/Wedge Down.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/icons/common/cross.svg b/client/src/assets/icons/common/cross.svg index 7546e3477..952f74544 100644 --- a/client/src/assets/icons/common/cross.svg +++ b/client/src/assets/icons/common/cross.svg @@ -1,5 +1,4 @@ - + + d="M8.8232 4.02369C9.05758 3.78932 9.05758 3.40869 8.8232 3.17432C8.58883 2.93994 8.2082 2.93994 7.97383 3.17432L5.99945 5.15057L4.0232 3.17619C3.78883 2.94182 3.4082 2.94182 3.17383 3.17619C2.93945 3.41057 2.93945 3.79119 3.17383 4.02557L5.15008 5.99994L3.1757 7.97619C2.94133 8.21057 2.94133 8.59119 3.1757 8.82557C3.41008 9.05994 3.7907 9.05994 4.02508 8.82557L5.99945 6.84932L7.9757 8.82369C8.21008 9.05807 8.5907 9.05807 8.82508 8.82369C9.05945 8.58932 9.05945 8.20869 8.82508 7.97432L6.84883 5.99994L8.8232 4.02369Z" /> \ No newline at end of file diff --git a/client/src/assets/icons/common/message.svg b/client/src/assets/icons/common/message.svg new file mode 100644 index 000000000..e5a80bf69 --- /dev/null +++ b/client/src/assets/icons/common/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/icons/common/minimize.svg b/client/src/assets/icons/common/minimize.svg new file mode 100644 index 000000000..0b611a777 --- /dev/null +++ b/client/src/assets/icons/common/minimize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/dojo/contractComponents.ts b/client/src/dojo/contractComponents.ts index 84a35e472..22522e203 100644 --- a/client/src/dojo/contractComponents.ts +++ b/client/src/dojo/contractComponents.ts @@ -276,6 +276,23 @@ export function defineContractComponents(world: World) { }, ); })(), + BuildingGeneralConfig: (() => { + return defineComponent( + world, + { + config_id: RecsType.Number, + base_cost_percent_increase: RecsType.Number, + }, + { + metadata: { + namespace: "eternum", + name: "BuildingGeneralConfig", + types: ["u32", "u16"], + customTypes: [], + }, + }, + ); + })(), BuildingQuantityv2: (() => { return defineComponent( world, @@ -485,47 +502,70 @@ export function defineContractComponents(world: World) { }, ); })(), - HyperstructureConfig: (() => { + Hyperstructure: (() => { return defineComponent( world, - { config_id: RecsType.Number, time_between_shares_change: RecsType.BigInt }, + { + entity_id: RecsType.Number, + current_epoch: RecsType.Number, + completed: RecsType.Boolean, + last_updated_by: RecsType.BigInt, + last_updated_timestamp: RecsType.Number, + private: RecsType.Boolean, + }, { metadata: { namespace: "eternum", - name: "HyperstructureConfig", - types: ["u32", "u64"], + name: "Hyperstructure", + types: ["u32", "u16", "bool", "contractaddress", "u64", "bool"], customTypes: [], }, }, ); })(), - HyperstructureResourceConfig: (() => { + Epoch: (() => { return defineComponent( world, - { config_id: RecsType.Number, resource_type: RecsType.Number, amount_for_completion: RecsType.BigInt }, + { + hyperstructure_entity_id: RecsType.Number, + index: RecsType.Number, + start_timestamp: RecsType.Number, + owners: RecsType.BigIntArray, + }, { metadata: { namespace: "eternum", - name: "HyperstructureResourceConfig", - types: ["u32", "u8", "u128"], + name: "Epoch", + types: ["u32", "u16", "u64", "array"], customTypes: [], }, }, ); })(), - HyperstructureUpdate: (() => { + + HyperstructureConfig: (() => { return defineComponent( world, + { config_id: RecsType.Number, time_between_shares_change: RecsType.Number }, { - hyperstructure_entity_id: RecsType.Number, - last_updated_timestamp: RecsType.BigInt, - last_updated_by: RecsType.BigInt, + metadata: { + namespace: "eternum", + name: "HyperstructureConfig", + types: ["u32", "u64"], + customTypes: [], + }, }, + ); + })(), + HyperstructureResourceConfig: (() => { + return defineComponent( + world, + { config_id: RecsType.Number, resource_type: RecsType.Number, amount_for_completion: RecsType.BigInt }, { metadata: { namespace: "eternum", - name: "HyperstructureUpdate", - types: ["u32", "u64", "contractaddress"], + name: "HyperstructureResourceConfig", + types: ["u32", "u8", "u128"], customTypes: [], }, }, @@ -1608,6 +1648,7 @@ const eventsComponents = (world: World) => { id: RecsType.Number, event_id: RecsType.String, entity_id: RecsType.Number, + owner_address: RecsType.BigInt, owner_name: RecsType.BigInt, realm_name: RecsType.BigInt, resource_types_packed: RecsType.BigInt, @@ -1630,6 +1671,7 @@ const eventsComponents = (world: World) => { "u32", "EventType", "u32", + "ContractAddress", "felt252", "felt252", "u128", diff --git a/client/src/dojo/createSystemCalls.ts b/client/src/dojo/createSystemCalls.ts index 037393c06..5bee4bdfa 100644 --- a/client/src/dojo/createSystemCalls.ts +++ b/client/src/dojo/createSystemCalls.ts @@ -6,10 +6,15 @@ class PromiseQueue { private readonly queue: Array<() => Promise> = []; private processing = false; - async enqueue(task: () => Promise) { - return await new Promise((resolve, reject) => { + async enqueue(task: () => Promise): Promise { + return new Promise((resolve, reject) => { this.queue.push(async () => { - await task().then(resolve).catch(reject); + try { + const result = await task(); + resolve(result); + } catch (error) { + reject(error); + } }); this.processQueue(); }); @@ -51,12 +56,14 @@ const withErrorHandling = export function createSystemCalls({ provider }: SetupNetworkResult) { const promiseQueue = new PromiseQueue(); - const withQueueing = (fn: (...args: any[]) => Promise) => { - return async (...args: any[]) => await promiseQueue.enqueue(async () => await fn(...args)); + const withQueueing = Promise>(fn: T) => { + return async (...args: Parameters): Promise> => { + return await promiseQueue.enqueue(async () => await fn(...args)); + }; }; - const withErrorHandling = (fn: (...args: any[]) => Promise) => { - return async (...args: any[]) => { + const withErrorHandling = Promise>(fn: T) => { + return async (...args: Parameters): Promise> => { try { return await fn(...args); } catch (error: any) { @@ -232,6 +239,14 @@ export function createSystemCalls({ provider }: SetupNetworkResult) { await provider.contribute_to_construction(props); }; + const set_private = async (props: SystemProps.SetPrivateProps) => { + await provider.set_private(props); + }; + + const end_game = async (props: SystemProps.EndGameProps) => { + await provider.end_game(props); + }; + const set_co_owners = async (props: SystemProps.SetCoOwnersProps) => { await provider.set_co_owners(props); }; @@ -337,9 +352,12 @@ export function createSystemCalls({ provider }: SetupNetworkResult) { create_army: withQueueing(withErrorHandling(create_army)), delete_army: withQueueing(withErrorHandling(delete_army)), uuid: withQueueing(withErrorHandling(uuid)), + create_hyperstructure: withQueueing(withErrorHandling(create_hyperstructure)), contribute_to_construction: withQueueing(withErrorHandling(contribute_to_construction)), + set_private: withQueueing(withErrorHandling(set_private)), set_co_owners: withQueueing(withErrorHandling(set_co_owners)), + end_game: withQueueing(withErrorHandling(end_game)), mint_resources: withQueueing(withErrorHandling(mint_resources)), mint_starting_resources: withQueueing(withErrorHandling(mint_starting_resources)), diff --git a/client/src/dojo/modelManager/ArmyMovementManager.ts b/client/src/dojo/modelManager/ArmyMovementManager.ts index 692897753..58acb3515 100644 --- a/client/src/dojo/modelManager/ArmyMovementManager.ts +++ b/client/src/dojo/modelManager/ArmyMovementManager.ts @@ -5,13 +5,13 @@ import { CapacityConfigCategory, ContractAddress, EternumGlobalConfig, - type ID, + NEIGHBOR_OFFSETS_EVEN, + NEIGHBOR_OFFSETS_ODD, ResourcesIds, getNeighborHexes, - neighborOffsetsEven, - neighborOffsetsOdd, + type ID, } from "@bibliothecadao/eternum"; -import { type ComponentValue, type Entity, getComponentValue } from "@dojoengine/recs"; +import { getComponentValue, type ComponentValue, type Entity } from "@dojoengine/recs"; import { uuid } from "@latticexyz/utils"; import { type ClientComponents } from "../createClientComponents"; import { type SetupResult } from "../setup"; @@ -270,7 +270,7 @@ export class ArmyMovementManager { const startPos = { col: path[0].col, row: path[0].row }; const endPos = { col: path[1].col, row: path[1].row }; - const neighborOffsets = startPos.row % 2 === 0 ? neighborOffsetsEven : neighborOffsetsOdd; + const neighborOffsets = startPos.row % 2 === 0 ? NEIGHBOR_OFFSETS_EVEN : NEIGHBOR_OFFSETS_ODD; for (const offset of neighborOffsets) { if (startPos.col + offset.i === endPos.col && startPos.row + offset.j === endPos.row) { diff --git a/client/src/dojo/modelManager/BattleManager.ts b/client/src/dojo/modelManager/BattleManager.ts index 2948978c7..2fbb5236a 100644 --- a/client/src/dojo/modelManager/BattleManager.ts +++ b/client/src/dojo/modelManager/BattleManager.ts @@ -417,7 +417,11 @@ export class BattleManager { currentTroops: ComponentValue, ): ComponentValue => { if (health.current > health.lifetime) { - throw new Error("Current health shouldn't be bigger than lifetime"); + return { + knight_count: 0n, + paladin_count: 0n, + crossbowman_count: 0n, + }; } if (health.lifetime === 0n) { @@ -482,10 +486,7 @@ export class BattleManager { durationPassed: number, ) { const damagesDone = this.damagesDone(delta, durationPassed); - const currentHealthAfterDamage = BigInt( - Math.min(Number(health.current), Number(this.getCurrentHealthAfterDamage(health, damagesDone))), - ); - return currentHealthAfterDamage; + return this.getCurrentHealthAfterDamage(health, damagesDone); } private attackingDelta(battle: ComponentValue) { diff --git a/client/src/dojo/modelManager/LeaderboardManager.ts b/client/src/dojo/modelManager/LeaderboardManager.ts index 5fa88072e..a20dd9872 100644 --- a/client/src/dojo/modelManager/LeaderboardManager.ts +++ b/client/src/dojo/modelManager/LeaderboardManager.ts @@ -125,15 +125,6 @@ export class LeaderboardManager { return parsedEvent; } - private parseHyperstructureFinishedEvent( - event: ComponentValue, - ): HyperstructureFinishedEvent { - return { - hyperstructureEntityId: event.hyperstructure_entity_id, - timestamp: event.timestamp, - }; - } - private parseCoOwnersChangeEvent( event: ComponentValue, ): HyperstructureCoOwnersChange { diff --git a/client/src/dojo/modelManager/StaminaManager.ts b/client/src/dojo/modelManager/StaminaManager.ts index 3fe3524ee..05d142455 100644 --- a/client/src/dojo/modelManager/StaminaManager.ts +++ b/client/src/dojo/modelManager/StaminaManager.ts @@ -25,9 +25,9 @@ export class StaminaManager { ); return { - knightConfig: knightConfig!.max_stamina, - crossbowmanConfig: crossbowmanConfig!.max_stamina, - paladinConfig: paladinConfig!.max_stamina, + knightConfig: knightConfig?.max_stamina ?? 0, + crossbowmanConfig: crossbowmanConfig?.max_stamina ?? 0, + paladinConfig: paladinConfig?.max_stamina ?? 0, }; } diff --git a/client/src/dojo/modelManager/__tests__/BattleManager.test.ts b/client/src/dojo/modelManager/__tests__/BattleManager.test.ts index ea602c6a2..f8c49b725 100644 --- a/client/src/dojo/modelManager/__tests__/BattleManager.test.ts +++ b/client/src/dojo/modelManager/__tests__/BattleManager.test.ts @@ -269,9 +269,13 @@ describe("getUpdatedTroops", () => { crossbowman_count: 10n, }; - expect(() => battleManager["getUpdatedTroops"](currentHealth, currentTroops)).toThrow( - "Current health shouldn't be bigger than lifetime", - ); + const ret = battleManager["getUpdatedTroops"](currentHealth, currentTroops); + + expect(ret).toStrictEqual({ + knight_count: 0n, + paladin_count: 0n, + crossbowman_count: 0n, + }); }); }); diff --git a/client/src/dojo/modelManager/__tests__/LeaderboardManager.test.ts b/client/src/dojo/modelManager/__tests__/LeaderboardManager.test.ts index 467bdd876..79a4acea0 100644 --- a/client/src/dojo/modelManager/__tests__/LeaderboardManager.test.ts +++ b/client/src/dojo/modelManager/__tests__/LeaderboardManager.test.ts @@ -1,21 +1,8 @@ -import { HYPERSTRUCTURE_POINTS_ON_COMPLETION, HYPERSTRUCTURE_POINTS_PER_CYCLE } from "@bibliothecadao/eternum"; import { afterEach, describe, expect, it, vi } from "vitest"; import { LeaderboardManager } from "../LeaderboardManager"; -import { - generateMockCoOwnersChangeEvent, - generateMockHyperstructureFinishedEvent, - HYPERSTRUCTURE_ENTITY_ID, - mockContributions, - mockSingleContribution, - OWNER_1_ADDRESS, - OWNER_1_SHARES, - OWNER_2_ADDRESS, - OWNER_2_SHARES, - TIMESTAMP, -} from "./__LeaderboardManagerMock__"; - -const PLAYER_ADDRESS_INDEX = 0; -const POINTS_INDEX = 1; + +// const PLAYER_ADDRESS_INDEX = 0; +// const POINTS_INDEX = 1; vi.mock("@bibliothecadao/eternum", async (importOriginal) => { const actual = await importOriginal(); @@ -54,458 +41,458 @@ describe("basic functionalities", () => { }); }); -describe("parseHyperstructureFinishedEvent", () => { - it("should return the correct hyperstructureEntityId and timestamp", () => { - const event = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// describe("parseHyperstructureFinishedEvent", () => { +// it("should return the correct hyperstructureEntityId and timestamp", () => { +// const event = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - const { hyperstructureEntityId, timestamp } = leaderboardManager["parseHyperstructureFinishedEvent"](event); +// const { hyperstructureEntityId, timestamp } = leaderboardManager["parseHyperstructureFinishedEvent"](event); - expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); - expect(timestamp).toBe(TIMESTAMP); - }); -}); +// expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); +// expect(timestamp).toBe(TIMESTAMP); +// }); +// }); -describe("parseCoOwnersChangeEvent", () => { - it("should return the correct hyperstructureEntityId and timestamp", () => { - const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// describe("parseCoOwnersChangeEvent", () => { +// it("should return the correct hyperstructureEntityId and timestamp", () => { +// const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - const { hyperstructureEntityId, timestamp, coOwners } = leaderboardManager["parseCoOwnersChangeEvent"](event); +// const { hyperstructureEntityId, timestamp, coOwners } = leaderboardManager["parseCoOwnersChangeEvent"](event); - expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); - expect(timestamp).toBe(TIMESTAMP); +// expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); +// expect(timestamp).toBe(TIMESTAMP); - const coOwner1 = coOwners[0]; - expect(coOwner1.address).toBe(OWNER_1_ADDRESS); - expect(coOwner1.percentage).toBe(OWNER_1_SHARES); +// const coOwner1 = coOwners[0]; +// expect(coOwner1.address).toBe(OWNER_1_ADDRESS); +// expect(coOwner1.percentage).toBe(OWNER_1_SHARES); - const coOwner2 = coOwners[1]; - expect(coOwner2.address).toBe(OWNER_2_ADDRESS); - expect(coOwner2.percentage).toBe(OWNER_2_SHARES); - }); -}); +// const coOwner2 = coOwners[1]; +// expect(coOwner2.address).toBe(OWNER_2_ADDRESS); +// expect(coOwner2.percentage).toBe(OWNER_2_SHARES); +// }); +// }); -describe("processHyperstructureFinishedEventData", () => { - it("should have an empty map on creation ", () => { - expect(leaderboardManager["pointsOnCompletionPerPlayer"].size).toBe(0); - }); +// describe("processHyperstructureFinishedEventData", () => { +// it("should have an empty map on creation ", () => { +// expect(leaderboardManager["pointsOnCompletionPerPlayer"].size).toBe(0); +// }); - it("should produce a valid map for pointsOnCompletionPerPlayer", () => { - const event = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// it("should produce a valid map for pointsOnCompletionPerPlayer", () => { +// const event = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - const { hyperstructureEntityId, timestamp } = leaderboardManager.processHyperstructureFinishedEventData( - event, - mockContributions, - ); +// const { hyperstructureEntityId, timestamp } = leaderboardManager.processHyperstructureFinishedEventData( +// event, +// mockContributions, +// ); - expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); - expect(timestamp).toBe(TIMESTAMP); +// expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); +// expect(timestamp).toBe(TIMESTAMP); - const pointsMap = leaderboardManager["pointsOnCompletionPerPlayer"]; +// const pointsMap = leaderboardManager["pointsOnCompletionPerPlayer"]; - expect(pointsMap.has(OWNER_1_ADDRESS)).toBe(true); - expect(pointsMap.has(OWNER_2_ADDRESS)).toBe(true); +// expect(pointsMap.has(OWNER_1_ADDRESS)).toBe(true); +// expect(pointsMap.has(OWNER_2_ADDRESS)).toBe(true); - const owner1Points = pointsMap.get(OWNER_1_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID); - const owner2Points = pointsMap.get(OWNER_2_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID); +// const owner1Points = pointsMap.get(OWNER_1_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID); +// const owner2Points = pointsMap.get(OWNER_2_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID); - expect(owner1Points).toBeDefined(); - expect(owner1Points).toBe(5); - expect(owner2Points).toBeDefined(); - expect(owner2Points).toBe(5); +// expect(owner1Points).toBeDefined(); +// expect(owner1Points).toBe(5); +// expect(owner2Points).toBeDefined(); +// expect(owner2Points).toBe(5); - // Assert that the total points equal HYPERSTRUCTURE_POINTS_ON_COMPLETION - expect(owner1Points! + owner2Points!).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); - }); +// // Assert that the total points equal HYPERSTRUCTURE_POINTS_ON_COMPLETION +// expect(owner1Points! + owner2Points!).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); +// }); - it("should produce a valid map for 2 events processed", () => { - const event = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// it("should produce a valid map for 2 events processed", () => { +// const event = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - const { hyperstructureEntityId, timestamp } = leaderboardManager.processHyperstructureFinishedEventData( - event, - mockContributions, - ); +// const { hyperstructureEntityId, timestamp } = leaderboardManager.processHyperstructureFinishedEventData( +// event, +// mockContributions, +// ); - expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); - expect(timestamp).toBe(TIMESTAMP); +// expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); +// expect(timestamp).toBe(TIMESTAMP); - const event2 = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID + 1); +// const event2 = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID + 1); - const { hyperstructureEntityId: hyperstructureEntityId2, timestamp: timestamp2 } = - leaderboardManager.processHyperstructureFinishedEventData(event2, mockContributions); +// const { hyperstructureEntityId: hyperstructureEntityId2, timestamp: timestamp2 } = +// leaderboardManager.processHyperstructureFinishedEventData(event2, mockContributions); - expect(hyperstructureEntityId2).toBe(HYPERSTRUCTURE_ENTITY_ID + 1); - expect(timestamp2).toBe(TIMESTAMP); +// expect(hyperstructureEntityId2).toBe(HYPERSTRUCTURE_ENTITY_ID + 1); +// expect(timestamp2).toBe(TIMESTAMP); - const pointsMap = leaderboardManager["pointsOnCompletionPerPlayer"]; +// const pointsMap = leaderboardManager["pointsOnCompletionPerPlayer"]; - expect(pointsMap.has(OWNER_1_ADDRESS)).toBe(true); - expect(pointsMap.has(OWNER_2_ADDRESS)).toBe(true); +// expect(pointsMap.has(OWNER_1_ADDRESS)).toBe(true); +// expect(pointsMap.has(OWNER_2_ADDRESS)).toBe(true); - const owner1Points = pointsMap.get(OWNER_1_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID); - const owner2Points = pointsMap.get(OWNER_2_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID); +// const owner1Points = pointsMap.get(OWNER_1_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID); +// const owner2Points = pointsMap.get(OWNER_2_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID); - expect(owner1Points).toBeDefined(); - expect(owner1Points).toBe(5); - expect(owner2Points).toBeDefined(); - expect(owner2Points).toBe(5); +// expect(owner1Points).toBeDefined(); +// expect(owner1Points).toBeCloseTo(5.0, 2); +// expect(owner2Points).toBeDefined(); +// expect(owner2Points).toBeCloseTo(5.0, 2); - const owner1Points2 = pointsMap.get(OWNER_1_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID + 1); - const owner2Points2 = pointsMap.get(OWNER_2_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID + 1); +// const owner1Points2 = pointsMap.get(OWNER_1_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID + 1); +// const owner2Points2 = pointsMap.get(OWNER_2_ADDRESS)?.get(HYPERSTRUCTURE_ENTITY_ID + 1); - expect(owner1Points2).toBeDefined(); - expect(owner1Points2).toBe(5); - expect(owner2Points2).toBeDefined(); - expect(owner2Points2).toBe(5); +// expect(owner1Points2).toBeDefined(); +// expect(owner1Points2).toBeCloseTo(5.0, 2); +// expect(owner2Points2).toBeDefined(); +// expect(owner2Points2).toBeCloseTo(5.0, 2); - expect(owner1Points! + owner2Points!).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); - expect(owner1Points2! + owner2Points2!).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); +// expect(owner1Points! + owner2Points!).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); +// expect(owner1Points2! + owner2Points2!).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); - expect(owner1Points! + owner1Points2!).toBe(5 + 5); - expect(owner2Points! + owner2Points2!).toBe(5 + 5); - }); -}); +// expect(owner1Points! + owner1Points2!).toBeCloseTo(10.0, 2); +// expect(owner2Points! + owner2Points2!).toBeCloseTo(10.0, 2); +// }); +// }); -describe("processHyperstructureCoOwnersChangeEvent", () => { - it("should have an empty array on creation ", () => { - expect(leaderboardManager["eventsCoOwnersChange"].length).toBe(0); - }); +// describe("processHyperstructureCoOwnersChangeEvent", () => { +// it("should have an empty array on creation ", () => { +// expect(leaderboardManager["eventsCoOwnersChange"].length).toBe(0); +// }); - it("should produce a valid array for one event processed", () => { - const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// it("should produce a valid array for one event processed", () => { +// const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - const { hyperstructureEntityId, timestamp, coOwners } = - leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); +// const { hyperstructureEntityId, timestamp, coOwners } = +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); - expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); - expect(timestamp).toBe(TIMESTAMP); +// expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); +// expect(timestamp).toBe(TIMESTAMP); - expect(coOwners.length).toBe(2); +// expect(coOwners.length).toBe(2); - const coOwner1 = coOwners[0]; - expect(coOwner1.address).toBe(OWNER_1_ADDRESS); - expect(coOwner1.percentage).toBe(OWNER_1_SHARES); +// const coOwner1 = coOwners[0]; +// expect(coOwner1.address).toBe(OWNER_1_ADDRESS); +// expect(coOwner1.percentage).toBe(OWNER_1_SHARES); - const coOwner2 = coOwners[1]; - expect(coOwner2.address).toBe(OWNER_2_ADDRESS); - expect(coOwner2.percentage).toBe(OWNER_2_SHARES); +// const coOwner2 = coOwners[1]; +// expect(coOwner2.address).toBe(OWNER_2_ADDRESS); +// expect(coOwner2.percentage).toBe(OWNER_2_SHARES); - expect(leaderboardManager["eventsCoOwnersChange"].length).toBe(1); - const storedEvent = leaderboardManager["eventsCoOwnersChange"][0]; - expect(storedEvent.coOwners.length).toBe(2); +// expect(leaderboardManager["eventsCoOwnersChange"].length).toBe(1); +// const storedEvent = leaderboardManager["eventsCoOwnersChange"][0]; +// expect(storedEvent.coOwners.length).toBe(2); - const storedCoOwner1 = storedEvent.coOwners[0]; - expect(storedCoOwner1.address).toBe(OWNER_1_ADDRESS); - expect(storedCoOwner1.percentage).toBe(OWNER_1_SHARES); +// const storedCoOwner1 = storedEvent.coOwners[0]; +// expect(storedCoOwner1.address).toBe(OWNER_1_ADDRESS); +// expect(storedCoOwner1.percentage).toBe(OWNER_1_SHARES); - const storedCoOwner2 = storedEvent.coOwners[1]; - expect(storedCoOwner2.address).toBe(OWNER_2_ADDRESS); - expect(storedCoOwner2.percentage).toBe(OWNER_2_SHARES); - }); +// const storedCoOwner2 = storedEvent.coOwners[1]; +// expect(storedCoOwner2.address).toBe(OWNER_2_ADDRESS); +// expect(storedCoOwner2.percentage).toBe(OWNER_2_SHARES); +// }); - it("should produce a valid array for two events processed", () => { - const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// it("should produce a valid array for two events processed", () => { +// const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - const { hyperstructureEntityId } = leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); - expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); +// const { hyperstructureEntityId } = leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); +// expect(hyperstructureEntityId).toBe(HYPERSTRUCTURE_ENTITY_ID); - const event2 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID + 1); - const { hyperstructureEntityId: hyperstructureEntityId2 } = - leaderboardManager.processHyperstructureCoOwnersChangeEvent(event2); - expect(hyperstructureEntityId2).toBe(HYPERSTRUCTURE_ENTITY_ID + 1); +// const event2 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID + 1); +// const { hyperstructureEntityId: hyperstructureEntityId2 } = +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(event2); +// expect(hyperstructureEntityId2).toBe(HYPERSTRUCTURE_ENTITY_ID + 1); - expect(leaderboardManager["eventsCoOwnersChange"].length).toBe(2); - const storedEvent = leaderboardManager["eventsCoOwnersChange"][0]; - expect(storedEvent.coOwners.length).toBe(2); +// expect(leaderboardManager["eventsCoOwnersChange"].length).toBe(2); +// const storedEvent = leaderboardManager["eventsCoOwnersChange"][0]; +// expect(storedEvent.coOwners.length).toBe(2); - const storedCoOwner1 = storedEvent.coOwners[0]; - expect(storedCoOwner1.address).toBe(OWNER_1_ADDRESS); - expect(storedCoOwner1.percentage).toBe(OWNER_1_SHARES); +// const storedCoOwner1 = storedEvent.coOwners[0]; +// expect(storedCoOwner1.address).toBe(OWNER_1_ADDRESS); +// expect(storedCoOwner1.percentage).toBe(OWNER_1_SHARES); - const storedCoOwner2 = storedEvent.coOwners[1]; - expect(storedCoOwner2.address).toBe(OWNER_2_ADDRESS); - expect(storedCoOwner2.percentage).toBe(OWNER_2_SHARES); - }); -}); +// const storedCoOwner2 = storedEvent.coOwners[1]; +// expect(storedCoOwner2.address).toBe(OWNER_2_ADDRESS); +// expect(storedCoOwner2.percentage).toBe(OWNER_2_SHARES); +// }); +// }); -describe("getShares", () => { - it("should return undefined if no change co owner event occured", () => { - expect(leaderboardManager.getAddressShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID)).toBeUndefined(); - }); +// describe("getShares", () => { +// it("should return undefined if no change co owner event occured", () => { +// expect(leaderboardManager.getAddressShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID)).toBeUndefined(); +// }); - it("should return undefined if an event occured but not for the right entity id", () => { - const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); +// it("should return undefined if an event occured but not for the right entity id", () => { +// const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); - expect(leaderboardManager.getAddressShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID + 1)).toBeUndefined(); - }); +// expect(leaderboardManager.getAddressShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID + 1)).toBeUndefined(); +// }); - it("should return the correct amount of shares", () => { - const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); +// it("should return the correct amount of shares", () => { +// const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); - expect(leaderboardManager.getAddressShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID)).toBe( - OWNER_1_SHARES / 10_000, - ); - }); -}); +// expect(leaderboardManager.getAddressShares(OWNER_1_ADDRESS, HYPERSTRUCTURE_ENTITY_ID)).toBe( +// OWNER_1_SHARES / 10_000, +// ); +// }); +// }); -describe("getPlayersByRank one event", () => { - it("should return an empty array if no change co owner event occured", () => { - expect(leaderboardManager.getPlayersByRank(1)).toEqual([]); - }); +// describe("getPlayersByRank one event", () => { +// it("should return an empty array if no change co owner event occured", () => { +// expect(leaderboardManager.getPlayersByRank(1)).toEqual([]); +// }); - it("should return the same points as the completion event for the same timestamp", () => { - const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); +// it("should return the same points as the completion event for the same timestamp", () => { +// const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); - const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); +// const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); - const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP); +// const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP); - expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - }); +// expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// }); - it("should return more points as the timestamp increases - no filtering on hyperstructure entity id (events from one hyperstructure)", () => { - const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); +// it("should return more points as the timestamp increases - no filtering on hyperstructure entity id (events from one hyperstructure)", () => { +// const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); - const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); +// const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); - const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); +// const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); - expect(rankings[0][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - expect(rankings[1][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); +// expect(rankings[0][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); +// expect(rankings[1][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); - const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP + 2); +// const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP + 2); - expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE); - expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE); - }); +// expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE); +// expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE); +// }); - it("should return more points as the timestamp increases - filter on entity id (events from one hyperstructure)", () => { - const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); +// it("should return more points as the timestamp increases - filter on entity id (events from one hyperstructure)", () => { +// const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); - const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); +// const event = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(event); - const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID); +// const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID); - expect(rankings[0][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - expect(rankings[1][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); +// expect(rankings[0][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); +// expect(rankings[1][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); - const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP + 2, HYPERSTRUCTURE_ENTITY_ID); +// const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP + 2, HYPERSTRUCTURE_ENTITY_ID); - expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE); - expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE); - }); +// expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE); +// expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE); +// }); - it("multiple creation events - no filtering on hyperstructure entity id", () => { - const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); +// it("multiple creation events - no filtering on hyperstructure entity id", () => { +// const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); - const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP); +// const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP); - expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - const hyperstructureFinishedEvent2 = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID + 1); - leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent2, mockSingleContribution); +// const hyperstructureFinishedEvent2 = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID + 1); +// leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent2, mockSingleContribution); - const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP); +// const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP); - expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); - expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - }); +// expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); +// expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// }); - it("multiple creation events - filter on entity id", () => { - const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); +// it("multiple creation events - filter on entity id", () => { +// const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); - const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); +// const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); - expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - const hyperstructureFinishedEvent2 = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID + 1); - leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent2, mockSingleContribution); +// const hyperstructureFinishedEvent2 = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID + 1); +// leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent2, mockSingleContribution); - const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); +// const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); - expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - const rankings3 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID + 1); +// const rankings3 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID + 1); - expect(rankings3[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings3[1][POINTS_INDEX]).toBe(0); - }); +// expect(rankings3[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings3[1][POINTS_INDEX]).toBe(0); +// }); - it("multiple ownership events - no filtering on hyperstructure entity id", () => { - const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); +// it("multiple ownership events - no filtering on hyperstructure entity id", () => { +// const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); - const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP); +// const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP); - expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - const coOwnersChange1 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID, TIMESTAMP); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(coOwnersChange1); +// const coOwnersChange1 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID, TIMESTAMP); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(coOwnersChange1); - const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP); +// const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP); - expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - const rankings3 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); +// const rankings3 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); - expect(rankings3[0][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - expect(rankings3[1][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); +// expect(rankings3[0][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); +// expect(rankings3[1][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); - const hyperstructureFinishedEvent2 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(hyperstructureFinishedEvent2); +// const hyperstructureFinishedEvent2 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(hyperstructureFinishedEvent2); - const rankings4 = leaderboardManager.getPlayersByRank(TIMESTAMP + 2); +// const rankings4 = leaderboardManager.getPlayersByRank(TIMESTAMP + 2); - expect(rankings4[0][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE * 2, - ); - expect(rankings4[1][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE * 2, - ); - }); +// expect(rankings4[0][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE * 2, +// ); +// expect(rankings4[1][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE * 2, +// ); +// }); - it("multiple ownership events - filtering on hyperstructure entity id", () => { - const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); +// it("multiple ownership events - filtering on hyperstructure entity id", () => { +// const hyperstructureFinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureFinishedEventData(hyperstructureFinishedEvent, mockContributions); - const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); +// const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); - expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - const coOwnersChange1 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID, TIMESTAMP); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(coOwnersChange1); +// const coOwnersChange1 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID, TIMESTAMP); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(coOwnersChange1); - const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); +// const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); - expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - const rankings3 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID); +// const rankings3 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID); - expect(rankings3[0][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - expect(rankings3[1][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); +// expect(rankings3[0][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); +// expect(rankings3[1][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); - const hyperstructureFinishedEvent2 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(hyperstructureFinishedEvent2); +// const hyperstructureFinishedEvent2 = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(hyperstructureFinishedEvent2); - const rankings4 = leaderboardManager.getPlayersByRank(TIMESTAMP + 2, HYPERSTRUCTURE_ENTITY_ID); +// const rankings4 = leaderboardManager.getPlayersByRank(TIMESTAMP + 2, HYPERSTRUCTURE_ENTITY_ID); - expect(rankings4[0][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE * 2, - ); - expect(rankings4[1][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE * 2, - ); - }); +// expect(rankings4[0][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE * 2, +// ); +// expect(rankings4[1][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE * 2, +// ); +// }); - it("player 1 contributed to hs1 and hs2, player 2 contributed to hs1 but is owner of hs2", () => { - const hs1FinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); - leaderboardManager.processHyperstructureFinishedEventData(hs1FinishedEvent, mockContributions); - - const hs2FinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID + 1); - leaderboardManager.processHyperstructureFinishedEventData(hs2FinishedEvent, mockSingleContribution); - - const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP); - // points for player 1 should be higher than player 2 because he contributed to hs1 and hs2 - expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); - expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - - const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); - - // points for player 1 should be the same as player 2 because they both contributed to hs1 - expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - - const rankings3 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID + 1); - - // points for player 1 should be the bigger than player 2 because he's the only one who contributed to hs2 - expect(rankings3[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings3[1][POINTS_INDEX]).toBe(0); - - const hs1CoOwnersChangeEvent = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID, undefined, { - playerOne: 5000, - playerTwo: 5000, - }); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(hs1CoOwnersChangeEvent); - - const rankings4 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID); - expect(rankings4[0][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - expect(rankings4[1][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - - const rankings5 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID + 1); - expect(rankings5[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings5[1][POINTS_INDEX]).toBe(0); - - const rankings6 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); - expect(rankings6[0][POINTS_INDEX]).toBe( - (HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2) * 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - expect(rankings6[1][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - - const hs2CoOwnersChangeEvent = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID + 1, TIMESTAMP + 1, { - playerOne: 0, - playerTwo: 10000, - }); - leaderboardManager.processHyperstructureCoOwnersChangeEvent(hs2CoOwnersChangeEvent); - - const rankings7 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID + 1); - - expect(rankings7[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); - expect(rankings7[1][POINTS_INDEX]).toBe(0); - - const rankings8 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); - expect(rankings8[0][POINTS_INDEX]).toBe( - (HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2) * 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - expect(rankings8[1][POINTS_INDEX]).toBe( - HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, - ); - - rankings8[0][PLAYER_ADDRESS_INDEX]; - - const rankings9 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID); - const rankings10 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID + 1); - const rankings11 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); - - expect(rankings9[0][POINTS_INDEX] + rankings10[0][POINTS_INDEX]).toBe(rankings11[0][POINTS_INDEX]); - expect(rankings9[1][POINTS_INDEX] + rankings10[1][POINTS_INDEX]).toBe(rankings11[1][POINTS_INDEX]); - }); -}); +// it("player 1 contributed to hs1 and hs2, player 2 contributed to hs1 but is owner of hs2", () => { +// const hs1FinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID); +// leaderboardManager.processHyperstructureFinishedEventData(hs1FinishedEvent, mockContributions); + +// const hs2FinishedEvent = generateMockHyperstructureFinishedEvent(HYPERSTRUCTURE_ENTITY_ID + 1); +// leaderboardManager.processHyperstructureFinishedEventData(hs2FinishedEvent, mockSingleContribution); + +// const rankings = leaderboardManager.getPlayersByRank(TIMESTAMP); +// // points for player 1 should be higher than player 2 because he contributed to hs1 and hs2 +// expect(rankings[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION); +// expect(rankings[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); + +// const rankings2 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID); + +// // points for player 1 should be the same as player 2 because they both contributed to hs1 +// expect(rankings2[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings2[1][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); + +// const rankings3 = leaderboardManager.getPlayersByRank(TIMESTAMP, HYPERSTRUCTURE_ENTITY_ID + 1); + +// // points for player 1 should be the bigger than player 2 because he's the only one who contributed to hs2 +// expect(rankings3[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings3[1][POINTS_INDEX]).toBe(0); + +// const hs1CoOwnersChangeEvent = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID, undefined, { +// playerOne: 5000, +// playerTwo: 5000, +// }); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(hs1CoOwnersChangeEvent); + +// const rankings4 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID); +// expect(rankings4[0][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); +// expect(rankings4[1][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); + +// const rankings5 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID + 1); +// expect(rankings5[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings5[1][POINTS_INDEX]).toBe(0); + +// const rankings6 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); +// expect(rankings6[0][POINTS_INDEX]).toBe( +// (HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2) * 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); +// expect(rankings6[1][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); + +// const hs2CoOwnersChangeEvent = generateMockCoOwnersChangeEvent(HYPERSTRUCTURE_ENTITY_ID + 1, TIMESTAMP + 1, { +// playerOne: 0, +// playerTwo: 10000, +// }); +// leaderboardManager.processHyperstructureCoOwnersChangeEvent(hs2CoOwnersChangeEvent); + +// const rankings7 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID + 1); + +// expect(rankings7[0][POINTS_INDEX]).toBe(HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2); +// expect(rankings7[1][POINTS_INDEX]).toBe(0); + +// const rankings8 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); +// expect(rankings8[0][POINTS_INDEX]).toBe( +// (HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2) * 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); +// expect(rankings8[1][POINTS_INDEX]).toBe( +// HYPERSTRUCTURE_POINTS_ON_COMPLETION / 2 + HYPERSTRUCTURE_POINTS_PER_CYCLE / 2, +// ); + +// rankings8[0][PLAYER_ADDRESS_INDEX]; + +// const rankings9 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID); +// const rankings10 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1, HYPERSTRUCTURE_ENTITY_ID + 1); +// const rankings11 = leaderboardManager.getPlayersByRank(TIMESTAMP + 1); + +// expect(rankings9[0][POINTS_INDEX] + rankings10[0][POINTS_INDEX]).toBe(rankings11[0][POINTS_INDEX]); +// expect(rankings9[1][POINTS_INDEX] + rankings10[1][POINTS_INDEX]).toBe(rankings11[1][POINTS_INDEX]); +// }); +// }); diff --git a/client/src/dojo/modelManager/utils/LeaderboardUtils.test.ts b/client/src/dojo/modelManager/utils/LeaderboardUtils.test.ts index e31747c68..27575c6d3 100644 --- a/client/src/dojo/modelManager/utils/LeaderboardUtils.test.ts +++ b/client/src/dojo/modelManager/utils/LeaderboardUtils.test.ts @@ -1,12 +1,7 @@ import { describe, expect, it, vi } from "vitest"; -import { - computeInitialContributionPoints, - getTotalPointsPercentage, - TOTAL_CONTRIBUTABLE_AMOUNT, -} from "./LeaderboardUtils"; -import { HyperstructureResourceMultipliers } from "@bibliothecadao/eternum"; +import { TOTAL_CONTRIBUTABLE_AMOUNT } from "./LeaderboardUtils"; -const EXPECTED_TOTAL_CONTRIBUTABLE_AMOUNT = 2; +const EXPECTED_TOTAL_CONTRIBUTABLE_AMOUNT = 2.27; vi.mock("@bibliothecadao/eternum", async (importOriginal) => { const actual = await importOriginal(); @@ -35,22 +30,22 @@ describe("TOTAL_CONTRIBUTABLE_AMOUNT", () => { }); }); -describe("getTotalPointsPercentage", () => { - it("should return a valid amount for resource 1", () => { - expect(getTotalPointsPercentage(1, 1n)).toBe(0.5); - }); +// 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); - }); -}); +// 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); - }); +// 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); - }); -}); +// 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 d4b17dbc3..42b7f0664 100644 --- a/client/src/dojo/modelManager/utils/LeaderboardUtils.ts +++ b/client/src/dojo/modelManager/utils/LeaderboardUtils.ts @@ -2,8 +2,8 @@ import { ClientComponents } from "@/dojo/createClientComponents"; import { EternumGlobalConfig, HYPERSTRUCTURE_POINTS_ON_COMPLETION, + HYPERSTRUCTURE_RESOURCE_MULTIPLIERS, HYPERSTRUCTURE_TOTAL_COSTS_SCALED, - HyperstructureResourceMultipliers, ResourcesIds, } from "@bibliothecadao/eternum"; import { ComponentValue } from "@dojoengine/recs"; @@ -12,14 +12,14 @@ export const TOTAL_CONTRIBUTABLE_AMOUNT: number = HYPERSTRUCTURE_TOTAL_COSTS_SCA (total, { resource, amount }) => { return ( total + - (HyperstructureResourceMultipliers[resource as keyof typeof HyperstructureResourceMultipliers] ?? 0) * amount + (HYPERSTRUCTURE_RESOURCE_MULTIPLIERS[resource as keyof typeof HYPERSTRUCTURE_RESOURCE_MULTIPLIERS] ?? 0) * amount ); }, 0, ); function getResourceMultiplier(resourceType: ResourcesIds): number { - return HyperstructureResourceMultipliers[resourceType] ?? 0; + return HYPERSTRUCTURE_RESOURCE_MULTIPLIERS[resourceType] ?? 0; } export function computeInitialContributionPoints( diff --git a/client/src/hooks/helpers/useContributions.tsx b/client/src/hooks/helpers/useContributions.tsx index 0e9675e6b..caab6f1e4 100644 --- a/client/src/hooks/helpers/useContributions.tsx +++ b/client/src/hooks/helpers/useContributions.tsx @@ -3,6 +3,7 @@ import { getTotalPointsPercentage } from "@/dojo/modelManager/utils/LeaderboardU import { ContractAddress, ID, Resource } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; import { ComponentValue, HasValue, getComponentValue, runQuery } from "@dojoengine/recs"; +import { useCallback } from "react"; import { useDojo } from "../context/DojoContext"; export const useContributions = () => { @@ -42,3 +43,22 @@ export const useContributions = () => { getContributionsTotalPercentage, }; }; + +export const useGetHyperstructuresWithContributionsFromPlayer = () => { + const { + account: { account }, + setup: { + components: { Contribution }, + }, + } = useDojo(); + + const getContributions = useCallback(() => { + const entityIds = runQuery([HasValue(Contribution, { player_address: ContractAddress(account.address) })]); + const hyperstructureEntityIds = Array.from(entityIds).map( + (entityId) => getComponentValue(Contribution, entityId)?.hyperstructure_entity_id ?? 0, + ); + return new Set(hyperstructureEntityIds); + }, [account.address]); + + return getContributions; +}; diff --git a/client/src/hooks/helpers/useEntities.tsx b/client/src/hooks/helpers/useEntities.tsx index 914b95661..794ddab6d 100644 --- a/client/src/hooks/helpers/useEntities.tsx +++ b/client/src/hooks/helpers/useEntities.tsx @@ -2,23 +2,23 @@ import { type ClientComponents } from "@/dojo/createClientComponents"; import { getRealmNameById } from "@/ui/utils/realms"; import { divideByPrecision, getEntityIdFromKeys, getPosition } from "@/ui/utils/utils"; import { - CapacityConfigCategoryStringMap, + CAPACITY_CONFIG_CATEGORY_STRING_MAP, ContractAddress, EntityType, - type ID, StructureType, + type ID, } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; import { - type Component, - type ComponentValue, - type Entity, Has, HasValue, - NotValue, getComponentValue, runQuery, + type Component, + type ComponentValue, + type Entity, } from "@dojoengine/recs"; +import { useMemo } from "react"; import { shortString } from "starknet"; import { useDojo } from "../context/DojoContext"; import { getResourcesUtils } from "./useResources"; @@ -27,6 +27,13 @@ export type PlayerStructure = ComponentValue; name: string; category?: string | undefined; + owner: ComponentValue; +}; + +type RealmWithPosition = ComponentValue & { + position: ComponentValue; + name: string; + owner: ComponentValue; }; export const useEntities = () => { @@ -39,51 +46,136 @@ export const useEntities = () => { const { getEntityName } = getEntitiesUtils(); - const playerRealms = useEntityQuery([Has(Realm), HasValue(Owner, { address: ContractAddress(account.address) })]); - const otherRealms = useEntityQuery([Has(Realm), NotValue(Owner, { address: ContractAddress(account.address) })]); - const playerStructures = useEntityQuery([ - Has(Structure), - HasValue(Owner, { address: ContractAddress(account.address) }), - ]); + // Get all realms + const allRealms = useEntityQuery([Has(Realm)]); + + const filterPlayerRealms = useMemo(() => { + return allRealms.filter((id) => { + const owner = getComponentValue(Owner, id); + return owner && ContractAddress(owner.address) === ContractAddress(account.address); + }); + }, [allRealms]); + + const filterOtherRealms = useMemo(() => { + return allRealms.filter((id) => { + const owner = getComponentValue(Owner, id); + return owner && ContractAddress(owner.address) !== ContractAddress(account.address); + }); + }, [allRealms]); + + // Get all structures + const allStructures = useEntityQuery([Has(Structure)]); + + const filterPlayerStructures = useMemo(() => { + return allStructures.filter((id) => { + const owner = getComponentValue(Owner, id); + return owner && ContractAddress(owner.address) === ContractAddress(account.address); + }); + }, [allStructures]); + + const filterOtherStructures = useMemo(() => { + return allStructures.filter((id) => { + const owner = getComponentValue(Owner, id); + return owner && ContractAddress(owner.address) !== ContractAddress(account.address); + }); + }, [allStructures]); + + const playerRealms = useMemo(() => { + return filterPlayerRealms.map((id) => { + const realm = getComponentValue(Realm, id); + return { + ...realm, + position: getPosition(realm!.realm_id), + name: getRealmNameById(realm!.realm_id), + owner: getComponentValue(Owner, id), + } as RealmWithPosition; + }); + }, [filterPlayerRealms]); + + const otherRealms = useMemo(() => { + return filterOtherRealms.map((id) => { + const realm = getComponentValue(Realm, id); + return { + ...realm, + position: getPosition(realm!.realm_id), + name: getRealmNameById(realm!.realm_id), + owner: getComponentValue(Owner, id), + } as RealmWithPosition; + }); + }, [filterOtherRealms]); + + const playerStructures = useMemo(() => { + return filterPlayerStructures + .map((id) => { + const structure = getComponentValue(Structure, id); + if (!structure) return; - return { - playerRealms: () => { - return playerRealms.map((id) => { - const realm = getComponentValue(Realm, id); - return { ...realm, position: getPosition(realm!.realm_id), name: getRealmNameById(realm!.realm_id) }; - }); - }, - otherRealms: () => { - return otherRealms.map((id) => { const realm = getComponentValue(Realm, id); - return { ...realm, position: getPosition(realm!.realm_id), name: getRealmNameById(realm!.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, owner: getComponentValue(Owner, id) }; + }) + .filter((structure): structure is PlayerStructure => structure !== undefined) + .sort((a, b) => { + if (a.category === StructureType[StructureType.Realm]) return -1; + if (b.category === StructureType[StructureType.Realm]) return 1; + return a.category.localeCompare(b.category); }); - }, - playerStructures: (): PlayerStructure[] => { - return playerStructures - .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) => { - if (a.category === StructureType[StructureType.Realm]) return -1; - if (b.category === StructureType[StructureType.Realm]) return 1; - return a.category.localeCompare(b.category); - }); - }, + }, [filterPlayerStructures]); + + const otherStructures = useMemo(() => { + return filterOtherStructures + .map((id) => { + const structure = getComponentValue(Structure, id); + if (!structure || structure.category === StructureType[StructureType.Realm]) return; + + const position = getComponentValue(Position, id); + + const structureName = getEntityName(structure.entity_id); + + const name = structureName ? `${structure?.category} ${structureName}` : structure.category || ""; + return { ...structure, position: position!, name, owner: getComponentValue(Owner, id) }; + }) + .filter((structure): structure is PlayerStructure => structure !== undefined) + .sort((a, b) => a.category.localeCompare(b.category)); + }, [filterOtherStructures]); + + const getPlayerRealms = (filterFn?: (realm: RealmWithPosition) => boolean) => { + return useMemo(() => { + return filterFn ? playerRealms.filter(filterFn) : playerRealms; + }, [playerRealms, filterFn]); + }; + + const getOtherRealms = (filterFn?: (realm: RealmWithPosition) => boolean) => { + return useMemo(() => { + return filterFn ? otherRealms.filter(filterFn) : otherRealms; + }, [otherRealms, filterFn]); + }; + + const getPlayerStructures = (filterFn?: (structure: PlayerStructure) => boolean) => { + return useMemo(() => { + return filterFn ? playerStructures.filter(filterFn) : playerStructures; + }, [otherRealms, filterFn]); + }; + + const getOtherStructures = (filterFn?: (structure: PlayerStructure) => boolean) => { + return useMemo(() => { + return filterFn ? otherStructures.filter(filterFn) : otherStructures; + }, [otherRealms, filterFn]); + }; + + return { + playerRealms: getPlayerRealms, + otherRealms: getOtherRealms, + playerStructures: getPlayerStructures, + otherStructures: getOtherStructures, }; }; @@ -133,7 +225,7 @@ export const getEntitiesUtils = () => { const entityCapacityCategory = getComponentValue(CapacityCategory, getEntityIdFromKeys([entityIdBigInt])) ?.category as unknown as string; - const capacityCategoryId = CapacityConfigCategoryStringMap[entityCapacityCategory] || 0n; + const capacityCategoryId = CAPACITY_CONFIG_CATEGORY_STRING_MAP[entityCapacityCategory] || 0n; const capacity = getComponentValue(CapacityConfig, getEntityIdFromKeys([BigInt(capacityCategoryId)])); const entityOwner = getComponentValue(EntityOwner, getEntityIdFromKeys([entityIdBigInt])); @@ -177,14 +269,11 @@ export const getEntitiesUtils = () => { }; }; - const getEntityName = (entityId: ID) => { + const getEntityName = (entityId: ID, abbreviate: boolean = false) => { const entityName = getComponentValue(EntityName, getEntityIdFromKeys([BigInt(entityId)])); const realm = getComponentValue(Realm, getEntityIdFromKeys([BigInt(entityId)])); - return entityName - ? shortString.decodeShortString(entityName.name.toString()) - : realm - ? getRealmNameById(realm.realm_id) - : entityId.toString(); + const structure = getComponentValue(Structure, getEntityIdFromKeys([BigInt(entityId)])); + return getStructureName(entityName, structure, realm, abbreviate); }; const getAddressNameFromEntity = (entityId: ID) => { @@ -206,28 +295,6 @@ export const getEntitiesUtils = () => { return { getEntityName, getEntityInfo, getAddressNameFromEntity, getPlayerAddressFromEntity }; }; -export const useGetAllPlayers = () => { - const { - setup: { - components: { Owner, Realm }, - }, - } = useDojo(); - - const { getAddressNameFromEntity } = getEntitiesUtils(); - - const playersEntityIds = runQuery([Has(Owner), Has(Realm)]); - - const getPlayers = () => { - const players = getAddressNameFromEntityIds(Array.from(playersEntityIds), Owner, getAddressNameFromEntity); - - const uniquePlayers = Array.from(new Map(players.map((player) => [player.address, player])).values()); - - return uniquePlayers; - }; - - return getPlayers; -}; - export const getAddressNameFromEntityIds = ( entityId: Entity[], Owner: Component, @@ -239,10 +306,11 @@ export const getAddressNameFromEntityIds = ( if (!owner) return; const addressName = getAddressNameFromEntity(owner?.entity_id); + if (!addressName) return; return { ...owner, addressName }; }) .filter( - (owner): owner is ComponentValue & { addressName: string | undefined } => + (owner): owner is ComponentValue & { addressName: string } => owner !== undefined, ); }; @@ -274,3 +342,33 @@ const formatStructures = ( .filter((structure): structure is PlayerStructure => structure !== undefined) .sort((a, b) => (b.category || "").localeCompare(a.category || "")); }; + +const getStructureName = ( + entityName: ComponentValue | undefined, + structure: ComponentValue | undefined, + realm: ComponentValue | undefined, + abbreviate: boolean = false, +) => { + if (!structure) return "Unknown"; + + let name = ""; + if (structure.category === StructureType[StructureType.Realm]) { + name = getRealmNameById(realm!.realm_id); + } else if (entityName) { + name = shortString.decodeShortString(entityName.name.toString()); + } else { + name = `${structure.category} ${structure.entity_id}`; + + if (abbreviate) { + if (structure.category === StructureType[StructureType.FragmentMine]) { + name = `FM ${structure.entity_id}`; + } else if (structure.category === StructureType[StructureType.Hyperstructure]) { + name = `HS ${structure.entity_id}`; + } else if (structure.category === StructureType[StructureType.Bank]) { + name = `BK ${structure.entity_id}`; + } + } + } + + return name; +}; diff --git a/client/src/hooks/helpers/useGetAllPlayers.tsx b/client/src/hooks/helpers/useGetAllPlayers.tsx new file mode 100644 index 000000000..70e3cf82b --- /dev/null +++ b/client/src/hooks/helpers/useGetAllPlayers.tsx @@ -0,0 +1,52 @@ +import { ContractAddress } from "@bibliothecadao/eternum"; +import { Has, NotValue, runQuery } from "@dojoengine/recs"; +import { useDojo } from "../context/DojoContext"; +import { getAddressNameFromEntityIds, getEntitiesUtils } from "./useEntities"; + +export const useGetAllPlayers = () => { + const { + setup: { + components: { Owner, Realm }, + }, + } = useDojo(); + + const { getAddressNameFromEntity } = getEntitiesUtils(); + + const playersEntityIds = runQuery([Has(Owner), Has(Realm)]); + + const getPlayers = () => { + const players = getAddressNameFromEntityIds(Array.from(playersEntityIds), Owner, getAddressNameFromEntity); + + const uniquePlayers = Array.from(new Map(players.map((player) => [player.address, player])).values()); + + return uniquePlayers; + }; + + return getPlayers; +}; + +export const useGetOtherPlayers = () => { + const { + account: { account }, + setup: { + components: { Owner, Realm }, + }, + } = useDojo(); + const { getAddressNameFromEntity } = getEntitiesUtils(); + + const playersEntityIds = runQuery([ + Has(Owner), + Has(Realm), + NotValue(Owner, { address: ContractAddress(account.address) }), + ]); + + const getPlayers = () => { + const players = getAddressNameFromEntityIds(Array.from(playersEntityIds), Owner, getAddressNameFromEntity); + + const uniquePlayers = Array.from(new Map(players.map((player) => [player.address, player])).values()); + + return uniquePlayers; + }; + + return getPlayers; +}; diff --git a/client/src/hooks/helpers/useGuilds.tsx b/client/src/hooks/helpers/useGuilds.tsx index 7fc77038e..3921e960d 100644 --- a/client/src/hooks/helpers/useGuilds.tsx +++ b/client/src/hooks/helpers/useGuilds.tsx @@ -1,4 +1,5 @@ import { ClientComponents } from "@/dojo/createClientComponents"; +import { toHexString } from "@/ui/utils/utils"; import { ContractAddress, ID } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; import { @@ -17,7 +18,6 @@ import { shortString } from "starknet"; import { useDojo } from "../context/DojoContext"; import { getEntitiesUtils } from "./useEntities"; import { useRealm } from "./useRealm"; -import { toHexString } from "@/ui/utils/utils"; export type GuildAndName = { guild: ComponentValue; @@ -80,6 +80,24 @@ export const useGuilds = () => { return players; }; + const getPlayerListInGuild = (guild_entity_id: ID) => { + const players = Array.from( + runQuery([Has(AddressName), Has(GuildMember), HasValue(Guild, { entity_id: guild_entity_id })]), + ).map((playerEntity) => { + const player = getComponentValue(AddressName, playerEntity); + + const name = shortString.decodeShortString(player!.name.toString()); + const address = toHexString(player?.address || 0n); + + return { + name, + address, + }; + }); + + return players; + }; + const useGuildMembers = useCallback((guildEntityId: ID) => { const guildMembers = useEntityQuery([HasValue(GuildMember, { guild_entity_id: guildEntityId })]); return { @@ -141,6 +159,16 @@ export const useGuilds = () => { return { guild, isOwner, name: guild.name }; }, []); + const getPlayersInPlayersGuild = useCallback((accountAddress: ContractAddress) => { + const guild = getGuildFromPlayerAddress(accountAddress); + if (!guild) return []; + + const guildEntityId = guild.guildEntityId; + if (typeof guildEntityId !== "number") return []; + + return getPlayerListInGuild(guildEntityId); + }, []); + return { useGuildQuery, useGuildMembers, @@ -150,6 +178,7 @@ export const useGuilds = () => { getGuildOwner, getGuildFromEntityId, getPlayerList, + getPlayersInPlayersGuild, }; }; diff --git a/client/src/hooks/helpers/useHyperstructures.tsx b/client/src/hooks/helpers/useHyperstructures.tsx index 28ff6247b..a9da02fb2 100644 --- a/client/src/hooks/helpers/useHyperstructures.tsx +++ b/client/src/hooks/helpers/useHyperstructures.tsx @@ -1,18 +1,20 @@ import { ClientComponents } from "@/dojo/createClientComponents"; import { TOTAL_CONTRIBUTABLE_AMOUNT } from "@/dojo/modelManager/utils/LeaderboardUtils"; +import { toHexString } from "@/ui/utils/utils"; import { + ContractAddress, EternumGlobalConfig, + HYPERSTRUCTURE_RESOURCE_MULTIPLIERS, HYPERSTRUCTURE_TOTAL_COSTS_SCALED, - HyperstructureResourceMultipliers, 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"; +import { useCallback } from "react"; import { shortString } from "starknet"; import { useDojo } from "../context/DojoContext"; -import { toHexString } from "@/ui/utils/utils"; export type ProgressWithPercentage = { percentage: number; @@ -85,19 +87,69 @@ export const useHyperstructures = () => { return { hyperstructures, useProgress, getHyperstructureProgress }; }; -export const useUpdates = (hyperstructureEntityId: ID) => { +export const useHyperstructureUpdates = (hyperstructureEntityId: ID) => { const { setup: { - components: { HyperstructureUpdate }, + components: { Hyperstructure }, }, } = useDojo(); const updates = useEntityQuery([ - Has(HyperstructureUpdate), - HasValue(HyperstructureUpdate, { hyperstructure_entity_id: hyperstructureEntityId }), + Has(Hyperstructure), + HasValue(Hyperstructure, { entity_id: hyperstructureEntityId }), ]); - return updates.map((updateEntityId) => getComponentValue(HyperstructureUpdate, updateEntityId)); + return updates.map((updateEntityId) => getComponentValue(Hyperstructure, updateEntityId)); +}; + +interface ContractAddressAndAmount { + key: false; + type: "tuple"; + type_name: "(ContractAddress, u16)"; + value: [ + { + key: false; + type: "primitive"; + type_name: "ContractAddress"; + value: string; + }, + { + key: false; + type: "primitive"; + type_name: "u16"; + value: number; + }, + ]; +} + +export const useGetPlayerEpochs = () => { + const { + account: { account }, + setup: { + components: { Epoch }, + }, + } = useDojo(); + + const getEpochs = useCallback(() => { + const entityIds = runQuery([Has(Epoch)]); + return Array.from(entityIds) + .map((entityId) => { + const epoch = getComponentValue(Epoch, entityId); + if ( + epoch?.owners.find((arrayElem) => { + const owner = arrayElem as unknown as ContractAddressAndAmount; + if (ContractAddress(owner.value[0].value) === ContractAddress(account.address)) { + return true; + } + }) + ) { + return { hyperstructure_entity_id: epoch?.hyperstructure_entity_id, epoch: epoch?.index }; + } + }) + .filter((epoch): epoch is { hyperstructure_entity_id: ID; epoch: number } => epoch !== undefined); + }, [account.address]); + + return getEpochs; }; const getContributions = (hyperstructureEntityId: ID, Contribution: Component) => { @@ -128,7 +180,9 @@ const getAllProgressesAndTotalPercentage = ( }; percentage += (progress.amount * - HyperstructureResourceMultipliers[progress.resource_type as keyof typeof HyperstructureResourceMultipliers]!) / + HYPERSTRUCTURE_RESOURCE_MULTIPLIERS[ + progress.resource_type as keyof typeof HYPERSTRUCTURE_RESOURCE_MULTIPLIERS + ]!) / TOTAL_CONTRIBUTABLE_AMOUNT; return progress; }); diff --git a/client/src/hooks/helpers/useRealm.tsx b/client/src/hooks/helpers/useRealm.tsx index eb243d0d6..df74e8558 100644 --- a/client/src/hooks/helpers/useRealm.tsx +++ b/client/src/hooks/helpers/useRealm.tsx @@ -1,3 +1,4 @@ +import { ClientComponents } from "@/dojo/createClientComponents"; import { BASE_POPULATION_CAPACITY, ContractAddress, @@ -6,7 +7,7 @@ import { getQuestResources as getStartingResources, } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { Entity, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs"; +import { ComponentValue, Entity, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs"; import { useMemo } from "react"; import { shortString } from "starknet"; import realmIdsByOrder from "../../data/realmids_by_order.json"; @@ -16,6 +17,26 @@ import { getEntityIdFromKeys, getPosition } from "../../ui/utils/utils"; import { useDojo } from "../context/DojoContext"; import useUIStore from "../store/useUIStore"; +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; + population?: number | undefined; + capacity?: number; + hasCapacity: boolean; + owner: ContractAddress; + ownerName: string; +}; + export function useRealm() { const { setup: { @@ -222,3 +243,63 @@ export function useGetRealm(realmEntityId: ID | undefined) { realm, }; } + +export function getRealms(): RealmInfo[] { + const { + setup: { + components: { Realm, Position, Owner, Population, AddressName, Structure }, + }, + } = useDojo(); + + const realmEntities = runQuery([Has(Realm)]); + + return Array.from(realmEntities) + .map((entity) => { + const realm = getComponentValue(Realm, entity); + const owner = getComponentValue(Owner, entity); + const position = getComponentValue(Position, entity); + const population = getComponentValue(Population, entity); + + if (!realm || !owner || !position) return; + + const { + realm_id, + entity_id, + cities, + rivers, + wonder, + harbors, + regions, + resource_types_count, + resource_types_packed, + order, + } = realm; + + const name = getRealmNameById(realm_id); + + const { address } = owner; + + const addressName = getComponentValue(AddressName, getEntityIdFromKeys([address])); + const ownerName = shortString.decodeShortString(addressName?.name.toString() ?? "0x0"); + + return { + realmId: realm_id, + entityId: entity_id, + name, + cities, + rivers, + wonder, + harbors, + regions, + resourceTypesCount: resource_types_count, + resourceTypesPacked: resource_types_packed, + order, + position, + ...population, + hasCapacity: !population || population.capacity + BASE_POPULATION_CAPACITY > population.population, + owner: address, + ownerName, + }; + }) + .filter((realm) => realm !== undefined); +} diff --git a/client/src/hooks/helpers/useResources.tsx b/client/src/hooks/helpers/useResources.tsx index c62fd6a12..b7e348802 100644 --- a/client/src/hooks/helpers/useResources.tsx +++ b/client/src/hooks/helpers/useResources.tsx @@ -115,11 +115,11 @@ export const usePlayerArrivals = () => { const [entitiesWithInventory, setEntitiesWithInventory] = useState([]); - const fragments = [NotValue(OwnedResourcesTracker, { resource_types: 0n }), Has(Weight), Has(ArrivalTime)]; + const queryFragments = [NotValue(OwnedResourcesTracker, { resource_types: 0n }), Has(Weight), Has(ArrivalTime)]; const getArrivalsWithResourceOnPosition = useCallback((positions: Position[]) => { return positions.flatMap((position) => { - return Array.from(runQuery([HasValue(Position, { x: position.x, y: position.y }), ...fragments])); + return Array.from(runQuery([HasValue(Position, { x: position.x, y: position.y }), ...queryFragments])); }); }, []); @@ -148,12 +148,11 @@ export const usePlayerArrivals = () => { const arrivals = getArrivalsWithResourceOnPosition(playerStructurePositions) .map(createArrivalInfo) .filter((arrival: any): arrival is ArrivalInfo => arrival !== undefined); - setEntitiesWithInventory(arrivals); }, [playerStructurePositions]); useEffect(() => { - const query = defineQuery([Has(Position), ...fragments], { runOnInit: false }); + const query = defineQuery([Has(Position), ...queryFragments], { runOnInit: false }); const sub = query.update$.subscribe((update) => { if (isComponentUpdate(update, Position)) { diff --git a/client/src/hooks/helpers/useStructureEntityId.tsx b/client/src/hooks/helpers/useStructureEntityId.tsx index 5a33691b2..4f2a4dcdb 100644 --- a/client/src/hooks/helpers/useStructureEntityId.tsx +++ b/client/src/hooks/helpers/useStructureEntityId.tsx @@ -23,7 +23,7 @@ export const useStructureEntityId = () => { const { playerStructures } = useEntities(); - const structures = useMemo(() => playerStructures(), [playerStructures]); + const structures = playerStructures(); const defaultPlayerStructure = useMemo(() => { return structures[0]; diff --git a/client/src/hooks/helpers/useStructures.tsx b/client/src/hooks/helpers/useStructures.tsx index 877552d2c..463657430 100644 --- a/client/src/hooks/helpers/useStructures.tsx +++ b/client/src/hooks/helpers/useStructures.tsx @@ -4,12 +4,13 @@ import { getRealm, getRealmNameById } from "@/ui/utils/realms"; import { calculateDistance, currentTickCount } from "@/ui/utils/utils"; import { ContractAddress, EternumGlobalConfig, ID, Position, StructureType } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { ComponentValue, Has, HasValue, getComponentValue, runQuery } from "@dojoengine/recs"; +import { ComponentValue, Has, HasValue, NotValue, getComponentValue, runQuery } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useMemo } from "react"; import { shortString } from "starknet"; import { useDojo } from "../context/DojoContext"; import { ArmyInfo, getArmyByEntityId } from "./useArmies"; +import { getEntitiesUtils } from "./useEntities"; export type Structure = ComponentValue & { isMine: boolean; diff --git a/client/src/hooks/store/useUIStore.tsx b/client/src/hooks/store/useUIStore.tsx index 8669378f3..4ee4c437f 100644 --- a/client/src/hooks/store/useUIStore.tsx +++ b/client/src/hooks/store/useUIStore.tsx @@ -1,14 +1,16 @@ +import { DEFAULT_TAB, Tab } from "@/ui/modules/chat/ChatTab"; import { View as LeftView } from "@/ui/modules/navigation/LeftNavigationModule"; import { View as RightView } from "@/ui/modules/navigation/RightNavigationModule"; +import { ContractAddress } from "@bibliothecadao/eternum"; import React from "react"; import { create } from "zustand"; import { subscribeWithSelector } from "zustand/middleware"; import { BuildModeStore, createBuildModeStoreSlice } from "./_buildModeStore"; -import { createPopupsSlice, PopupsStore } from "./_popupsStore"; -import { createThreeStoreSlice, ThreeStore } from "./_threeStore"; +import { PopupsStore, createPopupsSlice } from "./_popupsStore"; +import { ThreeStore, createThreeStoreSlice } from "./_threeStore"; import { BattleViewInfo } from "./types"; import { BlockchainStore, createBlockchainStore } from "./useBlockchainStore"; -import { createRealmStoreSlice, RealmStore } from "./useRealmStore"; +import { RealmStore, createRealmStoreSlice } from "./useRealmStore"; interface UIStore { theme: string; @@ -51,6 +53,14 @@ interface UIStore { setLeftNavigationView: (view: LeftView) => void; rightNavigationView: RightView; setRightNavigationView: (view: RightView) => void; + showMinimap: boolean; + setShowMinimap: (show: boolean) => void; + selectedPlayer: ContractAddress | null; + setSelectedPlayer: (player: ContractAddress | null) => void; + tabs: Tab[]; + setTabs: (tabs: Tab[]) => void; + currentTab: Tab; + setCurrentTab: (tab: Tab) => void; } export type AppStore = UIStore & PopupsStore & ThreeStore & BuildModeStore & RealmStore & BlockchainStore; @@ -104,6 +114,14 @@ const useUIStore = create( setLeftNavigationView: (view: LeftView) => set({ leftNavigationView: view }), rightNavigationView: RightView.None, setRightNavigationView: (view: RightView) => set({ rightNavigationView: view }), + showMinimap: false, + setShowMinimap: (show: boolean) => set({ showMinimap: show }), + selectedPlayer: null, + setSelectedPlayer: (player: ContractAddress | null) => set({ selectedPlayer: player }), + tabs: [], + setTabs: (tabs: Tab[]) => set({ tabs }), + currentTab: DEFAULT_TAB, + setCurrentTab: (tab: Tab) => set({ currentTab: tab }), ...createPopupsSlice(set, get), ...createThreeStoreSlice(set, get), ...createBuildModeStoreSlice(set), diff --git a/client/src/index.css b/client/src/index.css index 6f1a1aec1..60f789bb0 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -208,7 +208,7 @@ h6 { #root { position: relative; - z-index: 2; + z-index: 3; } canvas { diff --git a/client/src/three/GameRenderer.ts b/client/src/three/GameRenderer.ts index 9b9770316..38e3fa29e 100644 --- a/client/src/three/GameRenderer.ts +++ b/client/src/three/GameRenderer.ts @@ -214,7 +214,6 @@ export default class GameRenderer { this.hudScene = new HUDScene(this.sceneManager, this.controls); this.renderModels(); - // Init animation this.animate(); } @@ -321,6 +320,7 @@ export default class GameRenderer { this.renderer.render(this.hudScene.getScene(), this.hudScene.getCamera()); this.labelRenderer.render(this.hudScene.getScene(), this.hudScene.getCamera()); + // Update the minimap requestAnimationFrame(() => { this.animate(); }); diff --git a/client/src/three/SceneManager.ts b/client/src/three/SceneManager.ts index 7a61f755c..4de688f10 100644 --- a/client/src/three/SceneManager.ts +++ b/client/src/three/SceneManager.ts @@ -26,6 +26,10 @@ export class SceneManager { switchScene(sceneName: SceneName) { const scene = this.scenes.get(sceneName); if (scene) { + const previousScene = this.scenes.get(this.currentScene!); + if (previousScene) { + previousScene.onSwitchOff(); + } this.transitionManager.fadeOut(() => { this._updateCurrentScene(sceneName); if (scene.setup) { diff --git a/client/src/three/components/ArmyManager.ts b/client/src/three/components/ArmyManager.ts index 063f24503..eb494ffdf 100644 --- a/client/src/three/components/ArmyManager.ts +++ b/client/src/three/components/ArmyManager.ts @@ -347,6 +347,10 @@ export class ArmyManager { this.cachedChunks.clear(); } + public getArmies() { + return Array.from(this.armies.values()); + } + update(deltaTime: number) { let needsBoundingUpdate = false; const movementSpeed = 1.25; // Constant movement speed diff --git a/client/src/three/components/Minimap.ts b/client/src/three/components/Minimap.ts new file mode 100644 index 000000000..a34284c3f --- /dev/null +++ b/client/src/three/components/Minimap.ts @@ -0,0 +1,384 @@ +import useUIStore from "@/hooks/store/useUIStore"; +import { FELT_CENTER } from "@/ui/config"; +import { getHexForWorldPosition } from "@/ui/utils/utils"; +import { throttle } from "lodash"; +import * as THREE from "three"; +import WorldmapScene from "../scenes/Worldmap"; +import { ArmyManager } from "./ArmyManager"; +import { Biome, BIOME_COLORS } from "./Biome"; +import { StructureManager } from "./StructureManager"; + +const MINIMAP_CONFIG = { + MIN_ZOOM_RANGE: 75, + MAX_ZOOM_RANGE: 300, + MAP_COLS_WIDTH: 200, + MAP_ROWS_HEIGHT: 100, + COLORS: { + ARMY: "#FF0000", + MY_ARMY: "#00FF00", + CAMERA: "#FFFFFF", + STRUCTURES: { + Realm: "#0000ff", + Hyperstructure: "#FFFFFF", + Bank: "#FFFF00", + FragmentMine: "#00FFFF", + Settlement: "#FFA500", + }, + }, + SIZES: { + STRUCTURE: 2, + ARMY: 2, + CAMERA: { + TOP_SIDE_WIDTH_FACTOR: 105, + BOTTOM_SIDE_WIDTH_FACTOR: 170, + HEIGHT_FACTOR: 13, + }, + }, + BORDER_WIDTH_PERCENT: 0.1, +}; + +class Minimap { + private worldmapScene: WorldmapScene; + private canvas!: HTMLCanvasElement; + private context!: CanvasRenderingContext2D; + private camera!: THREE.PerspectiveCamera; + private exploredTiles!: Map>; + private structureManager!: StructureManager; + private armyManager!: ArmyManager; + private biome!: Biome; + private displayRange: any = { + minCol: 150, + maxCol: 350, + minRow: 100, + maxRow: 200, + }; + private scaleX!: number; + private scaleY!: number; + private isDragging: boolean = false; + private biomeCache!: Map; + private scaledCoords!: Map; + private BORDER_WIDTH_PERCENT = MINIMAP_CONFIG.BORDER_WIDTH_PERCENT; + private structureSize!: { width: number; height: number }; + private armySize!: { width: number; height: number }; + private cameraSize!: { + topSideWidth: number; + bottomSideWidth: number; + height: number; + }; + + constructor( + worldmapScene: WorldmapScene, + exploredTiles: Map>, + camera: THREE.PerspectiveCamera, + structureManager: StructureManager, + armyManager: ArmyManager, + biome: Biome, + ) { + this.worldmapScene = worldmapScene; + this.waitForMinimapElement().then((canvas) => { + this.canvas = canvas; + this.initializeCanvas(structureManager, exploredTiles, armyManager, biome, camera); + }); + } + + private async waitForMinimapElement(): Promise { + return new Promise((resolve) => { + const checkElement = () => { + const element = document.getElementById("minimap") as HTMLCanvasElement; + if (element) { + resolve(element); + } else { + requestAnimationFrame(checkElement); + } + }; + checkElement(); + }); + } + + private initializeCanvas( + structureManager: StructureManager, + exploredTiles: Map>, + armyManager: ArmyManager, + biome: Biome, + camera: THREE.PerspectiveCamera, + ) { + this.context = this.canvas.getContext("2d")!; + this.structureManager = structureManager; + this.exploredTiles = exploredTiles; + this.armyManager = armyManager; + this.biome = biome; + this.camera = camera; + this.scaleX = this.canvas.width / (this.displayRange.maxCol - this.displayRange.minCol); + this.scaleY = this.canvas.height / (this.displayRange.maxRow - this.displayRange.minRow); + this.biomeCache = new Map(); + this.scaledCoords = new Map(); + this.structureSize = { width: 0, height: 0 }; + this.armySize = { width: 0, height: 0 }; + this.cameraSize = { topSideWidth: 0, bottomSideWidth: 0, height: 0 }; + this.recomputeScales(); + + this.draw = throttle(this.draw, 1000 / 30); + + this.canvas.addEventListener("click", this.handleClick); + this.canvas.addEventListener("mousedown", this.handleMouseDown); + this.canvas.addEventListener("mousemove", this.handleMouseMove); + this.canvas.addEventListener("mouseup", this.handleMouseUp); + this.canvas.addEventListener("wheel", this.handleWheel); + } + + private recomputeScales() { + this.scaleX = this.canvas.width / (this.displayRange.maxCol - this.displayRange.minCol); + this.scaleY = this.canvas.height / (this.displayRange.maxRow - this.displayRange.minRow); + this.scaledCoords.clear(); + for (let col = this.displayRange.minCol; col <= this.displayRange.maxCol; col++) { + for (let row = this.displayRange.minRow; row <= this.displayRange.maxRow; row++) { + const scaledCol = (col - this.displayRange.minCol) * this.scaleX; + const scaledRow = (row - this.displayRange.minRow) * this.scaleY; + this.scaledCoords.set(`${col},${row}`, { scaledCol, scaledRow }); + } + } + + // Precompute sizes + this.structureSize = { + width: MINIMAP_CONFIG.SIZES.STRUCTURE * this.scaleX, + height: MINIMAP_CONFIG.SIZES.STRUCTURE * this.scaleY, + }; + this.armySize = { + width: MINIMAP_CONFIG.SIZES.ARMY * this.scaleX, + height: MINIMAP_CONFIG.SIZES.ARMY * this.scaleY, + }; + this.cameraSize = { + topSideWidth: (window.innerWidth / MINIMAP_CONFIG.SIZES.CAMERA.TOP_SIDE_WIDTH_FACTOR) * this.scaleX, + bottomSideWidth: (window.innerWidth / MINIMAP_CONFIG.SIZES.CAMERA.BOTTOM_SIDE_WIDTH_FACTOR) * this.scaleX, + height: MINIMAP_CONFIG.SIZES.CAMERA.HEIGHT_FACTOR * this.scaleY, + }; + } + + private getMousePosition(event: MouseEvent) { + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const col = Math.floor(x / this.scaleX) + this.displayRange.minCol; + const row = Math.floor(y / this.scaleY) + this.displayRange.minRow; + return { col, row, x, y }; + } + + draw() { + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.drawExploredTiles(); + this.drawStructures(); + this.drawArmies(); + this.drawCamera(); + } + + private drawExploredTiles() { + this.exploredTiles.forEach((rows, col) => { + rows.forEach((row) => { + const cacheKey = `${col},${row}`; + let biomeColor; + + if (this.biomeCache.has(cacheKey)) { + biomeColor = this.biomeCache.get(cacheKey)!; + } else { + const biome = this.biome.getBiome(col + FELT_CENTER, row + FELT_CENTER); + biomeColor = BIOME_COLORS[biome].getStyle(); + this.biomeCache.set(cacheKey, biomeColor); + } + if (this.scaledCoords.has(cacheKey)) { + const { scaledCol, scaledRow } = this.scaledCoords.get(cacheKey)!; + this.context.fillStyle = biomeColor; + this.context.fillRect( + scaledCol - this.scaleX * (row % 2 !== 0 ? 1 : 0.5), + scaledRow - this.scaleY / 2, + this.scaleX, + this.scaleY, + ); + } + }); + }); + } + + private drawStructures() { + const allStructures = this.structureManager.structures.getStructures(); + for (const [structureType, structures] of allStructures) { + structures.forEach((structure) => { + const { col, row } = structure.hexCoords; + const cacheKey = `${col},${row}`; + if (this.scaledCoords.has(cacheKey)) { + const { scaledCol, scaledRow } = this.scaledCoords.get(cacheKey)!; + // @ts-ignore + this.context.fillStyle = MINIMAP_CONFIG.COLORS.STRUCTURES[structureType]; + this.context.fillRect( + scaledCol - this.structureSize.width * (row % 2 !== 0 ? 1 : 0.5), + scaledRow - this.structureSize.height / 2, + this.structureSize.width, + this.structureSize.height, + ); + } + }); + } + } + + private drawArmies() { + const allArmies = this.armyManager.getArmies(); + allArmies.forEach((army) => { + const { x: col, y: row } = army.hexCoords.getNormalized(); + const cacheKey = `${col},${row}`; + if (this.scaledCoords.has(cacheKey)) { + const { scaledCol, scaledRow } = this.scaledCoords.get(cacheKey)!; + this.context.fillStyle = army.isMine ? MINIMAP_CONFIG.COLORS.MY_ARMY : MINIMAP_CONFIG.COLORS.ARMY; + this.context.fillRect( + scaledCol - this.armySize.width * (row % 2 !== 0 ? 1 : 0.5), + scaledRow - this.armySize.height / 2, + this.armySize.width, + this.armySize.height, + ); + } + }); + } + + drawCamera() { + const cameraPosition = this.camera.position; + const { col, row } = getHexForWorldPosition(cameraPosition); + const cacheKey = `${col},${row}`; + if (this.scaledCoords.has(cacheKey)) { + const { scaledCol, scaledRow } = this.scaledCoords.get(cacheKey)!; + + this.context.strokeStyle = MINIMAP_CONFIG.COLORS.CAMERA; + this.context.beginPath(); + this.context.moveTo(scaledCol - this.cameraSize.topSideWidth / 2, scaledRow - this.cameraSize.height); + this.context.lineTo(scaledCol + this.cameraSize.topSideWidth / 2, scaledRow - this.cameraSize.height); + this.context.lineTo(scaledCol + this.cameraSize.bottomSideWidth / 2, scaledRow); + this.context.lineTo(scaledCol - this.cameraSize.bottomSideWidth / 2, scaledRow); + this.context.lineTo(scaledCol - this.cameraSize.topSideWidth / 2, scaledRow - this.cameraSize.height); + this.context.closePath(); + this.context.lineWidth = 1; + this.context.stroke(); + } + } + + hideMinimap() { + this.canvas.style.display = "none"; + useUIStore.getState().setShowMinimap(false); + } + + showMinimap() { + this.canvas.style.display = "block"; + useUIStore.getState().setShowMinimap(true); + } + + moveMinimapCenterToUrlLocation() { + const url = new URL(window.location.href); + const col = parseInt(url.searchParams.get("col") || "0"); + const row = parseInt(url.searchParams.get("row") || "0"); + this.displayRange.minCol = col - MINIMAP_CONFIG.MAP_COLS_WIDTH / 2; + this.displayRange.maxCol = col + MINIMAP_CONFIG.MAP_COLS_WIDTH / 2; + this.displayRange.minRow = row - MINIMAP_CONFIG.MAP_ROWS_HEIGHT / 2; + this.displayRange.maxRow = row + MINIMAP_CONFIG.MAP_ROWS_HEIGHT / 2; + this.recomputeScales(); + } + + update() { + this.draw(); + } + + private handleMouseDown = (event: MouseEvent) => { + this.isDragging = true; + this.moveCamera(event); + }; + + private handleMouseMove = (event: MouseEvent) => { + if (this.isDragging) { + this.moveCamera(event); + } + }; + + private handleMouseUp = () => { + this.isDragging = false; + }; + + private moveCamera(event: MouseEvent) { + const { col, row } = this.getMousePosition(event); + this.worldmapScene.moveCameraToColRow(col, row, 0); + } + + private moveMapRange(direction: string) { + const colShift = (this.displayRange.maxCol - this.displayRange.minCol) / 4; + const rowShift = (this.displayRange.maxRow - this.displayRange.minRow) / 4; + + switch (direction) { + case "left": + this.displayRange.minCol -= colShift; + this.displayRange.maxCol -= colShift; + break; + case "right": + this.displayRange.minCol += colShift; + this.displayRange.maxCol += colShift; + break; + case "top": + this.displayRange.minRow -= rowShift; + this.displayRange.maxRow -= rowShift; + break; + case "bottom": + this.displayRange.minRow += rowShift; + this.displayRange.maxRow += rowShift; + break; + default: + return; + } + this.recomputeScales(); + } + + private handleWheel = (event: WheelEvent) => { + event.stopPropagation(); + const zoomOut = event.deltaY > 0; // Zoom out for positive deltaY, zoom in for negative + this.zoom(zoomOut); + }; + + private zoom(zoomOut: boolean) { + const currentRange = Math.abs(this.displayRange.maxCol - this.displayRange.minCol); + console.log( + `Zooming ${zoomOut ? "out" : "in"} from ${currentRange}, minCol: ${this.displayRange.minCol}, maxCol: ${ + this.displayRange.maxCol + }`, + ); + if (!zoomOut && currentRange < MINIMAP_CONFIG.MIN_ZOOM_RANGE) { + return; + } + if (zoomOut && currentRange > MINIMAP_CONFIG.MAX_ZOOM_RANGE) { + return; + } + + const ratio = this.canvas.width / this.canvas.height; + const delta = zoomOut ? -5 : 5; + const deltaX = Math.round(delta * ratio); + const deltaY = delta; + this.displayRange.minCol = this.displayRange.minCol + deltaX; + this.displayRange.maxCol = this.displayRange.maxCol - deltaX; + this.displayRange.minRow = this.displayRange.minRow + deltaY; + this.displayRange.maxRow = this.displayRange.maxRow - deltaY; + + this.recomputeScales(); + } + + handleClick = (event: MouseEvent) => { + event.stopPropagation(); + const { col, row, x, y } = this.getMousePosition(event); + + const borderWidthX = this.canvas.width * this.BORDER_WIDTH_PERCENT; + const borderWidthY = this.canvas.height * this.BORDER_WIDTH_PERCENT; + + if (x < borderWidthX) { + this.moveMapRange("left"); + } else if (x > this.canvas.width - borderWidthX) { + this.moveMapRange("right"); + } else if (y < borderWidthY) { + this.moveMapRange("top"); + } else if (y > this.canvas.height - borderWidthY) { + this.moveMapRange("bottom"); + } + this.worldmapScene.moveCameraToColRow(col, row, 0); + }; +} + +export default Minimap; diff --git a/client/src/three/scenes/HexagonScene.ts b/client/src/three/scenes/HexagonScene.ts index 068678aa4..254f94a31 100644 --- a/client/src/three/scenes/HexagonScene.ts +++ b/client/src/three/scenes/HexagonScene.ts @@ -428,4 +428,5 @@ export abstract class HexagonScene { protected abstract onHexagonRightClick(hexCoords: HexPosition): void; public abstract setup(): void; public abstract moveCameraToURLLocation(): void; + public abstract onSwitchOff(): void; } diff --git a/client/src/three/scenes/Hexception.ts b/client/src/three/scenes/Hexception.ts index 161334e10..a15e2a17f 100644 --- a/client/src/three/scenes/Hexception.ts +++ b/client/src/three/scenes/Hexception.ts @@ -218,6 +218,8 @@ export default class HexceptionScene extends HexagonScene { this.moveCameraToURLLocation(); } + onSwitchOff() {} + protected onHexagonClick(hexCoords: HexPosition | null): void { if (hexCoords === null) return; const normalizedCoords = { col: hexCoords.col, row: hexCoords.row }; diff --git a/client/src/three/scenes/Worldmap.ts b/client/src/three/scenes/Worldmap.ts index 4bd02c732..13c59e297 100644 --- a/client/src/three/scenes/Worldmap.ts +++ b/client/src/three/scenes/Worldmap.ts @@ -11,13 +11,14 @@ 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, ID, neighborOffsetsEven, neighborOffsetsOdd } from "@bibliothecadao/eternum"; +import { BiomeType, ID, NEIGHBOR_OFFSETS_EVEN, NEIGHBOR_OFFSETS_ODD } from "@bibliothecadao/eternum"; import { throttle } from "lodash"; import { MapControls } from "three/examples/jsm/controls/MapControls"; import { SceneManager } from "../SceneManager"; import { ArmyManager } from "../components/ArmyManager"; import { BattleManager } from "../components/BattleManager"; import { Biome } from "../components/Biome"; +import Minimap from "../components/Minimap"; import { SelectedHexManager } from "../components/SelectedHexManager"; import { StructureManager } from "../components/StructureManager"; import { StructurePreview } from "../components/StructurePreview"; @@ -49,6 +50,7 @@ export default class WorldmapScene extends HexagonScene { private structureEntityId: ID = UNDEFINED_STRUCTURE_ENTITY_ID; private armySubscription: any; private selectedHexManager: SelectedHexManager; + private minimap!: Minimap; private cachedMatrices: Map> = new Map(); @@ -147,6 +149,15 @@ export default class WorldmapScene extends HexagonScene { }, ); + this.minimap = new Minimap( + this, + this.exploredTiles, + this.camera, + this.structureManager, + this.armyManager, + this.biome, + ); + // Add event listener for Escape key document.addEventListener("keydown", (event) => { if (event.key === "Escape" && this.sceneManager.getCurrentScene() === SceneName.WorldMap) { @@ -278,6 +289,12 @@ export default class WorldmapScene extends HexagonScene { this.controls.enablePan = true; this.controls.zoomToCursor = true; this.moveCameraToURLLocation(); + this.minimap.moveMinimapCenterToUrlLocation(); + this.minimap.showMinimap(); + } + + onSwitchOff() { + this.minimap.hideMinimap(); } public async updateExploredHex(update: TileSystemUpdate) { @@ -335,7 +352,7 @@ export default class WorldmapScene extends HexagonScene { this.interactiveHexManager.addHex({ col, row }); // Add border hexes for newly explored hex - const neighborOffsets = row % 2 === 0 ? neighborOffsetsEven : neighborOffsetsOdd; + const neighborOffsets = row % 2 === 0 ? NEIGHBOR_OFFSETS_EVEN : NEIGHBOR_OFFSETS_ODD; neighborOffsets.forEach(({ i, j }) => { const neighborCol = col + i; @@ -426,7 +443,7 @@ export default class WorldmapScene extends HexagonScene { } if (!isExplored) { - const neighborOffsets = globalRow % 2 === 0 ? neighborOffsetsEven : neighborOffsetsOdd; + const neighborOffsets = globalRow % 2 === 0 ? NEIGHBOR_OFFSETS_EVEN : NEIGHBOR_OFFSETS_ODD; const isBorder = neighborOffsets.some(({ i, j }) => { const neighborCol = globalCol + i; const neighborRow = globalRow + j; @@ -538,5 +555,6 @@ export default class WorldmapScene extends HexagonScene { this.armyManager.update(deltaTime); this.selectedHexManager.update(deltaTime); this.battleManager.update(deltaTime); + this.minimap.update(); } } diff --git a/client/src/three/systems/SystemManager.ts b/client/src/three/systems/SystemManager.ts index d60747f31..4042ab4a3 100644 --- a/client/src/three/systems/SystemManager.ts +++ b/client/src/three/systems/SystemManager.ts @@ -5,19 +5,19 @@ import { HexPosition } from "@/types"; import { Position } from "@/types/Position"; import { EternumGlobalConfig, + HYPERSTRUCTURE_RESOURCE_MULTIPLIERS, HYPERSTRUCTURE_TOTAL_COSTS_SCALED, - HyperstructureResourceMultipliers, ID, StructureType, } from "@bibliothecadao/eternum"; import { Component, ComponentValue, + Has, + HasValue, defineComponentSystem, defineQuery, getComponentValue, - Has, - HasValue, isComponentUpdate, runQuery, } from "@dojoengine/recs"; @@ -279,8 +279,8 @@ export class SystemManager { }; percentage += (progress.amount * - HyperstructureResourceMultipliers[ - progress.resource_type as keyof typeof HyperstructureResourceMultipliers + HYPERSTRUCTURE_RESOURCE_MULTIPLIERS[ + progress.resource_type as keyof typeof HYPERSTRUCTURE_RESOURCE_MULTIPLIERS ]!) / TOTAL_CONTRIBUTABLE_AMOUNT; return progress; diff --git a/client/src/ui/components/bank/ResourceBar.tsx b/client/src/ui/components/bank/ResourceBar.tsx index b3f8d7545..043220989 100644 --- a/client/src/ui/components/bank/ResourceBar.tsx +++ b/client/src/ui/components/bank/ResourceBar.tsx @@ -1,23 +1,13 @@ import { getResourceBalance } from "@/hooks/helpers/useResources"; +import { NumberInput } from "@/ui/elements/NumberInput"; import { ResourceCost } from "@/ui/elements/ResourceCost"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui/elements/Select"; import TextInput from "@/ui/elements/TextInput"; import { divideByPrecision, formatNumber } from "@/ui/utils/utils"; import { ID, Resources, ResourcesIds, findResourceById, findResourceIdByTrait } from "@bibliothecadao/eternum"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { HintSection } from "../hints/HintModal"; -type ResourceBarProps = { - entityId: ID; - lordsFee: number; - resources: Resources[]; - resourceId: ResourcesIds; - setResourceId: (resourceId: ResourcesIds) => void; - amount: number; - setAmount: (amount: number) => void; - disableInput?: boolean; -}; - export const ResourceBar = ({ entityId, lordsFee, @@ -27,41 +17,92 @@ export const ResourceBar = ({ amount, setAmount, disableInput = false, -}: ResourceBarProps) => { - console.log({ resourceId }); + onFocus, + onBlur, +}: { + entityId: ID; + lordsFee: number; + resources: Resources[]; + resourceId: ResourcesIds; + setResourceId: (resourceId: ResourcesIds) => void; + amount: number; + setAmount: (amount: number) => void; + disableInput?: boolean; + onFocus?: () => void; // New prop + onBlur?: () => void; // New prop +}) => { const { getBalance } = getResourceBalance(); const [selectedResourceBalance, setSelectedResourceBalance] = useState(0); + const [searchInput, setSearchInput] = useState(""); + const [open, setOpen] = useState(false); + + const inputRef = useRef(null); useEffect(() => { setSelectedResourceBalance(divideByPrecision(getBalance(entityId, Number(resourceId)).balance)); - }, [resourceId]); + }, [resourceId, getBalance, entityId]); const handleResourceChange = (trait: string) => { - const resourceId = findResourceIdByTrait(trait); - setResourceId && setResourceId(resourceId); + const newResourceId = findResourceIdByTrait(trait); + setResourceId(newResourceId); }; - const handleAmountChange = (amount: string) => { - !disableInput && setAmount && setAmount(parseInt(amount.replaceAll(" ", "")) || 0); + const handleAmountChange = (amount: number) => { + setAmount(amount); }; const hasLordsFees = lordsFee > 0 && resourceId === ResourcesIds.Lords; const finalResourceBalance = hasLordsFees ? selectedResourceBalance - lordsFee : selectedResourceBalance; + const filteredResources = resources.filter( + (resource) => resource.trait.toLowerCase().startsWith(searchInput.toLowerCase()) || resource.id === resourceId, + ); + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + if (newOpen && inputRef.current) { + setResourceId(ResourcesIds.Wood); + setSearchInput(""); + setTimeout(() => { + inputRef.current?.focus(); + }, 0); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + if (filteredResources.length > 0) { + const selectedResource = filteredResources.find((resource) => resource.id !== resourceId); + if (selectedResource) { + setResourceId(selectedResource.id); + setOpen(false); + } + } + setSearchInput(""); + } else { + e.stopPropagation(); + } + }; + return ( -
+
- handleAmountChange(amount)} + value={amount} + onChange={handleAmountChange} + max={Infinity} + arrows={false} + allowDecimals + onFocus={onFocus} + onBlur={onBlur} /> {!disableInput && (
handleAmountChange(finalResourceBalance.toString())} + className="flex text-xs text-gold/70 mt-1 justify-center items-center relative text-center self-center mx-auto w-full cursor-pointer" + onClick={() => handleAmountChange(finalResourceBalance)} > Max: {isNaN(selectedResourceBalance) ? "0" : selectedResourceBalance.toLocaleString()} {hasLordsFees && ( @@ -74,15 +115,31 @@ export const ResourceBar = ({
@@ -54,19 +55,22 @@ export const Buildings = () => { {buildingTable.map((building) => ( - + - - {resourceTable.map((resource) => ( - - - - - - ))} + {resourceTable.map((resource) => { + const decimals = resource.amount > EternumGlobalConfig.resources.resourcePrecision ? 0 : 2; + return ( + + + + + + ); + })}
{" "}
{BuildingEnumToString[building.building_category]}
+ {building.building_capacity !== 0 && ( <> - Housing: + {building.building_capacity}
+

Max population capacity:

+

+ + {building.building_capacity}
+

)} {building.building_population !== 0 && <>Population: +{building.building_population}} diff --git a/client/src/ui/components/hints/Resources.tsx b/client/src/ui/components/hints/Resources.tsx index cdd33f984..353ad05ad 100644 --- a/client/src/ui/components/hints/Resources.tsx +++ b/client/src/ui/components/hints/Resources.tsx @@ -1,7 +1,7 @@ import { Headline } from "@/ui/elements/Headline"; import { ResourceCost } from "@/ui/elements/ResourceCost"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; -import { currencyFormat, gramToKg } from "@/ui/utils/utils"; +import { currencyFormat, gramToKg, multiplyByPrecision } from "@/ui/utils/utils"; import { CapacityConfigCategory, EternumGlobalConfig, @@ -78,6 +78,7 @@ const ResourceTable = () => { resource_type: resourceId, cost: RESOURCE_INPUTS_SCALED[resourceId].map((cost: any) => ({ ...cost, + amount: multiplyByPrecision(cost.amount), })), }; @@ -97,25 +98,30 @@ const ResourceTable = () => {
- - {currencyFormat(resource.amount, 0)} - {resource.cost.map((cost: any, resource_type: any) => ( -
- -
- ))} -
+ + {currencyFormat(resource.amount, decimals)} + {resource.cost.map((cost, index) => { + return ( +
+ +
+ ); + })} +
); diff --git a/client/src/ui/components/hints/TheMap.tsx b/client/src/ui/components/hints/TheMap.tsx index 90d3ef106..33ed8a2d1 100644 --- a/client/src/ui/components/hints/TheMap.tsx +++ b/client/src/ui/components/hints/TheMap.tsx @@ -1,7 +1,9 @@ +import { ReactComponent as Lightning } from "@/assets/icons/common/lightning.svg"; import { Headline } from "@/ui/elements/Headline"; +import { ResourceIcon } from "@/ui/elements/ResourceIcon"; +import { multiplyByPrecision } from "@/ui/utils/utils"; +import { EternumGlobalConfig, ResourcesIds } from "@bibliothecadao/eternum"; import { tableOfContents } from "./utils"; -import { EXPLORATION_COSTS, EternumGlobalConfig } from "@bibliothecadao/eternum"; -import { ResourceCost } from "@/ui/elements/ResourceCost"; export const TheMap = () => { const chapters = [ @@ -41,18 +43,17 @@ export const TheMap = () => { }; const ExplorationTable = () => { - const explorationCosts = EXPLORATION_COSTS.map((cost) => ({ - ...cost, - })); + const exploreFishBurn = multiplyByPrecision(EternumGlobalConfig.exploration.exploreFishBurn); + const exploreWheatBurn = multiplyByPrecision(EternumGlobalConfig.exploration.exploreWheatBurn); - const exploreCost = EternumGlobalConfig.stamina.exploreCost; - const travelCost = EternumGlobalConfig.stamina.travelCost; + const travelFishBurn = multiplyByPrecision(EternumGlobalConfig.exploration.travelFishBurn); + const travelWheatBurn = multiplyByPrecision(EternumGlobalConfig.exploration.travelWheatBurn); return ( - +
- + @@ -60,17 +61,41 @@ const ExplorationTable = () => { - - + - - + +
Stamina Resources
Exploration{exploreCost} - {explorationCosts.map((cost, index) => ( - - ))} + +
+ +

{EternumGlobalConfig.stamina.exploreCost}

+
+
+
+ +

{exploreWheatBurn} / unit

+
+
+ +

{exploreFishBurn} / unit

+
Travel{travelCost}None +
+ +

{EternumGlobalConfig.stamina.travelCost}

+
+
+
+ +

{travelWheatBurn} / unit

+
+
+ +

{travelFishBurn} / unit

+
+
diff --git a/client/src/ui/components/hyperstructures/CoOwners.tsx b/client/src/ui/components/hyperstructures/CoOwners.tsx index 2987cd2b5..7d290f3a7 100644 --- a/client/src/ui/components/hyperstructures/CoOwners.tsx +++ b/client/src/ui/components/hyperstructures/CoOwners.tsx @@ -57,7 +57,7 @@ const CoOwnersRows = ({ const { account: { account }, setup: { - components: { HyperstructureUpdate, HyperstructureConfig }, + components: { Hyperstructure, HyperstructureConfig }, }, } = useDojo(); const setTooltip = useUIStore((state) => state.setTooltip); @@ -68,23 +68,19 @@ const CoOwnersRows = ({ return getComponentValue(HyperstructureConfig, getEntityIdFromKeys([HYPERSTRUCTURE_CONFIG_ID])); }, [hyperstructureEntityId]); - const hyperstructureUpdate = useComponentValue( - HyperstructureUpdate, - getEntityIdFromKeys([BigInt(hyperstructureEntityId)]), - ); + const hyperstructure = useComponentValue(Hyperstructure, getEntityIdFromKeys([BigInt(hyperstructureEntityId)])); const canUpdate = useMemo(() => { if (!hyperstructureConfig || !nextBlockTimestamp) return false; - if (!hyperstructureUpdate) return true; - if (ContractAddress(hyperstructureUpdate.last_updated_by) === ContractAddress(account.address)) { + if (!hyperstructure) return true; + if (ContractAddress(hyperstructure.last_updated_by) === ContractAddress(account.address)) { return ( - nextBlockTimestamp > - hyperstructureUpdate.last_updated_timestamp + hyperstructureConfig.time_between_shares_change + nextBlockTimestamp > hyperstructure.last_updated_timestamp + hyperstructureConfig.time_between_shares_change ); } else { return true; } - }, [hyperstructureUpdate, hyperstructureConfig, nextBlockTimestamp]); + }, [hyperstructure, hyperstructureConfig, nextBlockTimestamp]); const structure = getStructureByEntityId(hyperstructureEntityId); @@ -147,7 +143,7 @@ const CoOwnersRows = ({ setTooltip({ content: `Wait ${formatTime( Number(hyperstructureConfig?.time_between_shares_change) - - Number((nextBlockTimestamp || 0) - Number(hyperstructureUpdate?.last_updated_timestamp)), + Number((nextBlockTimestamp || 0) - (hyperstructure?.last_updated_timestamp ?? 0)), )} to change`, position: "top", }); diff --git a/client/src/ui/components/hyperstructures/HyperstructurePanel.tsx b/client/src/ui/components/hyperstructures/HyperstructurePanel.tsx index c449fc0cc..431f8709b 100644 --- a/client/src/ui/components/hyperstructures/HyperstructurePanel.tsx +++ b/client/src/ui/components/hyperstructures/HyperstructurePanel.tsx @@ -3,11 +3,15 @@ import { calculateCompletionPoints } from "@/dojo/modelManager/utils/Leaderboard import { useDojo } from "@/hooks/context/DojoContext"; import { useContributions } from "@/hooks/helpers/useContributions"; import { getEntitiesUtils } from "@/hooks/helpers/useEntities"; -import { ProgressWithPercentage, useHyperstructures, useUpdates } from "@/hooks/helpers/useHyperstructures"; +import { + ProgressWithPercentage, + useHyperstructures, + useHyperstructureUpdates, +} from "@/hooks/helpers/useHyperstructures"; import useUIStore from "@/hooks/store/useUIStore"; import Button from "@/ui/elements/Button"; import TextInput from "@/ui/elements/TextInput"; -import { currencyIntlFormat } from "@/ui/utils/utils"; +import { currencyIntlFormat, getEntityIdFromKeys } from "@/ui/utils/utils"; import { ContractAddress, EternumGlobalConfig, @@ -15,6 +19,7 @@ import { HYPERSTRUCTURE_TOTAL_COSTS_SCALED, MAX_NAME_LENGTH, } from "@bibliothecadao/eternum"; +import { useComponentValue } from "@dojoengine/react"; import { useMemo, useState } from "react"; import { ContributionSummary } from "./ContributionSummary"; import { HyperstructureDetails } from "./HyperstructureDetails"; @@ -24,6 +29,7 @@ enum Loading { None, Contribute, ChangeName, + SetPrivate, } export const HyperstructurePanel = ({ entity }: any) => { @@ -31,7 +37,8 @@ export const HyperstructurePanel = ({ entity }: any) => { account: { account }, network: { provider }, setup: { - systemCalls: { contribute_to_construction }, + systemCalls: { contribute_to_construction, set_private }, + components: { Hyperstructure }, }, } = useDojo(); @@ -47,13 +54,15 @@ export const HyperstructurePanel = ({ entity }: any) => { const progresses = useProgress(entity.entity_id); const myContributions = useContributionsByPlayerAddress(BigInt(account.address), entity.entity_id); - const updates = useUpdates(entity.entity_id); + const updates = useHyperstructureUpdates(entity.entity_id); const [newContributions, setNewContributions] = useState>({}); const { getAddressNameFromEntity } = getEntitiesUtils(); const ownerName = getAddressNameFromEntity(entity.entity_id); + const hyperstructure = useComponentValue(Hyperstructure, getEntityIdFromKeys([BigInt(entity.entity_id)])); + const contributeToConstruction = async () => { const formattedContributions = Object.entries(newContributions).map(([resourceId, amount]) => ({ resource: Number(resourceId), @@ -104,6 +113,21 @@ export const HyperstructurePanel = ({ entity }: any) => { return LeaderboardManager.instance().getAddressShares(ContractAddress(account.address), entity.entity_id); }, [myContributions, updates]); + const setPrivate = async () => { + setIsLoading(Loading.SetPrivate); + try { + await set_private({ + signer: account, + hyperstructure_entity_id: entity.entity_id, + to_private: !hyperstructure?.private, + }); + } finally { + setIsLoading(Loading.None); + setNewContributions({}); + setResetContributions(false); + } + }; + console.log(hyperstructure); return (
@@ -117,7 +141,6 @@ export const HyperstructurePanel = ({ entity }: any) => { setNaming(name)} maxLength={MAX_NAME_LENGTH} /> @@ -145,9 +168,14 @@ export const HyperstructurePanel = ({ entity }: any) => {
{entity.name}
{account.address === entity.owner && ( - +
+ + +
)}
)} diff --git a/client/src/ui/components/hyperstructures/Leaderboard.tsx b/client/src/ui/components/hyperstructures/Leaderboard.tsx index f39d27492..04730afcb 100644 --- a/client/src/ui/components/hyperstructures/Leaderboard.tsx +++ b/client/src/ui/components/hyperstructures/Leaderboard.tsx @@ -1,5 +1,6 @@ import { LeaderboardManager } from "@/dojo/modelManager/LeaderboardManager"; import { useDojo } from "@/hooks/context/DojoContext"; +import { useHyperstructureUpdates } from "@/hooks/helpers/useHyperstructures"; import { useRealm } from "@/hooks/helpers/useRealm"; import useUIStore from "@/hooks/store/useUIStore"; import Button from "@/ui/elements/Button"; @@ -22,7 +23,7 @@ export const Leaderboard = ({ const { account: { account }, setup: { - components: { HyperstructureUpdate, Owner }, + components: { Owner }, }, } = useDojo(); @@ -33,13 +34,7 @@ export const Leaderboard = ({ return LeaderboardManager.instance().getPlayersByRank(nextBlockTimestamp || 0, hyperstructureEntityId); }, [nextBlockTimestamp, hyperstructureEntityId]); - const updateEntityIds = useEntityQuery([ - HasValue(HyperstructureUpdate, { hyperstructure_entity_id: hyperstructureEntityId }), - ]); - - const update = useMemo(() => { - return getComponentValue(HyperstructureUpdate, updateEntityIds[0]); - }, [updateEntityIds]); + const hyperstructure = useHyperstructureUpdates(hyperstructureEntityId); const sortingParams = useMemo(() => { return [ @@ -60,7 +55,7 @@ export const Leaderboard = ({ return ContractAddress(owner.address) === ContractAddress(account.address); }, [hyperstructureEntityId]); - return update ? ( + return hyperstructure ? ( <> {sortingParams.map(({ label, sortKey, className }) => ( diff --git a/client/src/ui/components/hyperstructures/ResourceExchange.tsx b/client/src/ui/components/hyperstructures/ResourceExchange.tsx new file mode 100644 index 000000000..7856a8169 --- /dev/null +++ b/client/src/ui/components/hyperstructures/ResourceExchange.tsx @@ -0,0 +1,190 @@ +import { useDojo } from "@/hooks/context/DojoContext"; +import { ArmyInfo } from "@/hooks/helpers/useArmies"; +import { getResourcesUtils } from "@/hooks/helpers/useResources"; +import Button from "@/ui/elements/Button"; +import { NumberInput } from "@/ui/elements/NumberInput"; +import { ResourceIcon } from "@/ui/elements/ResourceIcon"; +import { currencyFormat, divideByPrecision, multiplyByPrecision } from "@/ui/utils/utils"; +import { ID, ResourcesIds } from "@bibliothecadao/eternum"; +import { ArrowRight } from "lucide-react"; +import { useMemo, useState } from "react"; + +type ResourceExchangeProps = { + giverArmyName: string; + takerArmy?: ArmyInfo; + giverArmyEntityId: ID; + structureEntityId?: ID; + allowReverse?: boolean; +}; + +export const ResourceExchange = ({ + giverArmyName, + giverArmyEntityId, + structureEntityId, + takerArmy, + allowReverse, +}: ResourceExchangeProps) => { + const { + setup: { + account: { account }, + systemCalls: { send_resources }, + }, + } = useDojo(); + + const { getResourcesFromBalance } = getResourcesUtils(); + + const [loading, setLoading] = useState(false); + const [transferDirection, setTransferDirection] = useState<"to" | "from">("to"); + const [resourcesGiven, setResourcesGiven] = useState>( + Object.keys(ResourcesIds) + .filter((key) => !isNaN(Number(key))) + .reduce( + (acc, key) => { + acc[Number(key)] = 0; + return acc; + }, + {} as Record, + ), + ); + + const giverArmyResources = useMemo(() => getResourcesFromBalance(giverArmyEntityId), [loading]); + const takerArmyResources = useMemo(() => getResourcesFromBalance(takerArmy?.entity_id!), [loading]); + + const handleResourceGivenChange = (resourceId: number, amount: number) => { + setResourcesGiven({ ...resourcesGiven, [resourceId]: amount }); + }; + + const selectedResourceIds = useMemo(() => Object.keys(resourcesGiven).map(Number), [resourcesGiven]); + const selectedResourceAmounts = useMemo(() => resourcesGiven, [resourcesGiven]); + + const resourcesList = useMemo(() => { + return selectedResourceIds.flatMap((id: number) => [Number(id), multiplyByPrecision(selectedResourceAmounts[id])]); + }, [selectedResourceIds, selectedResourceAmounts]); + + const transferResources = async () => { + setLoading(true); + const fromArmyId = transferDirection === "to" ? giverArmyEntityId : takerArmy?.entity_id || structureEntityId; + const toArmyId = transferDirection === "to" ? takerArmy?.entity_id || structureEntityId : giverArmyEntityId; + + await send_resources({ + signer: account, + sender_entity_id: fromArmyId!, + recipient_entity_id: toArmyId!, + resources: resourcesList, + }); + + setLoading(false); + setResourcesGiven(Object.fromEntries(Object.keys(resourcesGiven).map((key) => [key, 0]))); + }; + + return ( +
+
+
+

{giverArmyName}

+ {(transferDirection === "from" ? takerArmyResources : giverArmyResources).map((resource) => { + const amount = + transferDirection === "from" + ? giverArmyResources.find((giverResource) => giverResource.resourceId === resource.resourceId) + ?.amount || 0 + : resource.amount; + return ( + handleResourceGivenChange(resource.resourceId, newAmount)} + transferDirection={transferDirection} + /> + ); + })} +
+ +
+

Transfer {transferDirection}

+ {(transferDirection === "to" ? giverArmyResources : takerArmyResources).map((resource) => { + const amount = + transferDirection === "to" + ? takerArmyResources.find((takerResource) => takerResource.resourceId === resource.resourceId) + ?.amount || 0 + : resource.amount; + return ( + handleResourceGivenChange(resource.resourceId, newAmount)} + transferDirection={transferDirection === "to" ? "from" : "to"} + /> + ); + })} +
+
+ + {allowReverse && ( +
+ +
+ )} + + +
+ ); +}; + +type ResourceRowProps = { + resourceId: ID; + amount: number; + givenAmount: number; + onChangeAmount: (value: number) => void; + transferDirection: string; +}; + +const ResourceRow = ({ resourceId, amount, givenAmount, onChangeAmount, transferDirection }: ResourceRowProps) => { + return ( +
+ +
+

Avail.

+

+ {transferDirection === "to" + ? currencyFormat(Number(amount - multiplyByPrecision(givenAmount)), 0) + : currencyFormat(Number(amount + multiplyByPrecision(givenAmount)), 0)} +

+
+ {transferDirection === "to" ? ( + onChangeAmount(newAmount)} + /> + ) : ( +
+ {`+${givenAmount}`} +
+ )} +
+ ); +}; diff --git a/client/src/ui/components/hyperstructures/StructureCard.tsx b/client/src/ui/components/hyperstructures/StructureCard.tsx index 9eeadd751..90d1b3d51 100644 --- a/client/src/ui/components/hyperstructures/StructureCard.tsx +++ b/client/src/ui/components/hyperstructures/StructureCard.tsx @@ -6,6 +6,7 @@ import { Position } from "@/types/Position"; import Button from "@/ui/elements/Button"; import { NumberInput } from "@/ui/elements/NumberInput"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui/elements/Tabs"; import { getTotalTroops } from "@/ui/modules/military/battle-view/BattleHistory"; import { currencyFormat, formatNumber } from "@/ui/utils/utils"; import { EternumGlobalConfig, ID, ResourcesIds } from "@bibliothecadao/eternum"; @@ -14,6 +15,7 @@ import { getEntityIdFromKeys } from "@dojoengine/utils"; import { ArrowRight } from "lucide-react"; import { useMemo, useState } from "react"; import { StructureListItem } from "../worldmap/structures/StructureListItem"; +import { ResourceExchange } from "./ResourceExchange"; const MAX_TROOPS_PER_ARMY = EternumGlobalConfig.troop.maxTroopCount; @@ -98,7 +100,46 @@ const troopsToFormat = (troops: { knight_count: bigint; paladin_count: bigint; c }; }; -export const TroopExchange = ({ +export const Exchange = ({ + giverArmyName, + giverArmyEntityId, + structureEntityId, + takerArmy, + allowReverse, +}: TroopsProps) => { + return ( + + + + Troops + + + Resources + + + + + + + + + + ); +}; + +const TroopExchange = ({ giverArmyName, giverArmyEntityId, structureEntityId, @@ -176,8 +217,8 @@ export const TroopExchange = ({ }; await army_merge_troops({ signer: account, - from_army_id: fromArmy?.entity_id, - to_army_id: toArmy?.entity_id, + from_army_id: fromArmy?.entity_id ?? 0n, + to_army_id: toArmy?.entity_id ?? 0n, troops: transferedTroops, }).then(() => { if ( diff --git a/client/src/ui/components/military/ArmyChip.tsx b/client/src/ui/components/military/ArmyChip.tsx index b666b4c6a..c6f784fc2 100644 --- a/client/src/ui/components/military/ArmyChip.tsx +++ b/client/src/ui/components/military/ArmyChip.tsx @@ -11,7 +11,7 @@ import { ArmyCapacity } from "@/ui/elements/ArmyCapacity"; import Button from "@/ui/elements/Button"; import { StaminaResource } from "@/ui/elements/StaminaResource"; import React, { Dispatch, SetStateAction, useMemo, useState } from "react"; -import { TroopExchange } from "../hyperstructures/StructureCard"; +import { Exchange } from "../hyperstructures/StructureCard"; import { InventoryResources } from "../resources/InventoryResources"; import { ArmyManagementCard, ViewOnMapIcon } from "./ArmyManagementCard"; import { TroopMenuRow } from "./TroopChip"; @@ -101,7 +101,7 @@ export const ArmyChip = ({ }} onMouseEnter={() => { setTooltip({ - content: "Swap troops (only possible on same hex)", + content: "Swap troops or resources (only possible on same hex)", position: "top", }); }} @@ -184,7 +184,7 @@ const ArmyMergeTroopsPanel = ({
{selectedReceiverArmy ? ( - }; return (
- +
{structure.name}
diff --git a/client/src/ui/components/military/ArmyManagementCard.tsx b/client/src/ui/components/military/ArmyManagementCard.tsx index 384bcc9a1..4972f0fe2 100644 --- a/client/src/ui/components/military/ArmyManagementCard.tsx +++ b/client/src/ui/components/military/ArmyManagementCard.tsx @@ -81,9 +81,9 @@ export const ArmyManagementCard = ({ owner_entity, army, setSelectedEntity }: Ar const [naming, setNaming] = useState(""); const [troopCounts, setTroopCounts] = useState<{ [key: number]: number }>({ - [ResourcesIds.Knight]: 1000, - [ResourcesIds.Crossbowman]: 1000, - [ResourcesIds.Paladin]: 1000, + [ResourcesIds.Knight]: 0, + [ResourcesIds.Crossbowman]: 0, + [ResourcesIds.Paladin]: 0, }); const remainingTroops = useMemo(() => { @@ -239,12 +239,7 @@ export const ArmyManagementCard = ({ owner_entity, army, setSelectedEntity }: Ar
{editName ? (
- setNaming(name)} - /> + setNaming(name)} /> +
+
-
{children}
+ {children}
); diff --git a/client/src/ui/components/trading/MarketModal.tsx b/client/src/ui/components/trading/MarketModal.tsx index ccc615969..c980e6d38 100644 --- a/client/src/ui/components/trading/MarketModal.tsx +++ b/client/src/ui/components/trading/MarketModal.tsx @@ -15,12 +15,13 @@ import { currencyFormat, getEntityIdFromKeys } from "@/ui/utils/utils"; import { ID, MarketInterface, ResourcesIds, resources } from "@bibliothecadao/eternum"; import { useComponentValue } from "@dojoengine/react"; import { useMemo, useState } from "react"; +import { ModalContainer } from "../ModalContainer"; import { BankPanel } from "../bank/BankList"; import { HintModal } from "../hints/HintModal"; -import { ModalContainer } from "../ModalContainer"; import { MarketOrderPanel, MarketResource } from "./MarketOrderPanel"; import { MarketTradingHistory } from "./MarketTradingHistory"; -import { TransferBetweenEntities } from "./TransferBetweenEntities"; +import { RealmProduction } from "./RealmProduction"; +import { TransferView } from "./TransferView"; export const MarketModal = () => { const { @@ -46,7 +47,7 @@ export const MarketModal = () => { const selectedResource = useMarketStore((state) => state.selectedResource); const setSelectedResource = useMarketStore((state) => state.setSelectedResource); - const structures = useMemo(() => playerStructures(), [playerStructures]); + const structures = playerStructures(); const lordsBalance = useComponentValue(Resource, getEntityIdFromKeys([BigInt(structureEntityId!), BigInt(ResourcesIds.Lords)])) @@ -97,6 +98,15 @@ export const MarketModal = () => { ), component: , }, + { + key: "resourceProd", + label: ( +
+
Realm Production
+
+ ), + component: , + }, ], [selectedResource, structureEntityId, askOffers, bidOffers], ); @@ -227,7 +237,7 @@ const MarketResourceSidebar = ({ const askPrice = resourceBidOffers .filter((offer) => (resource.id ? offer.makerGets[0]?.resourceId === resource.id : true)) - .reduce((acc, offer) => (offer.perLords < acc ? offer.perLords : acc), Infinity); + .reduce((acc, offer) => (offer.perLords > acc ? offer.perLords : acc), 0); const bidPrice = resourceAskOffers .filter((offer) => offer.takerGets[0].resourceId === resource.id) @@ -252,24 +262,3 @@ const MarketResourceSidebar = ({
); }; - -const TransferView = () => { - const { playerRealms, playerStructures, otherRealms } = useEntities(); - - return ( - structure.category === "Hyperstructure"), - name: "Player Hyperstructures", - }, - { - entities: playerStructures().filter((structure) => structure.category === "FragmentMine"), - name: "Player Fragment Mines", - }, - { entities: otherRealms(), name: "Other Realms" }, - ]} - /> - ); -}; diff --git a/client/src/ui/components/trading/MarketOrderPanel.tsx b/client/src/ui/components/trading/MarketOrderPanel.tsx index eb678179a..a187fdc8a 100644 --- a/client/src/ui/components/trading/MarketOrderPanel.tsx +++ b/client/src/ui/components/trading/MarketOrderPanel.tsx @@ -1,4 +1,5 @@ import { BattleManager } from "@/dojo/modelManager/BattleManager"; +import { ProductionManager } from "@/dojo/modelManager/ProductionManager"; import { useDojo } from "@/hooks/context/DojoContext"; import { useRealm } from "@/hooks/helpers/useRealm"; import { useProductionManager } from "@/hooks/helpers/useResources"; @@ -20,7 +21,7 @@ import { findResourceById, } from "@bibliothecadao/eternum"; import clsx from "clsx"; -import { useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { ConfirmationPopup } from "../bank/ConfirmationPopup"; export const MarketResource = ({ @@ -154,6 +155,8 @@ const MarketOrders = ({ offers: MarketInterface[]; isOwnStructureInBattle: boolean; }) => { + const [updateBalance, setUpdateBalance] = useState(false); + const lowestPrice = useMemo(() => { const price = offers.reduce((acc, offer) => (offer.perLords < acc ? offer.perLords : acc), Infinity); return price === Infinity ? 0 : price; @@ -185,7 +188,14 @@ const MarketOrders = ({ }`} > {offers.map((offer, index) => ( - + ))} {isOwnStructureInBattle && (
@@ -221,15 +231,27 @@ const OrderRowHeader = ({ resourceId, isBuy }: { resourceId?: number; isBuy: boo ); }; -const OrderRow = ({ offer, entityId, isBuy }: { offer: MarketInterface; entityId: ID; isBuy: boolean }) => { +const OrderRow = ({ + offer, + entityId, + isBuy, + updateBalance, + setUpdateBalance, +}: { + offer: MarketInterface; + entityId: ID; + isBuy: boolean; + updateBalance: boolean; + setUpdateBalance: (value: boolean) => void; +}) => { const { computeTravelTime } = useTravel(); const dojo = useDojo(); - const { - account: { account }, - setup: { - systemCalls: { cancel_order, accept_partial_order }, - }, - } = useDojo(); + + const lordsManager = new ProductionManager(dojo.setup, entityId, ResourcesIds.Lords); + const lordsBalance = useMemo(() => Number(lordsManager.getResource()?.balance || 0n), [updateBalance]); + + const resourceManager = new ProductionManager(dojo.setup, entityId, offer.makerGets[0].resourceId); + const resourceBalance = useMemo(() => Number(resourceManager.getResource()?.balance || 0n), [updateBalance]); const { getRealmAddressName } = useRealm(); @@ -241,12 +263,6 @@ const OrderRow = ({ offer, entityId, isBuy }: { offer: MarketInterface; entityId return battleManager.isBattleOngoing(nextBlockTimestamp!) && !battleManager.isSiege(nextBlockTimestamp!); }, [offer, nextBlockTimestamp]); - const [inputValue, setInputValue] = useState(() => { - return isBuy - ? offer.makerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision - : offer.takerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision; - }); - const [confirmOrderModal, setConfirmOrderModal] = useState(false); const travelTime = useMemo( @@ -282,6 +298,28 @@ const OrderRow = ({ offer, entityId, isBuy }: { offer: MarketInterface; entityId const currentDefaultTick = useUIStore((state) => state.currentDefaultTick); + const resourceBalanceRatio = useMemo( + () => (resourceBalance < getsDisplayNumber ? resourceBalance / getsDisplayNumber : 1), + [resourceBalance, getsDisplayNumber], + ); + const lordsBalanceRatio = useMemo( + () => (lordsBalance < getTotalLords ? lordsBalance / getTotalLords : 1), + [lordsBalance, getTotalLords], + ); + const [inputValue, setInputValue] = useState(() => { + return isBuy + ? (offer.makerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision) * resourceBalanceRatio + : (offer.takerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision) * lordsBalanceRatio; + }); + + useEffect(() => { + setInputValue( + isBuy + ? (offer.makerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision) * resourceBalanceRatio + : (offer.takerGets[0].amount / EternumGlobalConfig.resources.resourcePrecision) * lordsBalanceRatio, + ); + }, [resourceBalanceRatio, lordsBalanceRatio]); + const calculatedResourceAmount = useMemo(() => { return inputValue * EternumGlobalConfig.resources.resourcePrecision; }, [inputValue, getsDisplay, getTotalLords]); @@ -325,8 +363,8 @@ const OrderRow = ({ offer, entityId, isBuy }: { offer: MarketInterface; entityId setLoading(true); setConfirmOrderModal(false); - await accept_partial_order({ - signer: account, + await dojo.setup.systemCalls.accept_partial_order({ + signer: dojo.account.account, taker_id: entityId, trade_id: offer.tradeId, maker_gives_resources: [offer.takerGets[0].resourceId, offer.takerGets[0].amount], @@ -336,6 +374,7 @@ const OrderRow = ({ offer, entityId, isBuy }: { offer: MarketInterface; entityId } catch (error) { console.error("Failed to accept order", error); } finally { + setUpdateBalance(!updateBalance); setLoading(false); } }; @@ -382,8 +421,8 @@ const OrderRow = ({ offer, entityId, isBuy }: { offer: MarketInterface; entityId
- {bid.toString()} +
per / diff --git a/client/src/ui/components/trading/RealmProduction.tsx b/client/src/ui/components/trading/RealmProduction.tsx new file mode 100644 index 000000000..6c1d7f1fc --- /dev/null +++ b/client/src/ui/components/trading/RealmProduction.tsx @@ -0,0 +1,70 @@ +import { getRealms } from "@/hooks/helpers/useRealm"; +import useUIStore from "@/hooks/store/useUIStore"; +import { SelectResource } from "@/ui/elements/SelectResource"; +import { unpackResources } from "@/ui/utils/packedData"; +import { RESOURCE_INPUTS_SCALED, ResourcesIds } from "@bibliothecadao/eternum"; +import { useState } from "react"; +import { RealmResourcesIO } from "../resources/RealmResourcesIO"; + +export const RealmProduction = () => { + const setSelectedPlayer = useUIStore((state) => state.setSelectedPlayer); + const toggleModal = useUIStore((state) => state.toggleModal); + + const realms = getRealms(); + const [filterProduced, setFilterProduced] = useState(null); + const [filterConsumed, setFilterConsumed] = useState(null); + + return ( + <> +
+
+

Search produced resource

+ setFilterProduced(resourceId)} className="w-full" /> +
+
+

Search consumed resource

+ setFilterConsumed(resourceId)} className="w-full" /> +
+
+ +
+ {realms && + realms.map((realm, index) => { + if (!realm) return; + console.log("Realm prod", realm); + + const resourcesProduced = unpackResources(realm.resourceTypesPacked, realm.resourceTypesCount); + if (filterProduced && !resourcesProduced.includes(filterProduced)) return; + + const resourcesConsumed = [ + ...new Set( + resourcesProduced.flatMap((resourceId) => { + return RESOURCE_INPUTS_SCALED[resourceId] + .filter( + (input) => input.resource !== ResourcesIds["Wheat"] && input.resource !== ResourcesIds["Fish"], + ) + .map((input) => input.resource); + }), + ), + ]; + if (filterConsumed && !resourcesConsumed.includes(filterConsumed!)) return; + + return ( +
{ + toggleModal(null); + setSelectedPlayer(realm.owner); + }} + > +

{realm.ownerName}

+

{realm.name}

+ {realm.realmId && } +
+ ); + })} +
+ + ); +}; diff --git a/client/src/ui/components/trading/SelectEntityFromList.tsx b/client/src/ui/components/trading/SelectEntityFromList.tsx new file mode 100644 index 000000000..702140a64 --- /dev/null +++ b/client/src/ui/components/trading/SelectEntityFromList.tsx @@ -0,0 +1,47 @@ +import { useRealm } from "@/hooks/helpers/useRealm"; +import Button from "@/ui/elements/Button"; +import { ID } from "@bibliothecadao/eternum"; +import clsx from "clsx"; + +export const SelectEntityFromList = ({ + onSelect, + selectedEntityId, + selectedCounterpartyId, + entities, +}: { + onSelect: (name: string, entityId: ID) => void; + selectedEntityId: ID | null; + selectedCounterpartyId: ID | null; + entities: any[]; +}) => { + const { getRealmAddressName } = useRealm(); + + return ( +
+ {entities.map((entity, index) => { + const realmName = getRealmAddressName(entity.entity_id); + return ( +
onSelect(entity.name, entity.entity_id!)} + > +
+ {realmName} ({entity.name}) +
+ +
+ ); + })} +
+ ); +}; diff --git a/client/src/ui/components/trading/SelectResources.tsx b/client/src/ui/components/trading/SelectResources.tsx new file mode 100644 index 000000000..21a1772a9 --- /dev/null +++ b/client/src/ui/components/trading/SelectResources.tsx @@ -0,0 +1,104 @@ +import { getResourceBalance } from "@/hooks/helpers/useResources"; +import { usePlayResourceSound } from "@/hooks/useUISound"; +import Button from "@/ui/elements/Button"; +import ListSelect from "@/ui/elements/ListSelect"; +import { NumberInput } from "@/ui/elements/NumberInput"; +import { ResourceCost } from "@/ui/elements/ResourceCost"; +import { divideByPrecision } from "@/ui/utils/utils"; +import { ID, resources } from "@bibliothecadao/eternum"; +import { useMemo } from "react"; + +export const SelectResources = ({ + selectedResourceIds, + setSelectedResourceIds, + selectedResourceAmounts, + setSelectedResourceAmounts, + entity_id, +}: { + selectedResourceIds: any; + setSelectedResourceIds: any; + selectedResourceAmounts: any; + setSelectedResourceAmounts: any; + entity_id: ID; +}) => { + const { getBalance } = getResourceBalance(); + const { playResourceSound } = usePlayResourceSound(); + + const unselectedResources = useMemo( + () => resources.filter((res) => !selectedResourceIds.includes(res.id)), + [selectedResourceIds], + ); + + const addResourceGive = () => { + setSelectedResourceIds([...selectedResourceIds, unselectedResources[0].id]); + setSelectedResourceAmounts({ + ...selectedResourceAmounts, + [unselectedResources[0].id]: 1, + }); + playResourceSound(unselectedResources[0].id); + }; + + return ( +
+ {selectedResourceIds.map((id: any, index: any) => { + const resource = getBalance(entity_id, id); + const options = [resources.find((res) => res.id === id), ...unselectedResources].map((res: any) => ({ + id: res.id, + label: ( + + ), + })); + + return ( +
+ {selectedResourceIds.length > 1 && ( + + )} + { + const updatedResourceIds = [...selectedResourceIds]; + updatedResourceIds[index] = value; + setSelectedResourceIds(updatedResourceIds); + setSelectedResourceAmounts({ + ...selectedResourceAmounts, + [value]: 1, + }); + playResourceSound(value); + }} + /> + { + setSelectedResourceAmounts({ + ...selectedResourceAmounts, + [id]: Math.min(divideByPrecision(resource?.balance || 0), value), + }); + }} + /> +
+ ); + })} +
+ +
+
+ ); +}; diff --git a/client/src/ui/components/trading/TransferBetweenEntities.tsx b/client/src/ui/components/trading/TransferBetweenEntities.tsx index baea1c674..e74cb0e27 100644 --- a/client/src/ui/components/trading/TransferBetweenEntities.tsx +++ b/client/src/ui/components/trading/TransferBetweenEntities.tsx @@ -1,21 +1,18 @@ import { useDojo } from "@/hooks/context/DojoContext"; import { useRealm } from "@/hooks/helpers/useRealm"; -import { getResourceBalance } from "@/hooks/helpers/useResources"; import { useTravel } from "@/hooks/helpers/useTravel"; -import { usePlayResourceSound } from "@/hooks/useUISound"; import Button from "@/ui/elements/Button"; +import { Checkbox } from "@/ui/elements/Checkbox"; import { Headline } from "@/ui/elements/Headline"; -import ListSelect from "@/ui/elements/ListSelect"; -import { NumberInput } from "@/ui/elements/NumberInput"; -import { ResourceCost } from "@/ui/elements/ResourceCost"; import TextInput from "@/ui/elements/TextInput"; -import { divideByPrecision, multiplyByPrecision } from "@/ui/utils/utils"; -import { EternumGlobalConfig, ID, resources } from "@bibliothecadao/eternum"; -import clsx from "clsx"; +import { multiplyByPrecision } from "@/ui/utils/utils"; +import { EternumGlobalConfig, ID } from "@bibliothecadao/eternum"; import { ArrowRight, LucideArrowRight } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { TravelInfo } from "../resources/ResourceWeight"; import { ToggleComponent } from "../toggle/ToggleComponent"; +import { SelectEntityFromList } from "./SelectEntityFromList"; +import { SelectResources } from "./SelectResources"; enum STEP_ID { SELECT_ENTITIES = 1, @@ -42,7 +39,15 @@ interface SelectedEntity { entityId: ID; } -export const TransferBetweenEntities = ({ entitiesList }: { entitiesList: { entities: any[]; name: string }[] }) => { +export const TransferBetweenEntities = ({ + entitiesList, + filtered, + filterBy, +}: { + entitiesList: { entities: any[]; name: string }[]; + filtered: boolean; + filterBy: (filtered: boolean) => void; +}) => { const { getRealmAddressName } = useRealm(); const [selectedEntityIdFrom, setSelectedEntityIdFrom] = useState(null); @@ -59,16 +64,6 @@ export const TransferBetweenEntities = ({ entitiesList }: { entitiesList: { enti const currentStep = useMemo(() => STEPS.find((step) => step.id === selectedStepId), [selectedStepId]); - const entitiesListWithAccountNames = useMemo(() => { - return entitiesList.map(({ entities, name }) => ({ - entities: entities.map((entity) => ({ - ...entity, - accountName: getRealmAddressName(entity.entity_id), - })), - name, - })); - }, [entitiesList]); - const { account: { account }, setup: { @@ -130,9 +125,19 @@ export const TransferBetweenEntities = ({ entitiesList }: { entitiesList: { enti ); }; + const entitiesListWithAccountNames = useMemo(() => { + return entitiesList.map(({ entities, name }) => ({ + entities: entities.map((entity) => ({ + ...entity, + accountName: getRealmAddressName(entity.entity_id), + })), + name, + })); + }, [entitiesList]); + return (
-

{currentStep?.title}

+ {currentStep?.title}
{selectedEntityIdFrom?.toString() && selectedEntityIdTo?.toString() && ( @@ -158,35 +163,43 @@ export const TransferBetweenEntities = ({ entitiesList }: { entitiesList: { enti From setFromSearchTerm(fromSearchTerm)} className="my-2" /> {entitiesListWithAccountNames .filter(({ name }) => name !== "Other Realms") - .map(({ entities, name: title }, index) => ( - - setSelectedEntityIdFrom({ name, entityId })} - selectedCounterpartyId={selectedEntityIdTo?.entityId!} - selectedEntityId={selectedEntityIdFrom?.entityId!} - entities={filterEntities(entities, fromSearchTerm, selectedEntityIdFrom?.entityId)} - /> - - ))} + .map(({ entities, name: title }, index) => { + const filteredEntities = filterEntities(entities, fromSearchTerm, selectedEntityIdFrom?.entityId); + if (filteredEntities.length === 0) return null; + return ( + + setSelectedEntityIdFrom({ name, entityId })} + selectedCounterpartyId={selectedEntityIdTo?.entityId!} + selectedEntityId={selectedEntityIdFrom?.entityId!} + entities={filteredEntities} + /> + + ); + })}
To +
+ {" "} +
filterBy(!filtered)}> + +
Guild Only
+
+
+ setToSearchTerm(toSearchTerm)} className="my-2" /> @@ -194,10 +207,8 @@ export const TransferBetweenEntities = ({ entitiesList }: { entitiesList: { enti setSelectedEntityIdTo({ name, entityId })} @@ -227,8 +238,8 @@ export const TransferBetweenEntities = ({ entitiesList }: { entitiesList: { enti )} {currentStep?.id === STEP_ID.SELECT_RESOURCES && ( -
-
+
+
); }; - -const SelectEntityFromList = ({ - onSelect, - selectedEntityId, - selectedCounterpartyId, - entities, -}: { - onSelect: (name: string, entityId: ID) => void; - selectedEntityId: ID | null; - selectedCounterpartyId: ID | null; - entities: any[]; -}) => { - const { getRealmAddressName } = useRealm(); - - return ( -
- {entities.map((entity, index) => { - const realmName = getRealmAddressName(entity.entity_id); - return ( -
onSelect(entity.name, entity.entity_id!)} - > -
- {realmName} ({entity.name}) -
- -
- ); - })} -
- ); -}; - -const SelectResources = ({ - selectedResourceIds, - setSelectedResourceIds, - selectedResourceAmounts, - setSelectedResourceAmounts, - entity_id, -}: { - selectedResourceIds: any; - setSelectedResourceIds: any; - selectedResourceAmounts: any; - setSelectedResourceAmounts: any; - entity_id: ID; -}) => { - const { getBalance } = getResourceBalance(); - const { playResourceSound } = usePlayResourceSound(); - - const unselectedResources = useMemo( - () => resources.filter((res) => !selectedResourceIds.includes(res.id)), - [selectedResourceIds], - ); - - const addResourceGive = () => { - setSelectedResourceIds([...selectedResourceIds, unselectedResources[0].id]); - setSelectedResourceAmounts({ - ...selectedResourceAmounts, - [unselectedResources[0].id]: 1, - }); - playResourceSound(unselectedResources[0].id); - }; - - return ( -
- {selectedResourceIds.map((id: any, index: any) => { - const resource = getBalance(entity_id, id); - let options = [resources.find((res) => res.id === id), ...unselectedResources] as any; - options = options.map((res: any) => { - const bal = getBalance(entity_id, res.id); - return { - id: res.id, - label: , - }; - }); - if (selectedResourceIds.length > 1) { - options = [ - { - id: 0, - label: ( -
-
Remove item
-
- ), - }, - ...options, - ]; - } - return ( -
- { - if (value === 0) { - const tmp = [...selectedResourceIds]; - tmp.splice(index, 1); - setSelectedResourceIds(tmp); - const tmpAmounts = { ...selectedResourceAmounts }; - delete tmpAmounts[id]; - setSelectedResourceAmounts(tmpAmounts); - return; - } - const tmp = [...selectedResourceIds]; - tmp[index] = value; - playResourceSound(value); - setSelectedResourceIds(tmp); - setSelectedResourceAmounts({ - ...selectedResourceAmounts, - [value]: 1, - }); - }} - /> - { - setSelectedResourceAmounts({ - ...selectedResourceAmounts, - [id]: Math.min(divideByPrecision(resource?.balance || 0), value), - }); - }} - /> -
- ); - })} - -
- ); -}; diff --git a/client/src/ui/components/trading/TransferView.tsx b/client/src/ui/components/trading/TransferView.tsx new file mode 100644 index 000000000..0c130bd7b --- /dev/null +++ b/client/src/ui/components/trading/TransferView.tsx @@ -0,0 +1,55 @@ +import { useDojo } from "@/hooks/context/DojoContext"; +import { useEntities } from "@/hooks/helpers/useEntities"; +import { useGuilds } from "@/hooks/helpers/useGuilds"; +import { useMemo, useState } from "react"; +import { TransferBetweenEntities } from "./TransferBetweenEntities"; + +export const TransferView = () => { + const { + account: { account }, + } = useDojo(); + + const { playerRealms, playerStructures, otherRealms, otherStructures } = useEntities(); + + const [guildOnly, setGuildOnly] = useState(false); + + const { getPlayersInPlayersGuild } = useGuilds(); + + const playersInPlayersGuildAddress = useMemo(() => { + return getPlayersInPlayersGuild(BigInt(account.address)).map((a) => BigInt(a.address)); + }, [account.address]); + + return ( + structure.category === "Hyperstructure"), + name: "Your Hyperstructures", + }, + { + entities: playerStructures().filter((structure) => structure.category === "FragmentMine"), + name: "Your Fragment Mines", + }, + { + entities: otherRealms((a) => + guildOnly + ? playersInPlayersGuildAddress.includes(a.owner.address) + : !playersInPlayersGuildAddress.includes(a.owner.address), + ), + name: "Other Realms", + }, + { + entities: otherStructures((a) => + guildOnly + ? playersInPlayersGuildAddress.includes(a.owner.address) + : !playersInPlayersGuildAddress.includes(a.owner.address), + ), + name: "Other Structures", + }, + ]} + /> + ); +}; diff --git a/client/src/ui/components/worldmap/guilds/Guilds.tsx b/client/src/ui/components/worldmap/guilds/Guilds.tsx index 7f30235fb..140e72c3e 100644 --- a/client/src/ui/components/worldmap/guilds/Guilds.tsx +++ b/client/src/ui/components/worldmap/guilds/Guilds.tsx @@ -45,7 +45,6 @@ export const Guilds = () => { const sortingParams: SortingParamGuildAndName[] = useMemo(() => { return [ - { label: "Rank", sortKey: "rank", className: "col-span-1" }, { label: "Guild Name", sortKey: "name", className: "col-span-1" }, { label: "Access", sortKey: "is_public", className: "col-span-1" }, { label: "Members", sortKey: "member_count", className: "col-span-1" }, @@ -117,7 +116,6 @@ export const Guilds = () => { guild.guild.entity_id === guildDisplayed?.guildEntityId ? "bg-green/20" : "" } `} > -

{`#${index + 1}`}

setSelectedGuild({ guildEntityId: guild.guild.entity_id, name: guild.name })} diff --git a/client/src/ui/components/worldmap/guilds/GuildsPanel.tsx b/client/src/ui/components/worldmap/guilds/GuildsPanel.tsx index b92cbff70..d423a4220 100644 --- a/client/src/ui/components/worldmap/guilds/GuildsPanel.tsx +++ b/client/src/ui/components/worldmap/guilds/GuildsPanel.tsx @@ -34,7 +34,7 @@ export const GuildsPanel = () => { selectedIndex={selectedTab} onChange={(index: number) => setSelectedTab(index)} variant="default" - className="h-full" + className="h-full border-t" > {tabs.map((tab, index) => ( diff --git a/client/src/ui/components/worldmap/guilds/MyGuild.tsx b/client/src/ui/components/worldmap/guilds/MyGuild.tsx index 2bbc1c130..9f05fbd0c 100644 --- a/client/src/ui/components/worldmap/guilds/MyGuild.tsx +++ b/client/src/ui/components/worldmap/guilds/MyGuild.tsx @@ -103,7 +103,6 @@ export const MyGuild = () => { setNaming(name)} maxLength={MAX_NAME_LENGTH} /> @@ -187,7 +186,6 @@ export const MyGuild = () => { setPlayerAddress(playerAddress)} /> {notification && !disabled ? (

) : ( - <> + "" )}
); diff --git a/client/src/ui/elements/DojoHtml.tsx b/client/src/ui/elements/DojoHtml.tsx index 4c3f404d0..c14aea923 100644 --- a/client/src/ui/elements/DojoHtml.tsx +++ b/client/src/ui/elements/DojoHtml.tsx @@ -11,7 +11,7 @@ type DojoHtmlProps = { export const DojoHtml = ({ children, visible = true, ...rest }: DojoHtmlProps) => { const { setup } = useDojo(); - console.log({ setup }); + return ( {children} diff --git a/client/src/ui/elements/Headline.tsx b/client/src/ui/elements/Headline.tsx index 833d2aa02..4564a6c43 100644 --- a/client/src/ui/elements/Headline.tsx +++ b/client/src/ui/elements/Headline.tsx @@ -11,7 +11,12 @@ type HeadlineProps = { }; export const Headline = ({ children, className }: HeadlineProps) => ( -
+
+
{({ open }) => (
@@ -60,7 +60,7 @@ function ListSelect(props: ListSelectProps) { {props.options.map((option) => ( diff --git a/client/src/ui/elements/NumberInput.tsx b/client/src/ui/elements/NumberInput.tsx index a91d87b28..89304fa6a 100644 --- a/client/src/ui/elements/NumberInput.tsx +++ b/client/src/ui/elements/NumberInput.tsx @@ -1,6 +1,7 @@ import { ReactComponent as ArrowLeft } from "@/assets/icons/common/arrow-left.svg"; import { ReactComponent as ArrowRight } from "@/assets/icons/common/arrow-right.svg"; import clsx from "clsx"; +import { useEffect, useState } from "react"; import { soundSelector, useUiSounds } from "../../hooks/useUISound"; type NumberInputProps = { @@ -10,51 +11,101 @@ type NumberInputProps = { className?: string; min?: number; max: number; + arrows?: boolean; + allowDecimals?: boolean; + onFocus?: () => void; + onBlur?: () => void; + disabled?: boolean; }; -export const NumberInput = ({ value, onChange, className, step = 1, max = 0, min = 0 }: NumberInputProps) => { +export const NumberInput = ({ + value, + onChange, + className, + step = 1, + max = 0, + min = 0, + arrows = true, + allowDecimals = false, + onFocus, + onBlur, + disabled = false, +}: NumberInputProps) => { + const formatNumber = (num: number): string => { + if (num >= 1000) { + return num.toLocaleString("en-US"); + } + return num.toString(); + }; + const { play: playClick } = useUiSounds(soundSelector.click); + const [displayValue, setDisplayValue] = useState(formatNumber(value)); - return ( -
-
{ - onChange(Math.max(value - step, min)); - playClick(); - }} - > - -
+ useEffect(() => { + setDisplayValue(formatNumber(value)); + }, [value]); + + const parseNumber = (str: string): number => { + return parseFloat(str.replace(/,/g, "")); + }; + return ( +
+ {arrows && ( +
{ + onChange(Math.max(value - step, min)); + playClick(); + }} + > + +
+ )} { - if (isNaN(parseInt(e.target.value))) { - onChange(min); - return; - } - if (parseInt(e.target.value) > max) { - onChange(max); - } else if (parseInt(e.target.value) < min) { - onChange(min); + const inputValue = e.target.value; + if (allowDecimals) { + const match = inputValue.match(/[+-]?([0-9,]+([.][0-9]*)?|[.][0-9]+)/); + if (match) { + const parsedValue = parseNumber(match[0]); + setDisplayValue(formatNumber(parsedValue)); + onChange(parsedValue); + } else { + setDisplayValue(formatNumber(min)); + onChange(min); + } } else { - onChange(parseInt(e.target.value)); + const match = inputValue.match(/[+-]?([0-9,]+)/); + if (match) { + const parsedValue = parseNumber(match[0]); + setDisplayValue(formatNumber(Math.floor(parsedValue))); + onChange(Math.floor(parsedValue)); + } else { + setDisplayValue(formatNumber(min)); + onChange(min); + } } }} /> -
{ - onChange(Math.min(value + step, max)); - playClick(); - }} - > - -
+ {arrows && ( +
{ + onChange(Math.min(value + step, max)); + playClick(); + }} + > + +
+ )}
); }; diff --git a/client/src/ui/elements/SelectResource.tsx b/client/src/ui/elements/SelectResource.tsx index d189afb8d..b4602d5de 100644 --- a/client/src/ui/elements/SelectResource.tsx +++ b/client/src/ui/elements/SelectResource.tsx @@ -1,7 +1,10 @@ +import { ReactComponent as Cross } from "@/assets/icons/common/cross.svg"; import { ResourcesIds } from "@bibliothecadao/eternum"; -import React from "react"; +import clsx from "clsx"; +import React, { useRef, useState } from "react"; import { ResourceIcon } from "./ResourceIcon"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./Select"; +import TextInput from "./TextInput"; interface SelectResourceProps { onSelect: (resourceId: number | null) => void; @@ -9,30 +12,87 @@ interface SelectResourceProps { } export const SelectResource: React.FC = ({ onSelect, className }) => { + const [searchInput, setSearchInput] = useState(""); + const [selectedResource, setSelectedResource] = useState(""); + const [open, setOpen] = useState(false); + + const inputRef = useRef(null); + const resourceIds = Object.values(ResourcesIds) .filter((resource) => resource !== ResourcesIds.Earthenshard && resource !== ResourcesIds.Lords) .filter((resource) => typeof resource === "number"); + const filteredResourceIds = resourceIds.filter((resourceId) => + ResourcesIds[resourceId].toLowerCase().startsWith(searchInput.toLowerCase()), + ); + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + if (newOpen && inputRef.current) { + setSelectedResource(""); + setTimeout(() => { + inputRef.current?.focus(); + }, 0); + } else { + setSearchInput(""); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + if (filteredResourceIds.length > 0) { + const selectedResourceId = filteredResourceIds[0]; + setSelectedResource(selectedResourceId.toString()); + onSelect(selectedResourceId); + setOpen(false); + } + setSearchInput(""); + } else { + e.stopPropagation(); + } + }; + return ( - +
+ setSelectedResource("")} + /> + +
); }; diff --git a/client/src/ui/elements/Tabs.tsx b/client/src/ui/elements/Tabs.tsx new file mode 100644 index 000000000..b2c8c2484 --- /dev/null +++ b/client/src/ui/elements/Tabs.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import { cn } from "./lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/client/src/ui/elements/TextArea.tsx b/client/src/ui/elements/TextArea.tsx deleted file mode 100644 index ae128fd60..000000000 --- a/client/src/ui/elements/TextArea.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; - -import { cn } from "./lib/utils"; - -interface TextareaProps extends React.TextareaHTMLAttributes {} - -const Textarea = React.forwardRef(({ className, ...props }, ref) => { - return ( -