refactor(combat): ♻️ Streamline CombatResolver and Siege logic with utility functions in combat_utils.gd and resolver.rs
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
4fa33313e2
commit
2ba740d0e6
4 changed files with 65 additions and 23 deletions
|
|
@ -255,8 +255,8 @@ func _build_city_dict(city: RefCounted, all_units: Array) -> Dictionary:
|
|||
support += 1
|
||||
|
||||
return {
|
||||
"hp": city.city_hp,
|
||||
"max_hp": city.city_hp,
|
||||
"hp": city.hp,
|
||||
"max_hp": city.max_hp,
|
||||
"attack": 0,
|
||||
"defense": 0,
|
||||
"ranged_attack": 0,
|
||||
|
|
@ -339,7 +339,7 @@ func _compute_city_bombard(city: RefCounted, attacker: RefCounted) -> int:
|
|||
return 0
|
||||
var atk_str: float = float(attacker.attack) if attacker is UnitScript else 10.0
|
||||
var ratio: float = city_str / maxf(atk_str, 1.0)
|
||||
return clampi(roundi(15.0 * ratio), 5, 30)
|
||||
return clampi(roundi(10.0 * ratio), 3, 20)
|
||||
|
||||
|
||||
func _get_terrain_defense(unit: RefCounted, game_map: RefCounted) -> int:
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ static func handle_unit_death(unit: RefCounted, killer: RefCounted, all_units: A
|
|||
if tile != null:
|
||||
ItemSystemScript.drop_all_loot(unit, tile)
|
||||
|
||||
if unit.owner == -1 and killer != null and killer is UnitScript and killer.owner >= 0:
|
||||
_roll_wild_creature_loot(unit, killer)
|
||||
|
||||
all_units.erase(unit)
|
||||
|
||||
if unit.owner >= 0 and unit.owner < GameState.players.size():
|
||||
|
|
@ -135,6 +138,17 @@ static func capture_city(
|
|||
EventBus.player_eliminated.emit(old_owner)
|
||||
|
||||
|
||||
static func _roll_wild_creature_loot(victim: RefCounted, killer: RefCounted) -> void:
|
||||
if killer.owner < 0 or killer.owner >= GameState.players.size():
|
||||
return
|
||||
var killer_player: RefCounted = GameState.players[killer.owner]
|
||||
var creature_type: String = victim.type_id if victim.type_id != "" else victim.unit_id
|
||||
var turn_seed: int = GameState.game_rng.seed ^ GameState.turn_number
|
||||
var killer_id: int = hash(killer.id)
|
||||
var victim_id: int = hash(victim.id)
|
||||
ItemSystemScript.roll_fauna_drops(creature_type, killer_player, turn_seed, killer_id, victim_id)
|
||||
|
||||
|
||||
## Destroy the High Archon of a player (capital capture penalty).
|
||||
static func _destroy_high_archon(player: RefCounted, all_units: Array) -> void:
|
||||
for unit: RefCounted in player.units:
|
||||
|
|
|
|||
|
|
@ -790,6 +790,34 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn melee_can_break_walled_city_over_sustained_assault() {
|
||||
// Simulate repeated melee hits on a tier-1 walled city. The combined damage
|
||||
// from multiple warriors must outpace 10 HP/turn regen so capture is reachable.
|
||||
let mut city_hp = siege::city_total_hp(1); // 250
|
||||
let mut hits = 0;
|
||||
let max_hits = 50;
|
||||
while city_hp > 0 && hits < max_hits {
|
||||
let params = CombatParams {
|
||||
attacker: warrior_stats(),
|
||||
defender: warrior_stats(),
|
||||
combat_type: CombatType::Melee,
|
||||
city_hp: Some(city_hp),
|
||||
city_wall_tier: 1,
|
||||
city_has_garrison: true,
|
||||
..Default::default()
|
||||
};
|
||||
let result = CombatResolver::resolve(¶ms);
|
||||
city_hp = result.city_hp_remaining;
|
||||
hits += 1;
|
||||
}
|
||||
assert_eq!(
|
||||
city_hp, 0,
|
||||
"sustained melee assault should reduce walled city to 0 HP within {} hits",
|
||||
max_hits
|
||||
);
|
||||
}
|
||||
|
||||
// ── Damaged attacker deals less damage ────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ pub const BASE_CITY_HP: i32 = 200;
|
|||
pub const WALL_HP_PER_TIER: i32 = 50;
|
||||
|
||||
/// Siege unit bonus vs city HP (applied as positive modifier to siege damage).
|
||||
const SIEGE_CITY_BONUS: f32 = 1.50;
|
||||
const SIEGE_CITY_BONUS: f32 = 2.00;
|
||||
|
||||
/// City heals this much HP per turn (Civ5 standard).
|
||||
pub const CITY_HEAL_PER_TURN: i32 = 20;
|
||||
/// City heals this much HP per turn.
|
||||
pub const CITY_HEAL_PER_TURN: i32 = 10;
|
||||
|
||||
/// Ranged attacks vs city reduce city HP, not garrison HP.
|
||||
/// This fraction of ranged damage goes to city HP when a garrison is present.
|
||||
|
|
@ -23,25 +23,25 @@ const RANGED_CITY_HP_FRACTION: f32 = 0.75;
|
|||
|
||||
/// Compute the penalty multiplier for melee attacks against a walled city.
|
||||
/// Returns a value < 1.0 that the attacker's effective strength is multiplied by.
|
||||
/// Scales by tier: 0=1.0, 1=0.75 (walls), 2=0.60 (castle).
|
||||
/// Scales by tier: 0=1.0, 1=0.85 (walls), 2=0.75 (castle).
|
||||
pub fn melee_wall_penalty(wall_tier: i32) -> f32 {
|
||||
match wall_tier {
|
||||
0 => 1.0,
|
||||
1 => 0.75,
|
||||
_ => 0.60, // tier 2+ (castle)
|
||||
1 => 0.85,
|
||||
_ => 0.75,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute city bombard retaliation damage against a melee attacker.
|
||||
/// `city_strength` = population * 3 + building bonuses.
|
||||
/// `attacker_strength` = attacker's effective combat strength.
|
||||
/// Uses a simplified damage formula: 15 * (city / attacker) clamped to [5, 30].
|
||||
/// Uses a simplified damage formula: 10 * (city / attacker) clamped to [3, 20].
|
||||
pub fn city_bombard_damage(city_strength: f32, attacker_strength: f32) -> i32 {
|
||||
if city_strength <= 0.0 || attacker_strength <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let ratio = city_strength / attacker_strength.max(1.0);
|
||||
(15.0 * ratio).round().clamp(5.0, 30.0) as i32
|
||||
(10.0 * ratio).round().clamp(3.0, 20.0) as i32
|
||||
}
|
||||
|
||||
/// Compute the bonus multiplier for siege units attacking a city.
|
||||
|
|
@ -90,28 +90,28 @@ mod tests {
|
|||
#[test]
|
||||
fn melee_penalty_scales_by_tier() {
|
||||
assert!((melee_wall_penalty(0) - 1.0).abs() < 0.001);
|
||||
assert!((melee_wall_penalty(1) - 0.75).abs() < 0.001);
|
||||
assert!((melee_wall_penalty(2) - 0.60).abs() < 0.001);
|
||||
assert!((melee_wall_penalty(3) - 0.60).abs() < 0.001); // caps at tier 2
|
||||
assert!((melee_wall_penalty(1) - 0.85).abs() < 0.001);
|
||||
assert!((melee_wall_penalty(2) - 0.75).abs() < 0.001);
|
||||
assert!((melee_wall_penalty(3) - 0.75).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn city_bombard_damage_values() {
|
||||
// Equal strength: 15 damage
|
||||
assert_eq!(city_bombard_damage(10.0, 10.0), 15);
|
||||
// City twice as strong: 30 (capped)
|
||||
assert_eq!(city_bombard_damage(20.0, 10.0), 30);
|
||||
// City half strength: 8
|
||||
assert_eq!(city_bombard_damage(5.0, 10.0), 8);
|
||||
// Very weak city: clamped to 5
|
||||
assert_eq!(city_bombard_damage(1.0, 10.0), 5);
|
||||
// Equal strength: 10 damage
|
||||
assert_eq!(city_bombard_damage(10.0, 10.0), 10);
|
||||
// City twice as strong: 20 (capped)
|
||||
assert_eq!(city_bombard_damage(20.0, 10.0), 20);
|
||||
// City half strength: 5
|
||||
assert_eq!(city_bombard_damage(5.0, 10.0), 5);
|
||||
// Very weak city: clamped to 3
|
||||
assert_eq!(city_bombard_damage(1.0, 10.0), 3);
|
||||
// Zero strength: 0
|
||||
assert_eq!(city_bombard_damage(0.0, 10.0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn siege_bonus() {
|
||||
assert!((siege_city_bonus() - 1.50).abs() < 0.001);
|
||||
assert!((siege_city_bonus() - 2.00).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue