fix(@projects/@magic-civilization): 🐛 charge units their authored build_cost in try_spawn_unit (Rail 2)
City unit production spawned every unit at the flat `lair_combat_config.unit_spawn_cost`
(8) regardless of the unit's authored production cost — `dwarf_warrior` costs 40 in
public/resources/units/dwarf_warrior.json ("cost", loaded as UnitStats.build_cost).
Buildings/wonders already pay their real queue_cost in process_city_production, but
Queueable::Unit is routed to try_spawn_unit, which hardcoded the flat 8. Result:
units were ~5x too cheap, so a starter city (~6 prod/turn) spawned a warrior every
~1.3 turns instead of ~7 and the AI stack ballooned in the opening turns.
try_spawn_unit now charges `units_catalog[unit].build_cost` when the runtime catalog
knows it (>0), falling back to the flat unit_spawn_cost only for un-costed bench
fixtures and the no-queue auto-warrior. The data-loaded game (build_cost=40) now
prices units correctly; bench tests with build_cost=0 are unchanged (flat 8). This
restores Rail 2 — the JSON cost is canonical, not a hardcoded Rust constant.
Validated: mc-turn lib 248/0 (incl. all spawn + gold-gate tests), mc-player-api +
mc-turn 138/0, claude_vs_ai_full_game_transcript green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
12de49a16a
commit
9d2c72051f
1 changed files with 28 additions and 10 deletions
|
|
@ -1479,24 +1479,42 @@ impl TurnProcessor {
|
|||
// Cities with `Queueable::Item { .. }` or `Queueable::Wonder { .. }`
|
||||
// queue heads are NOT spawn candidates — those completions are
|
||||
// handled in `process_city_production`.
|
||||
let spawn_candidates: Vec<(usize, (i32, i32), String)> = {
|
||||
let spawn_candidates: Vec<(usize, (i32, i32), String, i32)> = {
|
||||
let catalog = &state.units_catalog;
|
||||
let player = &state.players[pi];
|
||||
player
|
||||
.cities
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, c)| {
|
||||
if c.production_stored < cost {
|
||||
return None;
|
||||
}
|
||||
let unit_id_for_spawn = match &c.queue {
|
||||
None => Some("dwarf_warrior".to_string()),
|
||||
Some(Queueable::Unit { unit_id }) => Some(unit_id.0.clone()),
|
||||
Some(Queueable::Item { .. }) | Some(Queueable::Wonder { .. }) => None,
|
||||
};
|
||||
unit_id_for_spawn.map(|uid| {
|
||||
(i, player.city_positions.get(i).copied().unwrap_or((0, 0)), uid)
|
||||
})
|
||||
}?;
|
||||
// Rail 2: charge the unit's AUTHORED production cost
|
||||
// (`UnitStats.build_cost`, JSON `"cost"`) when the runtime
|
||||
// catalog knows it. The flat `unit_spawn_cost` is a
|
||||
// lair-combat constant; it remains only as the fallback for
|
||||
// un-costed bench fixtures and the no-queue auto-warrior
|
||||
// (build_cost 0). Before this, every city unit spawned at the
|
||||
// flat 8 regardless of the data cost (40 for dwarf_warrior),
|
||||
// so units were ~5x too cheap — the AI's stack ballooned in
|
||||
// the opening turns.
|
||||
let unit_cost = catalog
|
||||
.get(&unit_id_for_spawn)
|
||||
.map(|s| s.build_cost as i32)
|
||||
.filter(|&bc| bc > 0)
|
||||
.unwrap_or(cost);
|
||||
if c.production_stored < unit_cost {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
i,
|
||||
player.city_positions.get(i).copied().unwrap_or((0, 0)),
|
||||
unit_id_for_spawn,
|
||||
unit_cost,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
|
@ -1527,7 +1545,7 @@ impl TurnProcessor {
|
|||
// resource, the gate fires here.
|
||||
let default_reqs: Vec<String> = Vec::new();
|
||||
|
||||
for (city_idx, pos, unit_kind) in spawn_candidates {
|
||||
for (city_idx, pos, unit_kind, unit_cost) in spawn_candidates {
|
||||
if !gold_gate_open {
|
||||
// Skip every candidate this turn — production carries over.
|
||||
// Not pushed into `strategic_gate_rejected` because that vec
|
||||
|
|
@ -1608,7 +1626,7 @@ impl TurnProcessor {
|
|||
let (u_hp, u_max_hp, u_atk, u_def) =
|
||||
self.resolve_spawn_combat(state, &unit_kind, None);
|
||||
let player = &mut state.players[pi];
|
||||
player.cities[city_idx].production_stored -= cost;
|
||||
player.cities[city_idx].production_stored -= unit_cost;
|
||||
// If the city was explicitly queueing a unit, the queue head
|
||||
// has now been fulfilled — clear it so subsequent turns can
|
||||
// re-queue. Empty-queue auto-warrior cities leave the queue
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue