diff --git a/crates/game-service/tests/walk_to_tower.rs b/crates/game-service/tests/walk_to_tower.rs index 12abab7..8556e69 100644 --- a/crates/game-service/tests/walk_to_tower.rs +++ b/crates/game-service/tests/walk_to_tower.rs @@ -413,3 +413,68 @@ fn a_deployed_troop_counts_down_and_holds_still_until_ready() { .position(); assert_ne!(moved, spawn_pos, "a deployed troop moves"); } +#[test] +fn a_troop_in_range_damages_the_tower_with_the_client_hit_model() { + 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 (tower_id, tower_pos, start_hp) = mode + .battle + .objects + .objects + .iter() + .find_map(|e| match &e.body { + logic::battle::LogicObjectBody::Character(_) if e.owner_index() == 1 => { + let hp = e + .components + .get(2) + .and_then(|c| c.as_ref()) + .and_then(|c| match c { + logic::battle::LogicComponent::Hitpoint(h) => Some(h.hitpoints), + _ => None, + })?; + Some((e.global_id, e.position(), hp)) + } + _ => None, + }) + .expect("an enemy princess tower"); + let entries = builder.summon( + &LogicDataRef::spell("Knight"), + LogicVector2::new(tower_pos.0, tower_pos.1 - 800), + 0, + 0, + 100, + ); + for entry in entries { + mode.battle.objects.push(entry); + } + for tick in 1..200 { + mode.battle.tick(tick); + } + let hp = mode + .battle + .objects + .objects + .iter() + .find(|e| e.global_id == tower_id) + .and_then(|e| e.components.get(2)) + .and_then(|c| c.as_ref()) + .and_then(|c| match c { + logic::battle::LogicComponent::Hitpoint(h) => Some(h.hitpoints), + _ => None, + }) + .expect("tower still present"); + assert!(hp < start_hp, "the knight's attacks should reduce tower hp ({hp} !< {start_hp})"); +} diff --git a/crates/logic/src/battle/logic_simulation.rs b/crates/logic/src/battle/logic_simulation.rs index ac32ee9..51675c0 100644 --- a/crates/logic/src/battle/logic_simulation.rs +++ b/crates/logic/src/battle/logic_simulation.rs @@ -6,6 +6,7 @@ use crate::battle::logic_tilemap::SUBTILE_UNITS; use crate::data::LogicDataRef; pub const TICK_MILLISECONDS: i32 = 50; pub const CHARACTER_DEPLOY_TIME_COLUMN: &str = "DeployTime"; +pub const CHARACTER_SPECIAL_ATTACK_INTERVAL_COLUMN: &str = "SpecialAttackInterval"; pub const CHARACTER_STATE_MOVING: i32 = 1; pub const CHARACTER_STATE_DEPLOY: i32 = 5; pub const MANA_ACCUMULATOR_STEP: i32 = 5000; @@ -570,30 +571,67 @@ 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 - || self.objects.objects[index].is_deploying() - { - continue; - } + let alive = self.objects.objects[index].is_alive(); + let deploying = self.objects.objects[index].is_deploying(); + let special_interval = self.objects.objects[index] + .data + .data() + .map(|row| row.int(CHARACTER_SPECIAL_ATTACK_INTERVAL_COLUMN)) + .unwrap_or(0); + let hit_speed = stats.hit_speed; + let load_time = stats.load_time.max(0); + let range = stats.range; + let damage = stats.damage; let Some(component) = self.objects.objects[index].combat_mut() else { continue; }; + component.field_52 = (component.field_52 - TICK_MILLISECONDS).max(0); + component.field_60 = (component.field_60 - TICK_MILLISECONDS).max(0); + if !alive || deploying || hit_speed < 1 { + continue; + } let Some(target) = component.target_index else { component.hit_timer = 0; continue; }; - let reach = stats.range + radii.get(target).copied().unwrap_or(0); - if distance_squared(positions[index], positions[target]) > reach.saturating_mul(reach) { + let reach = range + radii.get(target).copied().unwrap_or(0); + if distance_squared(positions[index], positions[target]) + > reach.saturating_mul(reach) + { component.hit_timer = 0; continue; } - component.hit_timer += TICK_MILLISECONDS; - let period = stats.hit_speed.max(TICK_MILLISECONDS); - let wind_up = stats.load_time.max(0); - if component.hit_timer >= wind_up + period { - component.hit_timer = wind_up; - hits.push((target, stats.damage)); + let before = component.hit_timer; + let mut seeded = component.hit_timer; + if seeded == 0 && !component.flag_73 { + if load_time <= hit_speed { + seeded = load_time - component.field_52; + component.hit_timer = seeded; + component.field_52 = load_time; + } else if component.field_52 > hit_speed { + component.hit_timer = 0; + continue; + } else { + component.field_52 = 0; + seeded = 0; + } + } + if component.flag_73 { + component.hit_timer = seeded + hit_speed - seeded % hit_speed.max(1); + component.flag_73 = false; + } else { + component.hit_timer = seeded + TICK_MILLISECONDS; + } + if component.hit_timer / hit_speed.max(1) > before / hit_speed.max(1) { + component.field_52 = load_time; + if special_interval >= 2 { + component.field_64 = if component.field_64 == special_interval - 1 { + 0 + } else { + component.field_64 + 1 + }; + } + hits.push((target, damage)); } } for (target, amount) in hits {