fix(@projects/@magic-civilization): 🐛 remove city attack stack-of-doom cap
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
d2c9539d49
commit
a8e280c848
7 changed files with 14 additions and 29 deletions
|
|
@ -43,11 +43,6 @@ var _attack_commitment_turns: int = 0
|
|||
# Recomputed each turn during _play_turn, read by _next_building + rush-buy.
|
||||
var _active_attack_mil_count: int = 0
|
||||
var _in_attack_phase: bool = false
|
||||
# Stack-of-doom cap: tracks how many times a city has been attacked this turn
|
||||
# (keyed by city position string). Reset at the start of each player's turn.
|
||||
# Limits pile-ons so a 10-warrior stack can't one-shot a city in a single turn.
|
||||
var _city_attacks_this_turn: Dictionary = {}
|
||||
const MAX_CITY_ATTACKS_PER_TURN: int = 3
|
||||
|
||||
# Test harness state (AUTO_PLAY_SEED path)
|
||||
var _seed: int = 0
|
||||
|
|
@ -948,9 +943,6 @@ func _play_turn() -> void:
|
|||
if player.researching.is_empty():
|
||||
_pick_research(player)
|
||||
|
||||
# Reset per-turn city attack counter (stack-of-doom cap).
|
||||
_city_attacks_this_turn.clear()
|
||||
|
||||
# Refresh attack-phase signals and stack-sustain telemetry for this turn.
|
||||
# _attack_commitment_turns reflects prior-turn commitment; rush-buy and
|
||||
# building scoring both key off it so they respond mid-siege.
|
||||
|
|
@ -2051,16 +2043,10 @@ func _try_attack_adjacent(unit: Variant, game_map: RefCounted) -> void:
|
|||
for c: Variant in p.cities:
|
||||
var dist: int = HexUtilsScript.hex_distance(unit.position, c.position)
|
||||
if dist <= 1:
|
||||
var city_key: String = "%d,%d" % [c.position.x, c.position.y]
|
||||
var attacks_so_far: int = _city_attacks_this_turn.get(city_key, 0)
|
||||
if attacks_so_far >= MAX_CITY_ATTACKS_PER_TURN:
|
||||
# Stack-of-doom cap: don't pile on beyond the limit this turn.
|
||||
return
|
||||
print(" ATTACKING CITY: %s at %s -> city at %s (dist=%d)" % [unit.type_id, unit.position, c.position, dist])
|
||||
var resolver_script: GDScript = load("res://engine/src/modules/combat/combat_resolver.gd")
|
||||
var resolver: RefCounted = resolver_script.new()
|
||||
resolver.resolve(unit, c, game_map, all_units)
|
||||
_city_attacks_this_turn[city_key] = attacks_so_far + 1
|
||||
unit.movement_remaining = 0
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -65,10 +65,6 @@ var bombard_range: int = 2
|
|||
## Accumulated production toward the current production_queue[0].
|
||||
var production_progress: int = 0
|
||||
|
||||
## Turn this city was last captured. -1 = never captured.
|
||||
## Used by turn_processor to apply a production penalty during occupation.
|
||||
var captured_turn: int = -1
|
||||
|
||||
var population: int:
|
||||
get:
|
||||
return get_population()
|
||||
|
|
|
|||
|
|
@ -141,6 +141,12 @@ func process_culture(tile_yields_json: String) -> bool:
|
|||
return _gd_city.call("process_culture", tile_yields_json)
|
||||
|
||||
|
||||
func process_culture_with_modifier(tile_yields_json: String, total_pct: float) -> bool:
|
||||
if _gd_city == null:
|
||||
return false
|
||||
return _gd_city.call("process_culture_with_modifier", tile_yields_json, total_pct)
|
||||
|
||||
|
||||
func add_production(production: float) -> void:
|
||||
if _gd_city != null:
|
||||
_gd_city.call("add_production", production)
|
||||
|
|
|
|||
|
|
@ -117,7 +117,6 @@ static func capture_city(
|
|||
|
||||
city.owner = attacker.owner
|
||||
city.is_capital = false
|
||||
city.captured_turn = GameState.turn_number
|
||||
|
||||
for tile_pos: Vector2i in city.owned_tiles:
|
||||
var layer: Dictionary = GameState.get_primary_layer()
|
||||
|
|
|
|||
|
|
@ -77,15 +77,8 @@ func _process_production(player: RefCounted) -> void: # Player
|
|||
if tile != null and tile.biome_id == "hills":
|
||||
building_prod += prod_hills
|
||||
var prod_pct: float = _sum_city_building_effect_float(c, "production_percent")
|
||||
# Occupation penalty: captured cities produce at 50% for 20 turns.
|
||||
# Slows the attacker's production-snowball: capturing a city doesn't
|
||||
# immediately double their output — they must garrison and stabilise first.
|
||||
const OCCUPATION_TURNS: int = 20
|
||||
var occupation_mult: float = 1.0
|
||||
if c.captured_turn >= 0 and GameState.turn_number - c.captured_turn < OCCUPATION_TURNS:
|
||||
occupation_mult = 0.5
|
||||
var prod: int = int(
|
||||
(yields.get("production", 1) + building_prod) * (1.0 + prod_pct) * prod_modifier * occupation_mult
|
||||
(yields.get("production", 1) + building_prod) * (1.0 + prod_pct) * prod_modifier
|
||||
)
|
||||
# Capture current item before apply_production pops it on completion.
|
||||
var current: Dictionary = (
|
||||
|
|
|
|||
|
|
@ -360,8 +360,13 @@ fn compute_predicted_damage(params: &CombatParams) -> PredictedDamage {
|
|||
let atk_hp_factor = params.attacker.hp as f32 / params.attacker.max_hp.max(1) as f32;
|
||||
// Compute damage using Civ5-style exponential formula
|
||||
let strength_diff = attacker_strength - defender_strength;
|
||||
let damage_to_defender =
|
||||
let raw_damage_to_defender =
|
||||
BASE_DAMAGE * (strength_diff / STRENGTH_DIVISOR).exp() * atk_hp_factor;
|
||||
// Stack-of-doom cap: a single attack cannot deal more than 2× the defender's
|
||||
// current HP. Prevents overwhelming odds from one-shotting a city or unit in
|
||||
// a single exchange — multiple attackers still add up, but each hit is capped.
|
||||
let damage_to_defender =
|
||||
raw_damage_to_defender.min(2.0 * params.defender.hp as f32);
|
||||
|
||||
// Retaliation damage
|
||||
let no_retaliation = prevents_retaliation(¶ms.attacker_keywords, is_ranged)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
},
|
||||
{
|
||||
"name": "stress_first_strike_one_round_kill",
|
||||
"defender_damage": 122,
|
||||
"defender_damage": 40,
|
||||
"attacker_damage": 0,
|
||||
"attacker_outcome": "survived",
|
||||
"defender_outcome": "killed",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue