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/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/models/skill.cairo b/backend/dojo_examples/combat_game/src/models/skill.cairo index c4a1fc6..68fd15f 100644 --- a/backend/dojo_examples/combat_game/src/models/skill.cairo +++ b/backend/dojo_examples/combat_game/src/models/skill.cairo @@ -14,6 +14,20 @@ 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 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] pub struct Skill { diff --git a/backend/dojo_examples/combat_game/src/store.cairo b/backend/dojo_examples/combat_game/src/store.cairo index 8b13789..dad22e9 100644 --- a/backend/dojo_examples/combat_game/src/store.cairo +++ b/backend/dojo_examples/combat_game/src/store.cairo @@ -1 +1,449 @@ +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, BeastTrait}, skill, skill::{Skill, SkillTrait}, + beast_skill::BeastSkill, beast_stats::{BeastStats, BeastStatsActionTrait}, battle::Battle, + }, + types::{ + beast_type::BeastType, skill::SkillType, status_condition::StatusCondition, + battle_status::BattleStatus, + }, +}; +use starknet::ContractAddress; + +#[derive(Drop, Copy)] +struct Store { + world: WorldStorage, +} + +#[generate_trait] +impl StoreImpl of StoreTrait { + fn new(world: WorldStorage) -> Store { + Store { world: world } + } + + // [ Initialization methods ] + 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); }, + } + } + + 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::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), + 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(), + 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 + } + + 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 + } + + 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: beast_type, + }; + self.world.write_model(@beast); + beast + } + + 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, + status_condition, + last_timestamp: starknet::get_block_timestamp(), + }; + self.world.write_model(@beast_stats); + beast_stats + } + + fn new_battle( + ref self: Store, + battle_id: u256, + player1: ContractAddress, + player2: ContractAddress, + battle_type: u8, + ) -> Battle { + let players = array![player1, player2]; + let battle = Battle { + 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(), + ), + status: BattleStatus::Waiting, + winner_id: Zero::zero(), + battle_type: battle_type, + }; + self.world.write_model(@battle); + battle + } + + 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(), + ), + status: 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)) + } + + fn read_skill(self: @Store, skill_id: u256) -> 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 { + self.world.read_model((starknet::get_caller_address(), beast_id)) + } + + 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) + } + + 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) { + 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, 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; + + // 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); + // beast.experience = ExperienceTrrait::remaining_exp_after_level_up(beast.level, + beast.experience = 5; + + // Update beast stats + 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) { + let mut battle = self.read_battle(battle_id); + assert!( + battle.status == BattleStatus::Active, + "Battle should be in `Active` status (current `{}`)", + battle.status, + ); + + 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'); + + // Calculate damage + 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.take_damage(damage); + + // Check if battle is over + if defender_stats.is_defeated() { + battle.status = BattleStatus::Finished; + battle.winner_id = starknet::get_caller_address(); + + // 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 {