diff --git a/crates/game-service/src/battle.rs b/crates/game-service/src/battle.rs index e651f4e..3e6c808 100644 --- a/crates/game-service/src/battle.rs +++ b/crates/game-service/src/battle.rs @@ -120,6 +120,11 @@ impl BattleBuilder { .data() .map(|row| row.int(CHARACTER_SPEED_COLUMN) > 0) .unwrap_or(false); + let deploy_time = data + .data() + .map(|row| row.int(logic::battle::CHARACTER_DEPLOY_TIME_COLUMN)) + .unwrap_or(0) + .max(0); let character = LogicCharacter { level_index, base: LogicGameObject { @@ -135,11 +140,14 @@ impl BattleBuilder { DIRECTION_BOTTOM }, ), - state: if moves { + state: if deploy_time >= 1 { + logic::battle::CHARACTER_STATE_DEPLOY + } else if moves { logic::battle::CHARACTER_STATE_MOVING } else { 0 }, + deploy_timer: deploy_time, ..LogicCharacter::default() }; let components = self.components_for(hitpoints, moves); diff --git a/crates/game-service/tests/walk_to_tower.rs b/crates/game-service/tests/walk_to_tower.rs index 7206f62..12abab7 100644 --- a/crates/game-service/tests/walk_to_tower.rs +++ b/crates/game-service/tests/walk_to_tower.rs @@ -342,3 +342,74 @@ fn a_dormant_king_clears_its_combat_bit_and_towers_do_not() { assert_eq!(b, c, "hashing the same idle state twice must agree"); let _ = a; } +#[test] +fn a_deployed_troop_counts_down_and_holds_still_until_ready() { + let root = assets(); + if let Ok(t) = LogicDataTables::load_from_dir(&root) { + LogicDataTables::install(Arc::new(t)); + } + let builder = BattleBuilder::new(&root); + let mut mode = builder + .build( + LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"), + LogicDataRef::None, + LogicDataRef::by_name(table::ARENAS, "Arena_T"), + vec![avatar(5), avatar(0)], + [None, None], + 1, + ) + .expect("battle"); + let entries = builder.summon( + &LogicDataRef::spell("Knight"), + LogicVector2::new(9000, 20000), + 0, + 0, + 100, + ); + let id = entries[0].global_id; + let deploy = entries[0] + .data + .data() + .map(|r| r.int("DeployTime")) + .unwrap_or(0); + assert!(deploy >= 1, "the knight has a deploy time"); + let spawn_pos = entries[0].position(); + for entry in entries { + mode.battle.objects.push(entry); + } + let timer_of = |mode: &logic::battle::LogicGameMode| { + mode.battle + .objects + .objects + .iter() + .find(|e| e.global_id == id) + .and_then(|e| match &e.body { + logic::battle::LogicObjectBody::Character(c) => Some(c.deploy_timer), + _ => None, + }) + }; + mode.battle.tick(1); + assert_eq!(timer_of(&mode), Some(deploy - 50), "deploy timer counts down"); + let here = mode + .battle + .objects + .objects + .iter() + .find(|e| e.global_id == id) + .unwrap() + .position(); + assert_eq!(here, spawn_pos, "a deploying troop holds still"); + for tick in 2..(deploy / 50 + 40) { + mode.battle.tick(tick); + } + assert_eq!(timer_of(&mode), Some(0), "deploy finished"); + let moved = mode + .battle + .objects + .objects + .iter() + .find(|e| e.global_id == id) + .unwrap() + .position(); + assert_ne!(moved, spawn_pos, "a deployed troop moves"); +} diff --git a/crates/logic/src/battle/logic_simulation.rs b/crates/logic/src/battle/logic_simulation.rs index 69e03b6..b5a7271 100644 --- a/crates/logic/src/battle/logic_simulation.rs +++ b/crates/logic/src/battle/logic_simulation.rs @@ -7,6 +7,7 @@ use crate::data::LogicDataRef; pub const TICK_MILLISECONDS: i32 = 50; pub const CHARACTER_DEPLOY_TIME_COLUMN: &str = "DeployTime"; pub const CHARACTER_STATE_MOVING: i32 = 1; +pub const CHARACTER_STATE_DEPLOY: i32 = 5; pub const MANA_ACCUMULATOR_STEP: i32 = 5000; pub const MANA_RATE_SCALE: i32 = 100; pub const GLOBAL_MAX_MANA: &str = "MAX_MANA"; @@ -122,6 +123,12 @@ impl LogicGameObjectEntry { pub fn owner_index(&self) -> i32 { self.body.base().owner_index } + pub fn is_deploying(&self) -> bool { + match &self.body { + LogicObjectBody::Character(character) => character.deploy_timer > 0, + LogicObjectBody::Summoner(summoner) => summoner.character.deploy_timer > 0, + } + } pub fn level_index(&self) -> i32 { self.body.level_index() } @@ -248,17 +255,23 @@ impl LogicBattle { } fn advance_deploy(&mut self) { for entry in self.objects.objects.iter_mut() { - let deploy_time = entry - .data - .data() - .map(|row| row.int(CHARACTER_DEPLOY_TIME_COLUMN)) - .unwrap_or(0); + let has_movement = matches!( + entry.components.get(crate::battle::logic_component::COMPONENT_MOVEMENT), + Some(Some(_)) + ); let LogicObjectBody::Character(character) = &mut entry.body else { continue; }; - if character.deploy_timer < deploy_time { + if character.deploy_timer > 0 { character.deploy_timer = - (character.deploy_timer + TICK_MILLISECONDS).min(deploy_time); + (character.deploy_timer - TICK_MILLISECONDS).max(0); + if character.deploy_timer == 0 { + character.state = if has_movement { + CHARACTER_STATE_MOVING + } else { + 0 + }; + } } } } @@ -278,7 +291,7 @@ impl LogicBattle { .collect(); for index in 0..self.objects.objects.len() { let (owner, position, stats, alive) = snapshot[index]; - if !alive || stats.range < 1 { + if !alive || stats.range < 1 || self.objects.objects[index].is_deploying() { continue; } let mut best: Option<(i32, usize)> = None; @@ -394,7 +407,10 @@ impl LogicBattle { .collect(); for index in 0..self.objects.objects.len() { let stats = self.objects.objects[index].stats(); - if stats.speed < 1 || !self.objects.objects[index].is_alive() { + if stats.speed < 1 + || !self.objects.objects[index].is_alive() + || self.objects.objects[index].is_deploying() + { continue; } let target = match self.objects.objects[index] @@ -539,7 +555,10 @@ impl LogicBattle { let mut hits: Vec<(usize, i32)> = Vec::new(); for index in 0..self.objects.objects.len() { let stats = self.objects.objects[index].stats(); - if !self.objects.objects[index].is_alive() || stats.hit_speed < 1 { + if !self.objects.objects[index].is_alive() + || stats.hit_speed < 1 + || self.objects.objects[index].is_deploying() + { continue; } let Some(component) = self.objects.objects[index].combat_mut() else { diff --git a/crates/logic/src/battle/mod.rs b/crates/logic/src/battle/mod.rs index 6c49329..52fb29d 100644 --- a/crates/logic/src/battle/mod.rs +++ b/crates/logic/src/battle/mod.rs @@ -34,7 +34,8 @@ pub use logic_game_object_manager::LogicGameObjectManager; pub use logic_game_object_ref::LogicGameObjectRef; pub use logic_pathfinder::find_path; pub use logic_simulation::{ - LogicCharacterStats, CHARACTER_DEPLOY_TIME_COLUMN, CHARACTER_STATE_MOVING, TICK_MILLISECONDS, + LogicCharacterStats, CHARACTER_DEPLOY_TIME_COLUMN, CHARACTER_STATE_DEPLOY, CHARACTER_STATE_MOVING, + TICK_MILLISECONDS, }; pub use logic_tilemap::{LogicTilemap, OBJECT_KING_TOWER, OBJECT_PRINCESS_TOWER, SUBTILE_UNITS}; pub use logic_time::LogicTime;