From 89a8e24e9a1037af070c6c38dded25e1a611bd5c Mon Sep 17 00:00:00 2001 From: Asten <0xasten@gmail.com> Date: Wed, 2 Jul 2025 11:30:44 +0800 Subject: [PATCH 1/2] rewrite recipe logic --- src/constants.cairo | 1 + src/models/Item.cairo | 1 + src/models/Recipe.cairo | 17 +++-- src/systems/actions.cairo | 132 +++++++++++++++++++++++--------------- src/systems/item.cairo | 1 + src/systems/recipe.cairo | 70 ++++++++------------ 6 files changed, 122 insertions(+), 100 deletions(-) diff --git a/src/constants.cairo b/src/constants.cairo index 73ef2cd..560a233 100644 --- a/src/constants.cairo +++ b/src/constants.cairo @@ -6,6 +6,7 @@ pub mod constants { pub const INIT_STAMINA: u8 = 100; pub const ITEMS_COUNTER_ID: felt252 = 'ITEMS_COUNTER_ID'; + pub const RECIPES_COUNTER_ID: felt252 = 'RECIPES_COUNTER_ID'; pub const STORAGE_FLAG: u32 = 999; // const EFFECT_ARMOR: felt252 = 'armor'; diff --git a/src/models/Item.cairo b/src/models/Item.cairo index 41c35c8..ecd8bc5 100644 --- a/src/models/Item.cairo +++ b/src/models/Item.cairo @@ -22,6 +22,7 @@ pub struct Item { pub cooldown: u8, pub energyCost: u8, pub isPlugin: bool, + pub enabled: bool, } #[derive(Drop, Serde)] diff --git a/src/models/Recipe.cairo b/src/models/Recipe.cairo index 0c426df..6fa2918 100644 --- a/src/models/Recipe.cairo +++ b/src/models/Recipe.cairo @@ -2,8 +2,17 @@ #[dojo::model] pub struct Recipe { #[key] - pub item1_id: u32, - #[key] - pub item2_id: u32, + pub id: u32, + pub item_ids: Array, + pub item_amounts: Array, pub result_item_id: u32, -} \ No newline at end of file + pub enabled: bool, +} + +#[derive(Drop, Serde)] +#[dojo::model] +pub struct RecipesCounter { + #[key] + pub id: u32, + pub count: u32, +} diff --git a/src/systems/actions.cairo b/src/systems/actions.cairo index 18fd693..9ad025c 100644 --- a/src/systems/actions.cairo +++ b/src/systems/actions.cairo @@ -22,6 +22,7 @@ pub trait IActions { fn move_item_from_storage_to_shop(ref self: T, storage_item_id: u32); fn move_item_from_shop_to_inventory(ref self: T, item_id: u32, x: u32, y: u32, rotation: u32); fn move_item_from_inventory_to_shop(ref self: T, inventory_item_id: u32); + fn craft_item(ref self: T, recipe_id: u32, storage_ids: Array); } // TODO: rename the count filed in counter model @@ -31,6 +32,7 @@ mod actions { use super::{IActions, WMClass}; use starknet::ContractAddress; use core::dict::Felt252Dict; + use core::array::Array; use core::bytes_31::bytes31; use starknet::{get_caller_address, get_block_timestamp}; @@ -44,7 +46,8 @@ mod actions { Character::{Characters, NameRecord}, Shop::Shop, Fight::{BattleLog, BattleLogCounter}, - Game::GameConfig + Game::GameConfig, + Recipe::Recipe }; use warpack_masters::items::{Backpack, Pack}; @@ -309,37 +312,14 @@ mod actions { } fn move_item_from_inventory_to_storage(ref self: ContractState, inventory_item_id: u32) { - let mut world = self.world(@"Warpacks"); - let player = get_caller_address(); // check if the player has joined the matching battle self._check_if_player_has_joined_a_matched_battle(player); - let itemId = self._remove_item_from_inventory(player, inventory_item_id); + let item_id = self._remove_item_from_inventory(player, inventory_item_id); - let mut storageCounter: CharacterItemsStorageCounter = world.read_model(player); - let mut count = storageCounter.count; - loop { - if count == 0 { - break; - } - - let mut storageItem: CharacterItemStorage = world.read_model((player, count)); - if storageItem.itemId == 0 { - storageItem.itemId = itemId; - world.write_model(@storageItem); - break; - } - - count -= 1; - }; - - if count == 0 { - storageCounter.count += 1; - world.write_model(@CharacterItemStorage { player, id: storageCounter.count, itemId: itemId, }); - world.write_model(@CharacterItemsStorageCounter { player, count: storageCounter.count }); - } + self._add_item_to_storage(player, item_id); } fn move_item_within_inventory(ref self: ContractState, inventory_item_id: u32, x: u32, y: u32, rotation: u32) { @@ -353,36 +333,11 @@ mod actions { } fn move_item_from_shop_to_storage(ref self: ContractState, item_id: u32) { - let mut world = self.world(@"Warpacks"); - let player = get_caller_address(); self._buy_item(player, item_id); - let mut storageCounter: CharacterItemsStorageCounter = world.read_model(player); - let mut count = storageCounter.count; - let mut isUpdated = false; - loop { - if count == 0 { - break; - } - - let mut storageItem: CharacterItemStorage = world.read_model((player, count)); - if storageItem.itemId == 0 { - storageItem.itemId = item_id; - isUpdated = true; - world.write_model(@storageItem); - break; - } - - count -= 1; - }; - - if isUpdated == false { - storageCounter.count += 1; - world.write_model(@CharacterItemStorage { player, id: storageCounter.count, itemId: item_id, }); - world.write_model(@CharacterItemsStorageCounter { player, count: storageCounter.count }); - } + self._add_item_to_storage(player, item_id); } fn move_item_from_storage_to_shop(ref self: ContractState, storage_item_id: u32) { @@ -414,6 +369,52 @@ mod actions { let item_id = self._remove_item_from_inventory(player, inventory_item_id); self._sell_item(player, item_id); } + + fn craft_item( + ref self: ContractState, recipe_id: u32, storage_ids: Array + ) { + let mut world = self.world(@"Warpacks"); + + let player = get_caller_address(); + + let recipe: Recipe = world.read_model(recipe_id); + assert(recipe.enabled, 'recipe is not enabled'); + + let item_ids_len = recipe.item_ids.len(); + assert(item_ids_len > 0, 'must have at least one item'); + assert(item_ids_len == recipe.item_amounts.len(), 'must the same length'); + + let mut required_items: Felt252Dict = Default::default(); + for i in 0..item_ids_len { + let item_id = *recipe.item_ids[i]; + let item_amount = *recipe.item_amounts[i]; + required_items.insert(item_id.into(), item_amount); + }; + + let storage_ids_len = storage_ids.len(); + assert(storage_ids_len > 0, 'must have at least one item'); + + for i in 0..storage_ids_len { + let storage_id = *storage_ids[i]; + let mut storage_item: CharacterItemStorage = world.read_model((player, storage_id)); + assert(storage_item.itemId != 0, 'item not owned'); + + let required_item_amount = required_items.get(storage_item.itemId.into()); + if (required_item_amount > 0) { + required_items.insert(storage_item.itemId.into(), required_item_amount - 1); + storage_item.itemId = 0; + world.write_model(@storage_item); + } + }; + + for i in 0..item_ids_len { + let item_id = *recipe.item_ids[i]; + + assert(required_items.get(item_id.into()) == 0, 'item not enough'); + }; + + self._add_item_to_storage(player, recipe.result_item_id); + } } #[generate_trait] @@ -843,5 +844,32 @@ mod actions { world.write_model(@playerChar); } + + fn _add_item_to_storage(ref self: ContractState, player: ContractAddress, item_id: u32) { + let mut world = self.world(@"Warpacks"); + + let mut storageCounter: CharacterItemsStorageCounter = world.read_model(player); + let mut count = storageCounter.count; + loop { + if count == 0 { + break; + } + + let mut storageItem: CharacterItemStorage = world.read_model((player, count)); + if storageItem.itemId == 0 { + storageItem.itemId = item_id; + world.write_model(@storageItem); + break; + } + + count -= 1; + }; + + if count == 0 { + storageCounter.count += 1; + world.write_model(@CharacterItemStorage { player, id: storageCounter.count, itemId: item_id }); + world.write_model(@storageCounter); + } + } } } diff --git a/src/systems/item.cairo b/src/systems/item.cairo index 430a9d7..18694cc 100644 --- a/src/systems/item.cairo +++ b/src/systems/item.cairo @@ -103,6 +103,7 @@ mod item_system { cooldown, energyCost, isPlugin, + enabled: true, }; world.write_model(@item); diff --git a/src/systems/recipe.cairo b/src/systems/recipe.cairo index d084cdb..f75d135 100644 --- a/src/systems/recipe.cairo +++ b/src/systems/recipe.cairo @@ -1,10 +1,7 @@ #[starknet::interface] pub trait IRecipe { fn add_recipe( - ref self: T, item1_id: u32, item2_id: u32, result_item_id: u32 - ); - fn craft_item( - ref self: T, storage_item_id1: u32, storage_item_id2: u32 + ref self: T, item_ids: Array, item_amounts: Array, result_item_id: u32 ); } @@ -14,10 +11,10 @@ mod recipe_system { use starknet::{get_caller_address}; use warpack_masters::models::{ - CharacterItem::CharacterItemStorage, Item::Item, - Recipe::Recipe, + Recipe::{Recipe, RecipesCounter}, }; + use warpack_masters::constants::constants::{RECIPES_COUNTER_ID}; use dojo::model::{ModelStorage}; use dojo::world::{IWorldDispatcherTrait}; @@ -25,57 +22,42 @@ mod recipe_system { #[abi(embed_v0)] impl RecipeImpl of IRecipe { fn add_recipe( - ref self: ContractState, item1_id: u32, item2_id: u32, result_item_id: u32 + ref self: ContractState, item_ids: Array, item_amounts: Array, result_item_id: u32 ) { let mut world = self.world(@"Warpacks"); let player = get_caller_address(); assert(world.dispatcher.is_owner(0, player), 'player not world owner'); - let item1: Item = world.read_model(item1_id); - assert(item1.height != 0, 'item1 does not exist'); - let item2: Item = world.read_model(item2_id); - assert(item2.height != 0, 'item2 does not exist'); + let item_ids_len = item_ids.len(); + assert(item_ids_len > 0, 'must have at least one item'); + assert(item_ids_len == item_amounts.len(), 'must the same length'); + + for i in 0..item_ids_len { + let item_id = *item_ids[i]; + let item_amount = *item_amounts[i]; + assert(item_amount > 0, 'amount must be greater than 0'); + + let item: Item = world.read_model(item_id); + assert(item.enabled, 'item is not enabled'); + }; - // make constructor let result_item: Item = world.read_model(result_item_id); - assert(result_item.height != 0, 'result item does not exist'); + assert(result_item.enabled, 'result item is not enabled'); + + let mut recipes_counter: RecipesCounter = world.read_model(RECIPES_COUNTER_ID); + let new_id = recipes_counter.count + 1; + recipes_counter.count = new_id; world.write_model(@Recipe { - item1_id, - item2_id, + id: new_id, + item_ids, + item_amounts, result_item_id, + enabled: true, }); - if item1_id != item2_id { - world.write_model(@Recipe { - item1_id: item2_id, - item2_id: item1_id, - result_item_id, - }); - } - } - - fn craft_item( - ref self: ContractState, storage_item_id1: u32, storage_item_id2: u32 - ) { - let mut world = self.world(@"Warpacks"); - - let player = get_caller_address(); - - let mut storageItem1: CharacterItemStorage = world.read_model((player, storage_item_id1)); - assert(storageItem1.itemId != 0, 'item not owned'); - - let mut storageItem2: CharacterItemStorage = world.read_model((player, storage_item_id2)); - assert(storageItem2.itemId != 0, 'item not owned'); - - let recipe: Recipe = world.read_model((storageItem1.itemId, storageItem2.itemId)); - assert(recipe.result_item_id != 0, 'No valid recipe found'); - - storageItem1.itemId = recipe.result_item_id; - storageItem2.itemId = 0; - world.write_model(@storageItem1); - world.write_model(@storageItem2); + world.write_model(@recipes_counter); } } } From d2171ec06134dd19a19eebab6474c448f5e94ee6 Mon Sep 17 00:00:00 2001 From: Asten <0xasten@gmail.com> Date: Wed, 2 Jul 2025 14:12:45 +0800 Subject: [PATCH 2/2] updates craft tests --- src/models/Recipe.cairo | 2 +- src/tests/test_recipe.cairo | 93 +++++++++++++++++++++++++------------ 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/models/Recipe.cairo b/src/models/Recipe.cairo index 6fa2918..a2e70f4 100644 --- a/src/models/Recipe.cairo +++ b/src/models/Recipe.cairo @@ -13,6 +13,6 @@ pub struct Recipe { #[dojo::model] pub struct RecipesCounter { #[key] - pub id: u32, + pub id: felt252, pub count: u32, } diff --git a/src/tests/test_recipe.cairo b/src/tests/test_recipe.cairo index b724038..bb282f5 100644 --- a/src/tests/test_recipe.cairo +++ b/src/tests/test_recipe.cairo @@ -8,12 +8,13 @@ mod tests { use warpack_masters::{ systems::{recipe::{recipe_system, IRecipeDispatcher, IRecipeDispatcherTrait}}, + systems::{actions::{actions, IActionsDispatcher, IActionsDispatcherTrait}}, systems::{item::{item_system, IItemDispatcher}}, models::Item::{m_Item, m_ItemsCounter}, models::CharacterItem::{ - CharacterItemStorage, m_CharacterItemStorage + CharacterItemStorage, m_CharacterItemStorage, CharacterItemsStorageCounter, m_CharacterItemsStorageCounter }, - models::Recipe::{Recipe, m_Recipe}, + models::Recipe::{Recipe, m_Recipe, m_RecipesCounter}, utils::test_utils::add_items }; @@ -24,9 +25,12 @@ mod tests { TestResource::Model(m_Item::TEST_CLASS_HASH.try_into().unwrap()), TestResource::Model(m_ItemsCounter::TEST_CLASS_HASH.try_into().unwrap()), TestResource::Model(m_CharacterItemStorage::TEST_CLASS_HASH.try_into().unwrap()), + TestResource::Model(m_CharacterItemsStorageCounter::TEST_CLASS_HASH.try_into().unwrap()), TestResource::Model(m_Recipe::TEST_CLASS_HASH.try_into().unwrap()), + TestResource::Model(m_RecipesCounter::TEST_CLASS_HASH.try_into().unwrap()), TestResource::Contract(item_system::TEST_CLASS_HASH), TestResource::Contract(recipe_system::TEST_CLASS_HASH), + TestResource::Contract(actions::TEST_CLASS_HASH), ].span() }; ndef @@ -38,6 +42,8 @@ mod tests { .with_writer_of([dojo::utils::bytearray_hash(@"Warpacks")].span()), ContractDefTrait::new(@"Warpacks", @"recipe_system") .with_writer_of([dojo::utils::bytearray_hash(@"Warpacks")].span()), + ContractDefTrait::new(@"Warpacks", @"actions") + .with_writer_of([dojo::utils::bytearray_hash(@"Warpacks")].span()), ].span() } @@ -56,17 +62,16 @@ mod tests { add_items(ref item_system); - recipe_system.add_recipe(1, 2, 3); - - let recipe: Recipe = world.read_model((1, 2)); - assert(recipe.item1_id == 1, 'wrong item1_id'); - assert(recipe.item2_id == 2, 'wrong item2_id'); - assert(recipe.result_item_id == 3, 'wrong result_item_id'); + recipe_system.add_recipe(array![1, 2], array![1, 1], 3); - let recipe: Recipe = world.read_model((2, 1)); - assert(recipe.item1_id == 2, 'wrong item1_id'); - assert(recipe.item2_id == 1, 'wrong item2_id'); + let recipe: Recipe = world.read_model(1); + assert(recipe.id == 1, 'wrong recipe id'); + assert(*recipe.item_ids[0] == 1, 'wrong item_id at index 0'); + assert(*recipe.item_ids[1] == 2, 'wrong item_id at index 1'); + assert(*recipe.item_amounts[0] == 1, 'wrong item_amount at index 0'); + assert(*recipe.item_amounts[1] == 1, 'wrong item_amount at index 1'); assert(recipe.result_item_id == 3, 'wrong result_item_id'); + assert(recipe.enabled == true, 'recipe should be enabled'); } #[test] @@ -84,12 +89,14 @@ mod tests { add_items(ref item_system); - recipe_system.add_recipe(1, 1, 2); + recipe_system.add_recipe(array![1], array![2], 2); - let recipe: Recipe = world.read_model((1, 1)); - assert(recipe.item1_id == 1, 'wrong item1_id'); - assert(recipe.item2_id == 1, 'wrong item2_id'); + let recipe: Recipe = world.read_model(1); + assert(recipe.id == 1, 'wrong recipe id'); + assert(*recipe.item_ids[0] == 1, 'wrong item_id at index 0'); + assert(*recipe.item_amounts[0] == 2, 'wrong item_amount at index 0'); assert(recipe.result_item_id == 2, 'wrong result_item_id'); + assert(recipe.enabled == true, 'recipe should be enabled'); } #[test] @@ -110,12 +117,12 @@ mod tests { let alice = starknet::contract_address_const::<0x1>(); set_contract_address(alice); - recipe_system.add_recipe(1, 2, 3); + recipe_system.add_recipe(array![1, 2], array![1, 1], 3); } #[test] #[available_gas(3000000000000000)] - #[should_panic(expected: ('item1 does not exist', 'ENTRYPOINT_FAILED'))] + #[should_panic(expected: ('item is not enabled', 'ENTRYPOINT_FAILED'))] fn test_add_recipe_item1_doesnt_exists() { let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); @@ -129,12 +136,12 @@ mod tests { add_items(ref item_system); - recipe_system.add_recipe(100, 2, 3); + recipe_system.add_recipe(array![100, 2], array![1, 1], 3); } #[test] #[available_gas(3000000000000000)] - #[should_panic(expected: ('item2 does not exist', 'ENTRYPOINT_FAILED'))] + #[should_panic(expected: ('item is not enabled', 'ENTRYPOINT_FAILED'))] fn test_add_recipe_item2_doesnt_exists() { let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); @@ -148,12 +155,12 @@ mod tests { add_items(ref item_system); - recipe_system.add_recipe(1, 200, 3); + recipe_system.add_recipe(array![1, 200], array![1, 1], 3); } #[test] #[available_gas(3000000000000000)] - #[should_panic(expected: ('result item does not exist', 'ENTRYPOINT_FAILED'))] + #[should_panic(expected: ('result item is not enabled', 'ENTRYPOINT_FAILED'))] fn test_add_recipe_result_doesnt_exists() { let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); @@ -167,7 +174,7 @@ mod tests { add_items(ref item_system); - recipe_system.add_recipe(1, 2, 300); + recipe_system.add_recipe(array![1, 2], array![1, 1], 300); } #[test] @@ -183,27 +190,47 @@ mod tests { let (contract_address, _) = world.dns(@"recipe_system").unwrap(); let mut recipe_system = IRecipeDispatcher { contract_address }; + let (contract_address, _) = world.dns(@"actions").unwrap(); + let mut actions = IActionsDispatcher { contract_address }; + add_items(ref item_system); - recipe_system.add_recipe(1, 2, 3); + recipe_system.add_recipe(array![1, 2], array![1, 1], 3); let alice = starknet::contract_address_const::<0x1>(); world.write_model(@CharacterItemStorage { player: alice, id: 1, itemId: 1}); world.write_model(@CharacterItemStorage { player: alice, id: 2, itemId: 2}); + world.write_model(@CharacterItemsStorageCounter { player: alice, count: 2}); set_contract_address(alice); - recipe_system.craft_item(1, 2); + actions.craft_item(1, array![1, 2]); let item_at_1: CharacterItemStorage = world.read_model((alice, 1)); - assert(item_at_1.itemId == 3, 'wrong itemId at (alice, 1)'); + assert(item_at_1.itemId == 0, 'item 1 should be consumed'); let item_at_2: CharacterItemStorage = world.read_model((alice, 2)); - assert(item_at_2.itemId == 0, 'wrong itemId at (alice, 2)'); + assert(item_at_2.itemId == 3, 'wrong crafted itemId'); + } + + #[test] + #[available_gas(3000000000000000)] + #[should_panic(expected: ('recipe is not enabled', 'ENTRYPOINT_FAILED'))] + fn test_craft_item_with_disabled_recipe() { + let ndef = namespace_def(); + let mut world = spawn_test_world([ndef].span()); + world.sync_perms_and_inits(contract_defs()); + + let (contract_address, _) = world.dns(@"actions").unwrap(); + let mut actions = IActionsDispatcher { contract_address }; + + let alice = starknet::contract_address_const::<0x1>(); + set_contract_address(alice); + actions.craft_item(999, array![1, 2]); } #[test] #[available_gas(3000000000000000)] - #[should_panic(expected: ('No valid recipe found', 'ENTRYPOINT_FAILED'))] - fn test_no_valid_recipe_found() { + #[should_panic(expected: ('item not enough', 'ENTRYPOINT_FAILED'))] + fn test_craft_item_insufficient_items() { let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); world.sync_perms_and_inits(contract_defs()); @@ -214,13 +241,19 @@ mod tests { let (contract_address, _) = world.dns(@"recipe_system").unwrap(); let mut recipe_system = IRecipeDispatcher { contract_address }; + let (contract_address, _) = world.dns(@"actions").unwrap(); + let mut actions = IActionsDispatcher { contract_address }; + add_items(ref item_system); + recipe_system.add_recipe(array![1, 2], array![2, 1], 3); // Requires 2 of item 1 + let alice = starknet::contract_address_const::<0x1>(); - world.write_model(@CharacterItemStorage { player: alice, id: 1, itemId: 1}); + world.write_model(@CharacterItemStorage { player: alice, id: 1, itemId: 1}); // Only 1 of item 1 world.write_model(@CharacterItemStorage { player: alice, id: 2, itemId: 2}); + world.write_model(@CharacterItemsStorageCounter { player: alice, count: 2}); set_contract_address(alice); - recipe_system.craft_item(1, 2); + actions.craft_item(1, array![1, 2]); } } \ No newline at end of file