diff --git a/crates/game-service/src/battle.rs b/crates/game-service/src/battle.rs index d4edf42..e651f4e 100644 --- a/crates/game-service/src/battle.rs +++ b/crates/game-service/src/battle.rs @@ -223,12 +223,16 @@ impl BattleBuilder { } else { LogicObjectBody::Character(Box::new(character)) }; - LogicGameObjectEntry::new( + let mut entry = LogicGameObjectEntry::new( data, LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, instance), body, self.components_for(hitpoints, false), - ) + ); + if summoner { + entry.body.base_mut().component_mask &= !(1 << logic::battle::COMPONENT_COMBAT); + } + entry } pub fn build( &self, diff --git a/crates/game-service/src/battle_session.rs b/crates/game-service/src/battle_session.rs index 99ecab5..86a3e35 100644 --- a/crates/game-service/src/battle_session.rs +++ b/crates/game-service/src/battle_session.rs @@ -9,6 +9,7 @@ pub fn snapshot_interval_ticks() -> i32 { .unwrap_or(SNAPSHOT_INTERVAL_TICKS) } pub const MAX_CATCH_UP_TICKS: i32 = 40; +pub const CHECKSUM_HISTORY: usize = 256; pub const BOT_DEPLOY_AHEAD: i32 = 2000; use std::time::Instant; use logic::battle::{ @@ -32,6 +33,7 @@ pub struct BattleSession { next_snapshot: i32, next_bot_play: i32, pub pushed_snapshots: u32, + recent_checksums: std::collections::VecDeque<(i32, i32)>, next_instance: i32, announced: bool, pending_bot_play: Option<(LogicDataRef, LogicVector2, i32)>, @@ -61,6 +63,7 @@ impl BattleSession { next_snapshot: snapshot_interval_ticks(), next_bot_play: crate::bot::BOT_OPENING_DELAY_SECONDS * BATTLE_TICKS_PER_SECOND, pushed_snapshots: 0, + recent_checksums: std::collections::VecDeque::with_capacity(CHECKSUM_HISTORY), next_instance: first_instance, announced: false, pending_bot_play: None, @@ -235,6 +238,12 @@ impl BattleSession { self.mode.time.tick = self.tick; self.release_queued(self.tick); self.mode.battle.tick(self.tick); + if let Ok(checksum) = self.mode.calculate_checksum() { + if self.recent_checksums.len() >= CHECKSUM_HISTORY { + self.recent_checksums.pop_front(); + } + self.recent_checksums.push_back((self.tick, checksum)); + } if self.pushes_snapshots() && self.tick >= self.next_snapshot { self.next_snapshot = self.tick + snapshot_interval_ticks(); if let Some(message) = self.snapshot_message() { @@ -292,6 +301,13 @@ impl BattleSession { pub fn checksum(&self) -> Option { self.mode.calculate_checksum().ok() } + pub fn checksum_at(&self, tick: i32) -> Option { + self.recent_checksums + .iter() + .rev() + .find(|(at, _)| *at == tick) + .map(|(_, checksum)| *checksum) + } pub fn towers_standing(&self) -> (usize, usize) { ( self.mode.battle.leader_towers[0].len(), @@ -410,7 +426,7 @@ impl BattleRegistry { stars: session.stars(), objects: session.object_count(), towers: session.towers_standing(), - checksum: session.checksum(), + checksum: session.checksum_at(tick).or_else(|| session.checksum()), mana: session.mana(), pushed: session.pushed_snapshots, }) diff --git a/crates/game-service/tests/walk_to_tower.rs b/crates/game-service/tests/walk_to_tower.rs index 1afb13c..7206f62 100644 --- a/crates/game-service/tests/walk_to_tower.rs +++ b/crates/game-service/tests/walk_to_tower.rs @@ -296,3 +296,49 @@ fn a_finished_battle_stops_ticking() { "a finished battle carried on ticking" ); } +#[test] +fn a_dormant_king_clears_its_combat_bit_and_towers_do_not() { + 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"); + for tick in 1..80 { + mode.battle.tick(tick); + } + let mut kings = 0; + let mut princesses = 0; + for entry in mode.battle.objects.objects.iter() { + let mask = entry.body.base().component_mask; + match &entry.body { + logic::battle::LogicObjectBody::Summoner(_) => { + assert_eq!(mask, 12, "a dormant king should clear its combat bit"); + kings += 1; + } + logic::battle::LogicObjectBody::Character(_) => { + assert_eq!(mask, 13, "a princess tower keeps its combat bit"); + princesses += 1; + } + } + } + assert_eq!(kings, 2); + assert_eq!(princesses, 4); + let a = mode.calculate_checksum().unwrap(); + mode.time.tick = 200; + mode.battle.tick(200); + let b = mode.calculate_checksum().unwrap(); + mode.time.tick = 200; + let c = mode.calculate_checksum().unwrap(); + assert_eq!(b, c, "hashing the same idle state twice must agree"); + let _ = a; +} diff --git a/crates/logic/src/battle/logic_simulation.rs b/crates/logic/src/battle/logic_simulation.rs index 3607106..69e03b6 100644 --- a/crates/logic/src/battle/logic_simulation.rs +++ b/crates/logic/src/battle/logic_simulation.rs @@ -13,6 +13,9 @@ pub const GLOBAL_MAX_MANA: &str = "MAX_MANA"; pub const GLOBAL_MANA_REGEN: &str = "MANA_REGEN_MS"; pub const GLOBAL_MANA_REGEN_END: &str = "MANA_REGEN_MS_END"; pub const GLOBAL_MANA_SPEED_UP_SECONDS: &str = "MANA_SPEED_UP_WHEN_REMAINING_SECONDS"; +pub const GLOBAL_KING_ACTIVATE_TIME_MS: &str = "KING_ACTIVATE_TIME_MS"; +pub const KING_ACTIVATION_STEP: i32 = 50; +pub const PRINCESS_TOWERS_TOTAL: i32 = 4; pub const COLUMN_SPEED: &str = "Speed"; pub const COLUMN_SIGHT_RANGE: &str = "SightRange"; pub const COLUMN_RANGE: &str = "Range"; @@ -196,6 +199,7 @@ impl LogicBattle { } } pub fn tick(&mut self, tick: i32) { + self.activate_summoners(); self.advance_deploy(); self.regenerate_mana(tick); self.retarget(); @@ -204,6 +208,44 @@ impl LogicBattle { self.resolve_attacks(); self.remove_dead(); } + fn activate_summoners(&mut self) { + let activate_ms = crate::LogicGlobals::number(GLOBAL_KING_ACTIVATE_TIME_MS).max(0); + let total = self + .tilemap + .as_ref() + .map(|map| map.princess_towers().len() as i32) + .filter(|count| *count > 0) + .unwrap_or(PRINCESS_TOWERS_TOTAL); + let standing = [ + self.leader_towers.first().map(Vec::len).unwrap_or(0) as i32, + self.leader_towers.get(1).map(Vec::len).unwrap_or(0) as i32, + ]; + for entry in self.objects.objects.iter_mut() { + let full_health = match entry.components.get(COMPONENT_HITPOINT) { + Some(Some(LogicComponent::Hitpoint(hp))) => hp.hitpoints >= hp.base_hitpoints, + _ => true, + }; + let owner = entry.owner_index().clamp(0, 1) as usize; + let LogicObjectBody::Summoner(summoner) = &mut entry.body else { + continue; + }; + let princess_lost = standing[owner] < total / 2; + if summoner.field_256 == 0 { + if !full_health || princess_lost { + summoner.field_256 = KING_ACTIVATION_STEP; + } + } else if summoner.field_256 <= activate_ms { + summoner.field_256 += KING_ACTIVATION_STEP; + } + let active = summoner.field_256 > activate_ms; + let mask = &mut summoner.character.base.component_mask; + if active { + *mask |= 1 << COMPONENT_COMBAT; + } else { + *mask &= !(1 << COMPONENT_COMBAT); + } + } + } fn advance_deploy(&mut self) { for entry in self.objects.objects.iter_mut() { let deploy_time = entry