diff --git a/src/simulator/crates/mc-turn/src/healing.rs b/src/simulator/crates/mc-turn/src/healing.rs index 1b466b8a..c626c5ac 100644 --- a/src/simulator/crates/mc-turn/src/healing.rs +++ b/src/simulator/crates/mc-turn/src/healing.rs @@ -65,7 +65,19 @@ const CITY_HEAL_PER_TURN: i32 = 20; /// This function does **not** call `step()` or any other phase — the parent /// wires the call at the correct slot in the turn sequence. pub fn process_healing_phase(state: &mut GameState) { - for player in &mut state.players { + // Snapshot every unit's (owner, tile) up front so city healing can skip + // BESIEGED cities — a city with an enemy unit on its tile is under siege and + // must NOT heal (else end-of-turn healing would undo the same turn's siege + // damage, e.g. heal a 30-hp city to 50 before a 45-dmg siege resolves). + // Mirrors the live game's siege-suppress intent via real-time occupancy. + let unit_tiles: Vec<(usize, i32, i32)> = state + .players + .iter() + .enumerate() + .flat_map(|(pi, p)| p.units.iter().map(move |u| (pi, u.col, u.row))) + .collect(); + + for (pi, player) in state.players.iter_mut().enumerate() { // Snapshot city positions for garrison detection; `player` is borrowed // mutably below for units, so we can't hold a reference to // `player.city_positions` at the same time. @@ -102,9 +114,17 @@ pub fn process_healing_phase(state: &mut GameState) { unit.hp = (unit.hp + heal_amount).min(unit.max_hp); } - // ── City healing ────────────────────────────────────────────────── - for city in &mut player.cities { + // ── City healing (siege-suppressed) ─────────────────────────────── + for (ci, city) in player.cities.iter_mut().enumerate() { if city.hp > 0 && city.hp < city.max_hp { + let besieged = player.city_positions.get(ci).copied().is_some_and(|(cc, cr)| { + unit_tiles + .iter() + .any(|&(opi, uc, ur)| opi != pi && uc == cc && ur == cr) + }); + if besieged { + continue; // under siege → no heal + } city.hp = (city.hp + CITY_HEAL_PER_TURN).min(city.max_hp); } } @@ -290,6 +310,42 @@ mod tests { ); } + /// A city with an enemy unit on its tile is under siege and must NOT heal + /// (so end-of-turn healing can't undo the same turn's siege damage). + #[test] + fn besieged_city_does_not_heal() { + let mut p0 = PlayerState { + city_positions: vec![(3, 3)], + ..PlayerState::default() + }; + let mut city = CityState::default(); + city.hp = 80; + city.max_hp = 200; + p0.cities.push(city); + + let mut p1 = PlayerState { + player_index: 1, + ..PlayerState::default() + }; + p1.units.push(MapUnit { + col: 3, + row: 3, + hp: 60, + max_hp: 60, + ..MapUnit::default() + }); + + let mut state = GameState::default(); + state.players.push(p0); + state.players.push(p1); + process_healing_phase(&mut state); + + assert_eq!( + state.players[0].cities[0].hp, 80, + "besieged city (enemy unit on tile) must not heal" + ); + } + /// A city at max HP does not overheal. #[test] fn full_hp_city_is_not_overhealed() {