From 4dea7cdf150fd00954a667bf84f5b4cdadfec524 Mon Sep 17 00:00:00 2001 From: dub_zn Date: Sun, 27 Apr 2025 19:57:09 -0300 Subject: [PATCH 1/7] Add `store.cairo` logic --- .../dojo_examples/combat_game/src/store.cairo | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/backend/dojo_examples/combat_game/src/store.cairo b/backend/dojo_examples/combat_game/src/store.cairo index 8b13789..4d3258d 100644 --- a/backend/dojo_examples/combat_game/src/store.cairo +++ b/backend/dojo_examples/combat_game/src/store.cairo @@ -1 +1,207 @@ +use dojo::{model::ModelStorage, world::WorldStorage}; +use core::num::traits::zero::Zero; +use combat_game::{ + constants::SECONDS_PER_DAY, + models::{player::Player, beast::Beast, beast_stats::BeastStats, battle::Battle}, + types::{beast::BeastType, status_condition::StatusCondition, battle_status::BattleStatus}, +}; +use starknet::ContractAddress; + +#[derive(Drop)] +struct Store { + world: WorldStorage, +} + +#[generate_trait] +impl StoreImpl of StoreTrait { + fn new(world: WorldStorage) -> Store { + Store { world: world } + } + + // [ Initialization methods ] + // TODO: add attacks based on beast type + // Blocked: Attacks are not implemented + fn init_beast_attacks(ref self: Store, beast_type: BeastType) { + match beast_type { + BeastType::Fire => {}, + BeastType::Water => {}, + BeastType::Earth => {}, + BeastType::Electric => {}, + BeastType::Dragon => {}, + BeastType::Ice => {}, + BeastType::Magic => {}, + BeastType::Rock => {}, + BeastType::Undefined => { + panic!( + "[ Store ] - BeastType `Undefined (id {})` cannot be initialize.", + Into::::into(beast_type), + ); + }, + } + } + + // Implementation includes initialization methods + // Suggestion: Since time will be handled as u64 because the standard, maybe we should change + // the type of `last_active_day` and `creation_date` to u64 + fn new_player(ref self: Store) -> Player { + let player = Player { + address: starknet::get_caller_address(), + current_beast_id: Zero::zero(), + battles_won: Zero::zero(), + battles_lost: Zero::zero(), + last_active_day: (starknet::get_block_timestamp() / SECONDS_PER_DAY) + .try_into() + .unwrap(), + creation_day: (starknet::get_block_timestamp() / SECONDS_PER_DAY).try_into().unwrap(), + }; + self.world.write_model(@player); + player + } + + // TODO: Not implemented + fn new_attack(ref self: Store) {} + + // TODO: Should the data for model initializations be sent as a parameter? + // Maybe consume `beast_id` from an entity like `BeastTracker` to set an autoincrement id? + fn new_beast(ref self: Store, beast_id: u16, beast_type: BeastType) -> Beast { + let beast = Beast { + player: starknet::get_caller_address(), + beast_id, + level: 1, + experience: Zero::zero(), + beast_type: Into::::into(beast_type), + }; + self.world.write_model(@beast); + beast + } + + // TODO: Should the data for model initializations be sent as a parameter? + // Is there a reason why `beast_id` in BeastStats is u256 + fn new_beast_stats( + ref self: Store, + beast_id: u16, + max_hp: u16, + current_hp: u16, + attack: u16, + defense: u16, + speed: u16, + accuracy: u8, + evasion: u8, + status_condition: StatusCondition, + ) -> BeastStats { + let beast_stats = BeastStats { + beast_id: beast_id.into(), + max_hp, + current_hp, + attack, + defense, + speed, + accuracy, + evasion, + status_condition: Into::::into(status_condition), + last_timestamp: starknet::get_block_timestamp(), + }; + self.world.write_model(@beast_stats); + beast_stats + } + + // Maybe consume `battle_id` from an entity like `BattleTracker` to set an autoincrement id? + // I think that BattleType enum is missing + // TODO: Use pseudo-random for initial turn + fn new_battle( + ref self: Store, + battle_id: u256, + player1: ContractAddress, + player2: ContractAddress, + battle_type: u8, + ) -> Battle { + let battle = Battle { + id: battle_id, + player1, + player2, + current_turn: player1, + status: Into::::into(BattleStatus::Waiting), + winner_id: Zero::zero(), + battle_type: battle_type, + }; + self.world.write_model(@battle); + battle + } + + // TODO: Use pseudo-random for initial turn + fn create_rematch(ref self: Store, battle_id: u256) -> Battle { + let battle = self.read_battle(battle_id); + let rematch = Battle { + id: battle_id, + player1: battle.player1, + player2: battle.player2, + current_turn: battle.player1, + status: Into::::into(BattleStatus::Waiting), + winner_id: Zero::zero(), + battle_type: battle.battle_type, + }; + self.world.write_model(@rematch); + rematch + } + + // [ Getter methods ] + fn read_player(self: @Store) -> Player { + self.read_player_from_address(starknet::get_caller_address()) + } + + fn read_player_from_address(self: @Store, player_address: ContractAddress) -> Player { + self.world.read_model((player_address)) + } + + // Blocked: Attacks are not implemented + fn read_attack(self: @Store, beast_id: u16, attack_id: u16) { + // self.world.read_model((beast_id, attack_id)) + } + + fn read_beast(self: @Store, beast_id: u16) -> Beast { + self.world.read_model((starknet::get_caller_address(), beast_id)) + } + + // TODO: Ask about this one + fn read_ai_beast(self: @Store) {} + + // Is there a reason why `beast_id` in BeastStats is u256 + fn read_beast_stats(self: @Store, beast_id: u16) -> BeastStats { + self.world.read_model((Into::::into(beast_id))) + } + + fn read_battle(self: @Store, battle_id: u256) -> Battle { + self.world.read_model((battle_id)) + } + + // [ Setter methods ] + + // Implementation includes setter methods: + fn write_player(ref self: Store, player: Player) { + self.world.write_model(@player) + } + + // Blocked: Attacks are not implemented + fn write_attack(ref self: Store, attack: u32) {// self.world.write_model(@attack) + } + + fn write_beast(ref self: Store, beast: Beast) { + self.world.write_model(@beast) + } + + fn write_beast_stats(ref self: Store, beast_stats: BeastStats) { + self.world.write_model(@beast_stats) + } + + fn write_battle(ref self: Store, battle: Battle) { + self.world.write_model(@battle) + } + + // [ Game logic methods] + + fn award_battle_experience(ref self: Store) {} + fn is_attack_usable(ref self: Store) {} + fn update_player_battle_result(ref self: Store) {} + fn process_attack(ref self: Store) {} +} From 8ae5b907548a1e84d215a3aed1fa1b68afc66f45 Mon Sep 17 00:00:00 2001 From: dub_zn Date: Sun, 27 Apr 2025 19:57:30 -0300 Subject: [PATCH 2/7] Change visibility for `beast`, `player`, and `beast_stats` --- backend/dojo_examples/combat_game/src/lib.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/dojo_examples/combat_game/src/lib.cairo b/backend/dojo_examples/combat_game/src/lib.cairo index f9e3a2b..e4d2ac5 100644 --- a/backend/dojo_examples/combat_game/src/lib.cairo +++ b/backend/dojo_examples/combat_game/src/lib.cairo @@ -3,9 +3,9 @@ mod store; mod models { pub mod battle; - mod beast; - mod player; - mod beast_stats; + pub mod beast; + pub mod player; + pub mod beast_stats; mod potion; mod bag; } From d9cee0a4add215983417a8759a972148457eefaf Mon Sep 17 00:00:00 2001 From: dub_zn Date: Sun, 4 May 2025 10:42:03 -0300 Subject: [PATCH 3/7] add `skill` to store --- .../dojo_examples/combat_game/src/store.cairo | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/backend/dojo_examples/combat_game/src/store.cairo b/backend/dojo_examples/combat_game/src/store.cairo index 916c3f2..a7167f0 100644 --- a/backend/dojo_examples/combat_game/src/store.cairo +++ b/backend/dojo_examples/combat_game/src/store.cairo @@ -1,9 +1,10 @@ use dojo::{model::ModelStorage, world::WorldStorage}; use core::num::traits::zero::Zero; use combat_game::{ + helpers::pseudo_random::PseudoRandom::generate_random_u8, constants::SECONDS_PER_DAY, - models::{player::Player, beast::Beast, beast_stats::BeastStats, battle::Battle}, - types::{beast_type::BeastType, status_condition::StatusCondition, battle_status::BattleStatus}, + models::{player::Player, beast::Beast, skill::Skill, beast_skill::BeastSkill, beast_stats::BeastStats, battle::Battle}, + types::{beast_type::BeastType, skill::SkillType, status_condition::StatusCondition, battle_status::BattleStatus}, }; use starknet::ContractAddress; @@ -21,7 +22,6 @@ impl StoreImpl of StoreTrait { // [ Initialization methods ] // TODO: add attacks based on beast type - // Blocked: Attacks are not implemented fn init_beast_attacks(ref self: Store, beast_type: BeastType) { match beast_type { BeastType::Light => {}, @@ -52,11 +52,17 @@ impl StoreImpl of StoreTrait { player } - // TODO: Not implemented - fn new_attack(ref self: Store) {} + fn new_skill(ref self: Store, id: u256, power: u16, skill_type: SkillType, min_level_required: u8) -> Skill { + let skill = Skill { + id, + power, + skill_type, + min_level_required + }; + self.world.write_model(@skill); + skill + } - // TODO: Should the data for model initializations be sent as a parameter? - // Maybe consume `beast_id` from an entity like `BeastTracker` to set an autoincrement id? fn new_beast(ref self: Store, beast_id: u16, beast_type: BeastType) -> Beast { let beast = Beast { player: starknet::get_caller_address(), @@ -69,8 +75,6 @@ impl StoreImpl of StoreTrait { beast } - // TODO: Should the data for model initializations be sent as a parameter? - // Is there a reason why `beast_id` in BeastStats is u256 fn new_beast_stats( ref self: Store, beast_id: u16, @@ -99,7 +103,6 @@ impl StoreImpl of StoreTrait { beast_stats } - // Maybe consume `battle_id` from an entity like `BattleTracker` to set an autoincrement id? // I think that BattleType enum is missing // TODO: Use pseudo-random for initial turn fn new_battle( @@ -109,11 +112,12 @@ impl StoreImpl of StoreTrait { player2: ContractAddress, battle_type: u8, ) -> Battle { + let players = array![player1, player2]; let battle = Battle { id: battle_id, player1, player2, - current_turn: player1, + current_turn: *players.at(generate_random_u8(battle_id.try_into().unwrap(), 0, 0, players.len().try_into().unwrap()).into()),, status: Into::::into(BattleStatus::Waiting), winner_id: Zero::zero(), battle_type: battle_type, @@ -122,14 +126,15 @@ impl StoreImpl of StoreTrait { battle } - // TODO: Use pseudo-random for initial turn fn create_rematch(ref self: Store, battle_id: u256) -> Battle { let battle = self.read_battle(battle_id); + let players = array![battle.player1, battle.player2]; + let rematch = Battle { id: battle_id, player1: battle.player1, player2: battle.player2, - current_turn: battle.player1, + current_turn: *players.at(generate_random_u8(battle_id.try_into().unwrap(), 0, 0, players.len().try_into().unwrap()).into()), status: Into::::into(BattleStatus::Waiting), winner_id: Zero::zero(), battle_type: battle.battle_type, @@ -147,9 +152,12 @@ impl StoreImpl of StoreTrait { self.world.read_model((player_address)) } - // Blocked: Attacks are not implemented - fn read_attack(self: @Store, beast_id: u16, attack_id: u16) { - // self.world.read_model((beast_id, attack_id)) + fn read_skill(self: @Store, skill_id: u16) -> Skill { + self.world.read_model((skill_id)) + } + + fn read_beast_skill(self: @Store, beast_id: u16) -> BeastSkill { + self.world.read_model((beast_id)) } fn read_beast(self: @Store, beast_id: u16) -> Beast { @@ -159,7 +167,6 @@ impl StoreImpl of StoreTrait { // TODO: Ask about this one fn read_ai_beast(self: @Store) {} - // Is there a reason why `beast_id` in BeastStats is u256 fn read_beast_stats(self: @Store, beast_id: u16) -> BeastStats { self.world.read_model((Into::::into(beast_id))) } @@ -169,14 +176,17 @@ impl StoreImpl of StoreTrait { } // [ Setter methods ] - // Implementation includes setter methods: fn write_player(ref self: Store, player: Player) { self.world.write_model(@player) } - // Blocked: Attacks are not implemented - fn write_attack(ref self: Store, attack: u32) {// self.world.write_model(@attack) + fn write_skill(ref self: Store, skill: Skill) { + self.world.write_model(@skill) + } + + fn write_beast_skills(ref self: Store, beast_skill: BeastSkill) { + self.world.write_model(@beast_skill) } fn write_beast(ref self: Store, beast: Beast) { From f4c67d3b2c4f66316951b43e8a06b86ba51d063b Mon Sep 17 00:00:00 2001 From: dub_zn Date: Sun, 4 May 2025 12:06:11 -0300 Subject: [PATCH 4/7] add `init_skills()` --- .../combat_game/src/models/skill.cairo | 13 ++ .../dojo_examples/combat_game/src/store.cairo | 135 +++++++++++++++--- 2 files changed, 125 insertions(+), 23 deletions(-) diff --git a/backend/dojo_examples/combat_game/src/models/skill.cairo b/backend/dojo_examples/combat_game/src/models/skill.cairo index c4a1fc6..6be617c 100644 --- a/backend/dojo_examples/combat_game/src/models/skill.cairo +++ b/backend/dojo_examples/combat_game/src/models/skill.cairo @@ -14,6 +14,19 @@ const FREEZE_SKILL_DAMAGE: u16 = 40; const SHOCK_SKILL_DAMAGE: u16 = 45; const DEFAULT_SKILL_DAMAGE: u16 = 30; +pub const SLASH_SKILL_ID: u256 = 1; +pub const BEAM_SKILL_ID: u256 = 2; +pub const WAVE_SKILL_ID: u256 = 3; +pub const PUNCH_SKILL_ID: u256 = 4; +pub const KICK_SKILL_ID: u256 = 5; +pub const BLAST_SKILL_ID: u256 = 6; +pub const PIERCE_SKILL_ID: u256 = 7; +pub const SMASH_SKILL_ID: u256 = 8; +pub const BURN_SKILL_ID: u256 = 9; +pub const FREEZE_SKILL_ID: u256 = 10; +pub const SHOCK_SKILL_ID: u256 = 11; +pub const DEFAULT_SKILL_ID: u256 = 12; + #[derive(Copy, Drop, Serde, Debug, Introspect, PartialEq)] #[dojo::model] pub struct Skill { diff --git a/backend/dojo_examples/combat_game/src/store.cairo b/backend/dojo_examples/combat_game/src/store.cairo index a7167f0..b5c5c9d 100644 --- a/backend/dojo_examples/combat_game/src/store.cairo +++ b/backend/dojo_examples/combat_game/src/store.cairo @@ -1,10 +1,15 @@ use dojo::{model::ModelStorage, world::WorldStorage}; use core::num::traits::zero::Zero; use combat_game::{ - helpers::pseudo_random::PseudoRandom::generate_random_u8, - constants::SECONDS_PER_DAY, - models::{player::Player, beast::Beast, skill::Skill, beast_skill::BeastSkill, beast_stats::BeastStats, battle::Battle}, - types::{beast_type::BeastType, skill::SkillType, status_condition::StatusCondition, battle_status::BattleStatus}, + helpers::pseudo_random::PseudoRandom::generate_random_u8, constants::SECONDS_PER_DAY, + models::{ + player::Player, beast::Beast, skill, skill::{Skill, SkillTrait}, beast_skill::BeastSkill, + beast_stats::BeastStats, battle::Battle, + }, + types::{ + beast_type::BeastType, skill::SkillType, status_condition::StatusCondition, + battle_status::BattleStatus, + }, }; use starknet::ContractAddress; @@ -27,16 +32,93 @@ impl StoreImpl of StoreTrait { BeastType::Light => {}, BeastType::Magic => {}, BeastType::Shadow => {}, - _ => { - panic!( - "[Store] - BeastType `{}` cannot be initialize.", beast_type); - }, + _ => { panic!("[Store] - BeastType `{}` cannot be initialize.", beast_type); }, } } - // Implementation includes initialization methods - // Suggestion: Since time will be handled as u64 because the standard, maybe we should change - // the type of `last_active_day` and `creation_date` to u64 + // TODO: Define `min_level_required` for every skill + fn init_skills(ref self: Store) { + self + .world + .write_models( + array![ + @Skill { + id: skill::SLASH_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Slash), + skill_type: SkillType::Slash, + min_level_required: 1, + }, + @Skill { + id: skill::BEAM_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Beam), + skill_type: SkillType::Beam, + min_level_required: 1, + }, + @Skill { + id: skill::WAVE_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Wave), + skill_type: SkillType::Wave, + min_level_required: 1, + }, + @Skill { + id: skill::PUNCH_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Punch), + skill_type: SkillType::Punch, + min_level_required: 1, + }, + @Skill { + id: skill::KICK_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Kick), + skill_type: SkillType::Kick, + min_level_required: 1, + }, + @Skill { + id: skill::BLAST_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Blast), + skill_type: SkillType::Blast, + min_level_required: 1, + }, + @Skill { + id: skill::PIERCE_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Pierce), + skill_type: SkillType::Pierce, + min_level_required: 1, + }, + @Skill { + id: skill::SMASH_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Smash), + skill_type: SkillType::Smash, + min_level_required: 1, + }, + @Skill { + id: skill::BURN_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Burn), + skill_type: SkillType::Burn, + min_level_required: 1, + }, + @Skill { + id: skill::FREEZE_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Freeze), + skill_type: SkillType::Freeze, + min_level_required: 1, + }, + @Skill { + id: skill::SHOCK_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Shock), + skill_type: SkillType::Shock, + min_level_required: 1, + }, + @Skill { + id: skill::DEFAULT_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Default), + skill_type: SkillType::Default, + min_level_required: 1, + }, + ] + .span(), + ); + } + fn new_player(ref self: Store) -> Player { let player = Player { address: starknet::get_caller_address(), @@ -52,13 +134,10 @@ impl StoreImpl of StoreTrait { player } - fn new_skill(ref self: Store, id: u256, power: u16, skill_type: SkillType, min_level_required: u8) -> Skill { - let skill = Skill { - id, - power, - skill_type, - min_level_required - }; + fn new_skill( + ref self: Store, id: u256, power: u16, skill_type: SkillType, min_level_required: u8, + ) -> Skill { + let skill = Skill { id, power, skill_type, min_level_required }; self.world.write_model(@skill); skill } @@ -103,8 +182,6 @@ impl StoreImpl of StoreTrait { beast_stats } - // I think that BattleType enum is missing - // TODO: Use pseudo-random for initial turn fn new_battle( ref self: Store, battle_id: u256, @@ -117,7 +194,13 @@ impl StoreImpl of StoreTrait { id: battle_id, player1, player2, - current_turn: *players.at(generate_random_u8(battle_id.try_into().unwrap(), 0, 0, players.len().try_into().unwrap()).into()),, + current_turn: *players + .at( + generate_random_u8( + battle_id.try_into().unwrap(), 0, 0, players.len().try_into().unwrap(), + ) + .into(), + ), status: Into::::into(BattleStatus::Waiting), winner_id: Zero::zero(), battle_type: battle_type, @@ -129,12 +212,18 @@ impl StoreImpl of StoreTrait { fn create_rematch(ref self: Store, battle_id: u256) -> Battle { let battle = self.read_battle(battle_id); let players = array![battle.player1, battle.player2]; - + let rematch = Battle { id: battle_id, player1: battle.player1, player2: battle.player2, - current_turn: *players.at(generate_random_u8(battle_id.try_into().unwrap(), 0, 0, players.len().try_into().unwrap()).into()), + current_turn: *players + .at( + generate_random_u8( + battle_id.try_into().unwrap(), 0, 0, players.len().try_into().unwrap(), + ) + .into(), + ), status: Into::::into(BattleStatus::Waiting), winner_id: Zero::zero(), battle_type: battle.battle_type, From ea10d662c2f05e0809955649268702d0983188d5 Mon Sep 17 00:00:00 2001 From: dub_zn Date: Tue, 6 May 2025 21:48:48 -0300 Subject: [PATCH 5/7] resolve feedback comments --- .../combat_game/src/models/battle.cairo | 5 +- .../combat_game/src/models/skill.cairo | 13 +- .../dojo_examples/combat_game/src/store.cairo | 210 ++++++++++++++++-- .../combat_game/src/types/battle_status.cairo | 14 +- 4 files changed, 211 insertions(+), 31 deletions(-) diff --git a/backend/dojo_examples/combat_game/src/models/battle.cairo b/backend/dojo_examples/combat_game/src/models/battle.cairo index c419510..2e9e80b 100644 --- a/backend/dojo_examples/combat_game/src/models/battle.cairo +++ b/backend/dojo_examples/combat_game/src/models/battle.cairo @@ -1,6 +1,7 @@ use starknet::ContractAddress; +use combat_game::types::battle_status::BattleStatus; -#[derive(Copy, Drop, Serde, Debug, PartialEq)] +#[derive(Copy, Drop, Serde, Debug, Introspect, PartialEq)] #[dojo::model] pub struct Battle { #[key] @@ -8,7 +9,7 @@ pub struct Battle { pub player1: ContractAddress, pub player2: ContractAddress, pub current_turn: ContractAddress, - pub status: u8, + pub status: BattleStatus, pub winner_id: ContractAddress, pub battle_type: u8, } diff --git a/backend/dojo_examples/combat_game/src/models/skill.cairo b/backend/dojo_examples/combat_game/src/models/skill.cairo index 6be617c..68fd15f 100644 --- a/backend/dojo_examples/combat_game/src/models/skill.cairo +++ b/backend/dojo_examples/combat_game/src/models/skill.cairo @@ -20,12 +20,13 @@ pub const WAVE_SKILL_ID: u256 = 3; pub const PUNCH_SKILL_ID: u256 = 4; pub const KICK_SKILL_ID: u256 = 5; pub const BLAST_SKILL_ID: u256 = 6; -pub const PIERCE_SKILL_ID: u256 = 7; -pub const SMASH_SKILL_ID: u256 = 8; -pub const BURN_SKILL_ID: u256 = 9; -pub const FREEZE_SKILL_ID: u256 = 10; -pub const SHOCK_SKILL_ID: u256 = 11; -pub const DEFAULT_SKILL_ID: u256 = 12; +pub const CRUSH_SKILL_ID: u256 = 7; +pub const PIERCE_SKILL_ID: u256 = 8; +pub const SMASH_SKILL_ID: u256 = 9; +pub const BURN_SKILL_ID: u256 = 10; +pub const FREEZE_SKILL_ID: u256 = 11; +pub const SHOCK_SKILL_ID: u256 = 12; +pub const DEFAULT_SKILL_ID: u256 = 13; #[derive(Copy, Drop, Serde, Debug, Introspect, PartialEq)] #[dojo::model] diff --git a/backend/dojo_examples/combat_game/src/store.cairo b/backend/dojo_examples/combat_game/src/store.cairo index b5c5c9d..8ac3d76 100644 --- a/backend/dojo_examples/combat_game/src/store.cairo +++ b/backend/dojo_examples/combat_game/src/store.cairo @@ -1,10 +1,10 @@ use dojo::{model::ModelStorage, world::WorldStorage}; use core::num::traits::zero::Zero; use combat_game::{ - helpers::pseudo_random::PseudoRandom::generate_random_u8, constants::SECONDS_PER_DAY, + helpers::{pseudo_random::PseudoRandom::generate_random_u8}, constants::SECONDS_PER_DAY, models::{ - player::Player, beast::Beast, skill, skill::{Skill, SkillTrait}, beast_skill::BeastSkill, - beast_stats::BeastStats, battle::Battle, + player::Player, beast::{Beast, BeastTrait}, skill, skill::{Skill, SkillTrait}, + beast_skill::BeastSkill, beast_stats::BeastStats, battle::Battle, }, types::{ beast_type::BeastType, skill::SkillType, status_condition::StatusCondition, @@ -14,7 +14,7 @@ use combat_game::{ use starknet::ContractAddress; -#[derive(Drop)] +#[derive(Drop, Copy)] struct Store { world: WorldStorage, } @@ -26,17 +26,58 @@ impl StoreImpl of StoreTrait { } // [ Initialization methods ] - // TODO: add attacks based on beast type - fn init_beast_attacks(ref self: Store, beast_type: BeastType) { - match beast_type { - BeastType::Light => {}, - BeastType::Magic => {}, - BeastType::Shadow => {}, - _ => { panic!("[Store] - BeastType `{}` cannot be initialize.", beast_type); }, + fn init_beast_skills(ref self: Store, beast_id: u16) { + let beast = self.read_beast(beast_id); + match beast.beast_type { + BeastType::Light => { + self + .write_beast_skills( + BeastSkill { + beast_id, + skills_ids: array![ + skill::BEAM_SKILL_ID, + skill::SLASH_SKILL_ID, + skill::PIERCE_SKILL_ID, + skill::WAVE_SKILL_ID, + ] + .span(), + }, + ); + }, + BeastType::Magic => { + self + .write_beast_skills( + BeastSkill { + beast_id, + skills_ids: array![ + skill::BLAST_SKILL_ID, + skill::FREEZE_SKILL_ID, + skill::BURN_SKILL_ID, + skill::PUNCH_SKILL_ID, + ] + .span(), + }, + ); + }, + BeastType::Shadow => { + self + .write_beast_skills( + BeastSkill { + beast_id, + skills_ids: array![ + skill::SMASH_SKILL_ID, + skill::CRUSH_SKILL_ID, + skill::SHOCK_SKILL_ID, + skill::KICK_SKILL_ID, + ] + .span(), + }, + ); + }, + _ => { panic!("[Store] - BeastType `{}` has no skills defined.", beast.beast_type); }, } } - // TODO: Define `min_level_required` for every skill fn init_skills(ref self: Store) { self .world @@ -78,6 +119,12 @@ impl StoreImpl of StoreTrait { skill_type: SkillType::Blast, min_level_required: 1, }, + @Skill { + id: skill::CRUSH_SKILL_ID, + power: SkillTrait::base_damage(SkillType::Crush), + skill_type: SkillType::Crush, + min_level_required: 1, + }, @Skill { id: skill::PIERCE_SKILL_ID, power: SkillTrait::base_damage(SkillType::Pierce), @@ -201,7 +248,7 @@ impl StoreImpl of StoreTrait { ) .into(), ), - status: Into::::into(BattleStatus::Waiting), + status: BattleStatus::Waiting, winner_id: Zero::zero(), battle_type: battle_type, }; @@ -224,7 +271,7 @@ impl StoreImpl of StoreTrait { ) .into(), ), - status: Into::::into(BattleStatus::Waiting), + status: BattleStatus::Waiting, winner_id: Zero::zero(), battle_type: battle.battle_type, }; @@ -241,7 +288,7 @@ impl StoreImpl of StoreTrait { self.world.read_model((player_address)) } - fn read_skill(self: @Store, skill_id: u16) -> Skill { + fn read_skill(self: @Store, skill_id: u256) -> Skill { self.world.read_model((skill_id)) } @@ -253,9 +300,6 @@ impl StoreImpl of StoreTrait { self.world.read_model((starknet::get_caller_address(), beast_id)) } - // TODO: Ask about this one - fn read_ai_beast(self: @Store) {} - fn read_beast_stats(self: @Store, beast_id: u16) -> BeastStats { self.world.read_model((Into::::into(beast_id))) } @@ -291,9 +335,131 @@ impl StoreImpl of StoreTrait { } // [ Game logic methods] + fn award_battle_experience(ref self: Store, beast_id: u16, exp_amount: u16) -> bool { + // Read beast and its stats + let mut beast = self.read_beast(beast_id); + + // Add experience + beast.experience += exp_amount; - fn award_battle_experience(ref self: Store) {} - fn is_attack_usable(ref self: Store) {} - fn update_player_battle_result(ref self: Store) {} - fn process_attack(ref self: Store) {} + // Check if level up is needed + // TODO: ExperienceCalculatorTrait is not implemented + // let exp_needed = ExperienceTrrait::calculate_exp_needed_for_level(beast.level); + let exp_needed = 10; + let level_up_occurred = beast.experience >= exp_needed; + + if level_up_occurred { + // Calculate remaining exp + // TODO: ExperienceCalculatorTrait is not implemented + // beast.experience = ExperienceTrrait::remaining_exp_after_level_up(beast.level, beast.experience); + beast.experience = 5; + + // Increase level + beast.level += 1; + + // Update beast stats + // TODO: `beast_stats.level_up()` is not implemented + let mut beast_stats = self.read_beast_stats(beast_id); + // beast_stats.level_up(beast.beast_type); + self.write_beast_stats(beast_stats); + } + + self.write_beast(beast); + level_up_occurred + } + + fn is_skill_usable(ref self: Store, beast_id: u16, skill_id: u256) -> bool { + let beast_skills = self.read_beast_skill(beast_id); + let mut found = false; + for beast_skill in beast_skills.skills_ids { + if beast_skill == @skill_id { + found = true; + break; + } + }; + found + } + + fn update_player_battle_result(mut self: Store, won: bool) { + let mut player = self.read_player(); + if won { + player.battles_won += 1; + } else { + player.battles_lost += 1; + } + player + .last_active_day = (starknet::get_block_timestamp() / SECONDS_PER_DAY) + .try_into() + .unwrap(); + self.write_player(player); + } + + // Process attack in battle + fn process_attack( + ref self: Store, + battle_id: u256, + attacker_beast_id: u16, + defender_beast_id: u16, + skill_id: u256, + ) -> (u16, bool, bool) { + // Read battle + let mut battle = self.read_battle(battle_id); + assert!( + battle.status == BattleStatus::Active, + "Battle should be in `Active` status (current `{}`)", + battle.status, + ); + + // Verify attack is usable + assert!( + self.is_skill_usable(attacker_beast_id, skill_id), + "Beast {} can't use skill {}", + attacker_beast_id, + skill_id, + ); + + // Read beast data + let mut attacker_beast = self.read_beast(attacker_beast_id); + let mut defender_beast = self.read_beast(defender_beast_id); + + // Read beast stats + let mut attacker_stats = self.read_beast_stats(attacker_beast_id); + let mut defender_stats = self.read_beast_stats(defender_beast_id); + + // Check if attacker can attack + // assert(attacker_stats.can_attack(), 'Beast cannot attack'); // No implemented + + // Calculate damage + // TODO: Define `attack_factor` right now is hard coded to 1 + let skill = self.read_skill(skill_id); + let (damage, is_favored, is_effective) = attacker_beast + .attack(defender_beast.beast_type, skill.skill_type, 1); + + defender_stats + .current_hp = + if damage > defender_stats.current_hp { + Zero::zero() + } else { + defender_stats.current_hp - damage + }; + // battle.update_timestamp(); // No implemented + + // Check if battle is over + if defender_stats.current_hp.is_zero() { + // End battle + battle.status = BattleStatus::Finished; + + // Update player stats + self.update_player_battle_result(won: true); + + // TODO: Define base experience for winning a game + self.award_battle_experience(attacker_beast_id, 1); + } + + // Save changes + self.write_battle(battle); + self.write_beast_stats(attacker_stats); + self.write_beast_stats(defender_stats); + (damage, is_favored, is_effective) + } } diff --git a/backend/dojo_examples/combat_game/src/types/battle_status.cairo b/backend/dojo_examples/combat_game/src/types/battle_status.cairo index 4a05c34..ca07804 100644 --- a/backend/dojo_examples/combat_game/src/types/battle_status.cairo +++ b/backend/dojo_examples/combat_game/src/types/battle_status.cairo @@ -1,4 +1,4 @@ -#[derive(Copy, Drop, Serde, Debug, PartialEq)] +#[derive(Copy, Drop, Serde, Debug, Introspect, PartialEq)] pub enum BattleStatus { Waiting, Active, @@ -44,6 +44,18 @@ pub impl Intou8BattleStatus of Into { } } +pub impl BattleStatusDisplay of core::fmt::Display { + fn fmt(self: @BattleStatus, ref f: core::fmt::Formatter) -> Result<(), core::fmt::Error> { + let s = match self { + BattleStatus::Waiting => "Waiting", + BattleStatus::Active => "Active", + BattleStatus::Finished => "Finished", + BattleStatus::None => "None", + }; + f.buffer.append(@s); + Result::Ok(()) + } +} #[cfg(test)] mod tests { From 85c22cb6fed954a321b66a33166877d4757aea96 Mon Sep 17 00:00:00 2001 From: dub_zn Date: Tue, 6 May 2025 21:57:53 -0300 Subject: [PATCH 6/7] set `winner_id` in `process_attack()` method --- backend/dojo_examples/combat_game/src/store.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/dojo_examples/combat_game/src/store.cairo b/backend/dojo_examples/combat_game/src/store.cairo index 8ac3d76..e24c5db 100644 --- a/backend/dojo_examples/combat_game/src/store.cairo +++ b/backend/dojo_examples/combat_game/src/store.cairo @@ -448,6 +448,7 @@ impl StoreImpl of StoreTrait { if defender_stats.current_hp.is_zero() { // End battle battle.status = BattleStatus::Finished; + battle.winner_id = starknet::get_caller_address(); // Update player stats self.update_player_battle_result(won: true); From 650fbfbe0996dd5920a830289970406cba6f4859 Mon Sep 17 00:00:00 2001 From: dub_zn Date: Fri, 6 Jun 2025 17:11:47 -0300 Subject: [PATCH 7/7] Remove `beast_stats` TODOs from `process_attack` --- .../combat_game/src/models/beast_stats.cairo | 2 +- .../dojo_examples/combat_game/src/store.cairo | 33 +++++-------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/backend/dojo_examples/combat_game/src/models/beast_stats.cairo b/backend/dojo_examples/combat_game/src/models/beast_stats.cairo index 60171e6..983d413 100644 --- a/backend/dojo_examples/combat_game/src/models/beast_stats.cairo +++ b/backend/dojo_examples/combat_game/src/models/beast_stats.cairo @@ -19,7 +19,7 @@ pub struct BeastStats { } #[generate_trait] -impl BeastStatsActions of BeastStatsActionTrait { +pub impl BeastStatsActions of BeastStatsActionTrait { fn generate_random_beast_stat(beast_id: u16, attribute_id: u16, min: u8, max: u8) -> u16 { let mut salt: u256 = poseidon_hash_span( array![beast_id.into(), attribute_id.into(), starknet::get_block_timestamp().into()] diff --git a/backend/dojo_examples/combat_game/src/store.cairo b/backend/dojo_examples/combat_game/src/store.cairo index e24c5db..dad22e9 100644 --- a/backend/dojo_examples/combat_game/src/store.cairo +++ b/backend/dojo_examples/combat_game/src/store.cairo @@ -4,7 +4,7 @@ use combat_game::{ helpers::{pseudo_random::PseudoRandom::generate_random_u8}, constants::SECONDS_PER_DAY, models::{ player::Player, beast::{Beast, BeastTrait}, skill, skill::{Skill, SkillTrait}, - beast_skill::BeastSkill, beast_stats::BeastStats, battle::Battle, + beast_skill::BeastSkill, beast_stats::{BeastStats, BeastStatsActionTrait}, battle::Battle, }, types::{ beast_type::BeastType, skill::SkillType, status_condition::StatusCondition, @@ -220,9 +220,7 @@ impl StoreImpl of StoreTrait { attack, defense, speed, - accuracy, - evasion, - status_condition: Into::::into(status_condition), + status_condition, last_timestamp: starknet::get_block_timestamp(), }; self.world.write_model(@beast_stats); @@ -351,16 +349,13 @@ impl StoreImpl of StoreTrait { if level_up_occurred { // Calculate remaining exp // TODO: ExperienceCalculatorTrait is not implemented - // beast.experience = ExperienceTrrait::remaining_exp_after_level_up(beast.level, beast.experience); + // beast.experience); + // beast.experience = ExperienceTrrait::remaining_exp_after_level_up(beast.level, beast.experience = 5; - // Increase level - beast.level += 1; - // Update beast stats - // TODO: `beast_stats.level_up()` is not implemented let mut beast_stats = self.read_beast_stats(beast_id); - // beast_stats.level_up(beast.beast_type); + beast_stats.level_up(beast.beast_type); self.write_beast_stats(beast_stats); } @@ -402,7 +397,6 @@ impl StoreImpl of StoreTrait { defender_beast_id: u16, skill_id: u256, ) -> (u16, bool, bool) { - // Read battle let mut battle = self.read_battle(battle_id); assert!( battle.status == BattleStatus::Active, @@ -410,7 +404,6 @@ impl StoreImpl of StoreTrait { battle.status, ); - // Verify attack is usable assert!( self.is_skill_usable(attacker_beast_id, skill_id), "Beast {} can't use skill {}", @@ -427,26 +420,16 @@ impl StoreImpl of StoreTrait { let mut defender_stats = self.read_beast_stats(defender_beast_id); // Check if attacker can attack - // assert(attacker_stats.can_attack(), 'Beast cannot attack'); // No implemented + assert(attacker_stats.can_attack(), 'Beast cannot attack'); // Calculate damage - // TODO: Define `attack_factor` right now is hard coded to 1 let skill = self.read_skill(skill_id); let (damage, is_favored, is_effective) = attacker_beast .attack(defender_beast.beast_type, skill.skill_type, 1); - - defender_stats - .current_hp = - if damage > defender_stats.current_hp { - Zero::zero() - } else { - defender_stats.current_hp - damage - }; - // battle.update_timestamp(); // No implemented + defender_stats.take_damage(damage); // Check if battle is over - if defender_stats.current_hp.is_zero() { - // End battle + if defender_stats.is_defeated() { battle.status = BattleStatus::Finished; battle.winner_id = starknet::get_caller_address();