feat(city-defense): city healing, tiered wall penalties, bombard damage function
- Cities heal 20 HP per turn (heal_per_turn in Rust, exposed via GDExtension, called from turn_processor after unit healing) - Wall penalty scales by tier: none=1.0, walls=0.75, castle=0.60 - Added city_bombard_damage() function: 15 * (city_str/attacker_str), [5,30] - Fixed "fortress"→"castle" references in combat_resolver.gd and combat_preview.gd - Fixed defender.city_hp→defender.hp already done in prior commit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0dc1fa5db8
commit
00aa2ef601
8 changed files with 69 additions and 14 deletions
|
|
@ -248,7 +248,7 @@ func _build_context_dict() -> Dictionary:
|
|||
var city_has_garrison: bool = false
|
||||
if _defender != null and not _defender is UnitScript and _lair_defender_dict.is_empty():
|
||||
city_hp = _defender.city_hp
|
||||
if _defender.has_building("fortress"):
|
||||
if _defender.has_building("castle"):
|
||||
city_wall_tier = 2
|
||||
elif _defender.has_building("walls"):
|
||||
city_wall_tier = 1
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ func end_turn() -> void:
|
|||
proc._process_golden_age(player, game_map)
|
||||
proc._process_mana(player, game_map)
|
||||
proc._process_healing(player)
|
||||
proc._process_city_healing(player)
|
||||
proc._process_improvements(player)
|
||||
proc._process_loot_decay()
|
||||
proc._process_spell_system(player)
|
||||
|
|
|
|||
|
|
@ -255,6 +255,12 @@ func add_production(production: float) -> void:
|
|||
_gd_city.call("add_production", production)
|
||||
|
||||
|
||||
## Heal the city by the standard per-turn amount (20 HP). Skips destroyed cities.
|
||||
func heal_per_turn() -> void:
|
||||
if _gd_city != null:
|
||||
_gd_city.call("heal_per_turn")
|
||||
|
||||
|
||||
## Get food surplus (food yield - consumption).
|
||||
func get_food_surplus(tile_yields_json: String) -> float:
|
||||
if _gd_city == null:
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ func _build_defender_dict(
|
|||
## Build a city as a defender dict.
|
||||
func _build_city_dict(city: RefCounted, all_units: Array) -> Dictionary:
|
||||
var wall_tier: int = 0
|
||||
if city.has_building("fortress"):
|
||||
if city.has_building("castle"):
|
||||
wall_tier = 2
|
||||
elif city.has_building("walls"):
|
||||
wall_tier = 1
|
||||
|
|
@ -282,7 +282,7 @@ func _build_context(
|
|||
|
||||
if defender is CityScript:
|
||||
city_hp = defender.hp
|
||||
if defender.has_building("fortress"):
|
||||
if defender.has_building("castle"):
|
||||
city_wall_tier = 2
|
||||
elif defender.has_building("walls"):
|
||||
city_wall_tier = 1
|
||||
|
|
|
|||
|
|
@ -211,6 +211,12 @@ func _process_growth(player: RefCounted) -> void: # Player
|
|||
c.process_growth(tile_json)
|
||||
|
||||
|
||||
func _process_city_healing(player: RefCounted) -> void:
|
||||
for city_ref: Variant in player.cities:
|
||||
if city_ref is CityScript:
|
||||
(city_ref as CityScript).heal_per_turn()
|
||||
|
||||
|
||||
func _process_healing(player: RefCounted) -> void: # Player
|
||||
var game_map: RefCounted = GameState.get_game_map() # GameMap
|
||||
if game_map == null:
|
||||
|
|
|
|||
|
|
@ -1214,6 +1214,12 @@ impl GdCity {
|
|||
fn heal(&mut self, amount: i64) {
|
||||
self.inner.heal(amount.max(0) as u32);
|
||||
}
|
||||
|
||||
/// Heal the city by the standard per-turn amount (20 HP). Skips destroyed cities.
|
||||
#[func]
|
||||
fn heal_per_turn(&mut self) {
|
||||
self.inner.heal_per_turn();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private helpers for GdCity ──────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -451,6 +451,15 @@ impl City {
|
|||
self.hp = (self.hp + amount).min(self.max_hp);
|
||||
}
|
||||
|
||||
/// Heal the city by the standard per-turn amount (20 HP).
|
||||
/// Skips destroyed cities (HP == 0).
|
||||
pub fn heal_per_turn(&mut self) {
|
||||
const HEAL_PER_TURN: u32 = 20;
|
||||
if self.hp > 0 && self.hp < self.max_hp {
|
||||
self.heal(HEAL_PER_TURN);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_destroyed(&self) -> bool {
|
||||
self.hp == 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,27 +11,39 @@ pub const DEFAULT_CITY_HP: i32 = 200;
|
|||
/// HP added per wall tier (3 tiers: ancient walls, medieval walls, renaissance walls).
|
||||
pub const WALL_HP_PER_TIER: i32 = 50;
|
||||
|
||||
/// Melee penalty when attacking a walled city (applied as negative modifier to attacker strength).
|
||||
const MELEE_WALL_PENALTY: f32 = 0.33;
|
||||
|
||||
/// Siege unit bonus vs city HP (applied as positive modifier to siege damage).
|
||||
const SIEGE_CITY_BONUS: f32 = 1.50;
|
||||
|
||||
/// City heals this much HP per turn (Civ5 standard).
|
||||
pub const CITY_HEAL_PER_TURN: i32 = 20;
|
||||
|
||||
/// Ranged attacks vs city reduce city HP, not garrison HP.
|
||||
/// This fraction of ranged damage goes to city HP when a garrison is present.
|
||||
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.
|
||||
/// `wall_tier` is 0 (no walls) to 3.
|
||||
/// Scales by tier: 0=1.0, 1=0.75 (walls), 2=0.60 (castle).
|
||||
pub fn melee_wall_penalty(wall_tier: i32) -> f32 {
|
||||
if wall_tier <= 0 {
|
||||
1.0
|
||||
} else {
|
||||
1.0 - MELEE_WALL_PENALTY
|
||||
match wall_tier {
|
||||
0 => 1.0,
|
||||
1 => 0.75,
|
||||
_ => 0.60, // tier 2+ (castle)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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].
|
||||
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
|
||||
}
|
||||
|
||||
/// Compute the bonus multiplier for siege units attacking a city.
|
||||
/// Returns a value > 1.0 applied to the siege unit's damage against city HP.
|
||||
pub fn siege_city_bonus() -> f32 {
|
||||
|
|
@ -76,10 +88,25 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn melee_penalty_with_walls() {
|
||||
fn melee_penalty_scales_by_tier() {
|
||||
assert!((melee_wall_penalty(0) - 1.0).abs() < 0.001);
|
||||
assert!((melee_wall_penalty(1) - 0.67).abs() < 0.001);
|
||||
assert!((melee_wall_penalty(3) - 0.67).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
|
||||
}
|
||||
|
||||
#[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);
|
||||
// Zero strength: 0
|
||||
assert_eq!(city_bombard_damage(0.0, 10.0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue