feat(@projects): add unit action variants and combat hooks

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-02 20:06:39 -04:00
parent 4c7d6a8ce5
commit 950aad39cd
20 changed files with 257 additions and 46 deletions

View file

@ -89,6 +89,82 @@ const STATUS: Readonly<Record<string, Status>> = {
"alchemist-workshop:produce": "shipped",
"barracks:train": "shipped",
// ── Infantry · Line (p2-53f) ────────────────────────────────────
"defender:shieldwall": "shipped", // p2-53f: ShieldWall/UnshieldWall variants + handler + ranged-defence hook
"defender:brace": "shipped", // p2-53f: Brace/Unbrace + first-strike resolver hook
"defender:shove": "shipped", // p2-53f: handle_shove, push displacement + formation-clear
// ── Infantry · Shock (p2-53f) ───────────────────────────────────
"berserker:rage": "shipped", // p2-53f: Rage + rage_turns countdown + rage_gates_fortify/sentry
"berserker:cleave": "shipped", // p2-53f: Cleave AoE 50% secondary in CombatResult
"berserker:warcry": "shipped", // p2-53f: WarCry adjacency-scan + war_cry_debuff_turns processor tick
// ── Ranged (p2-53g) ─────────────────────────────────────────────
"archer:aimed-shot": "shipped", // p2-53g: AimedShot + aimed_shot_pending + 50% defence-reduction hook
"archer:fire-arrows": "stubbed-rust", // p2-53g: FireArrows toggle exists; ignition deferred (mc-ecology fire system missing)
"archer:volley": "stubbed-rust", // p2-53g: Volley ActionKind exists; AoE queue-drain returns WrongTerrain — deferred
// ── Cavalry (p2-53h) ────────────────────────────────────────────
"cavalry:pursue": "shipped", // p2-53h: Pursue follow-through in CombatResult.pursue_advance_to + bridge
"cavalry:charge": "stubbed-rust", // p2-53h: Charge ActionKind exists; 2-hex straight-line queue-drain deferred
"cavalry:wheel": "stubbed-rust", // p2-53h: Wheel ActionKind exists; facing_edge field missing — deferred
// ── Support · Engineer (p2-53i) ─────────────────────────────────
"engineer:build-bridge": "stubbed-rust", // p2-53i: ActionKind + MultiTurnAction; bridge target-pick UI pending
"engineer:sap": "stubbed-rust", // p2-53i: ActionKind + MultiTurnAction; bridge target-pick UI pending
"engineer:breach": "stubbed-rust", // p2-53i: ActionKind + MultiTurnAction; bridge target-pick UI pending
"engineer:fortify-hex": "stubbed-rust", // p2-53i: ActionKind + MultiTurnAction; bridge target-pick UI pending
"engineer:demolish": "stubbed-rust", // p2-53i: ActionKind + MultiTurnAction; bridge target-pick UI pending
// ── Support · Pioneer (p2-53i) ──────────────────────────────────
"pioneer:clear-terrain": "shipped", // p2-53i: ClearTerrain handler + biome-flag mutation + lump-prod to city
"pioneer:repair": "shipped", // p2-53i: RepairImprovement handler (half-cost restore)
// ── Support · Medic (p2-53i) ────────────────────────────────────
"battle-medic:triage": "shipped", // p2-53i: Triage handler + replaces-attack gate
"battle-medic:field-aura": "shipped", // p2-53i: FieldAura passive regen; composes with Fortify
"battle-medic:stabilise": "shipped", // p2-53i: Stabilise once-per-battle → 1 HP floor
"battle-medic:remove-status": "shipped", // p2-53i: RemoveStatus clears StatusEffect vec on adjacent unit
// ── Support · Scout (p2-53i) ────────────────────────────────────
"scout:stealth": "shipped", // p2-53i: Stealth + is_stealthed; Sentry wake-predicate ignores stealthed
"scout:lookout": "shipped", // p2-53i: Lookout skip-turn + +2 vision + reveal stack
"scout:ambush": "shipped", // p2-53i: Ambush on-enter trigger + free-attack-at-50%
"scout:trail-mark": "shipped", // p2-53i: TrailMark tag + 5-turn fog-piercing visibility
// ── Buildings · Barracks (p2-53d) ───────────────────────────────
"barracks:drill": "stubbed-rust", // p2-53d: handler wired; building_panel GDScript signal in-flight (ui-wiring2)
"barracks:auto-promote": "stubbed-rust", // p2-53d: handler wired; building_panel GDScript signal in-flight
// ── Buildings · Watchtower (p2-53d) ─────────────────────────────
"watchtower:ranged-fire": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"watchtower:alarm": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"watchtower:fire-arrows": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
// ── Buildings · Wall (p2-53d) ───────────────────────────────────
"city-wall:repair": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"city-wall:murder-holes": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"city-wall:gate": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
// ── Buildings · City Centre (p2-53d) ────────────────────────────
"city-center:raze": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"city-center:annex": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
// ── Buildings · Workshop (p2-53d) ───────────────────────────────
"alchemist-workshop:stockpile": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"alchemist-workshop:overdrive": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"alchemist-workshop:research-aid": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
// ── Buildings · Ancestor Hall (p2-53d) ──────────────────────────
"ancestor-hall:invoke": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"ancestor-hall:inscribe": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
// ── Buildings · Outpost (p2-53d) ────────────────────────────────
"outpost:pack": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"outpost:supply": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"outpost:claim-tile": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
"outpost:beacon": "stubbed-rust", // p2-53d: handler wired; building_panel signal in-flight
// ── Out-of-Game-1 archetypes (caravan, aerial, heroic) ──────────
"caravan:route": "design-only-g2", // Caravan trade routes — g6 oos / g2 design
"caravan:deliver": "design-only-g2",

View file

@ -16,9 +16,9 @@
|---|---|---|---|---|---|---|---|
| **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 |
| **P1** | 43 | 1 | 7 | 0 | 14 | 1 | 66 |
| **P2** | 43 | 1 | 7 | 1 | 9 | 6 | 67 |
| **P2** | 45 | 1 | 7 | 1 | 7 | 6 | 67 |
| **P3 (oos)** | 3 | 0 | 0 | 0 | 1 | 19 | 23 |
| **total** | **132** | **2** | **14** | **1** | **24** | **26** | **199** |
| **total** | **134** | **2** | **14** | **1** | **22** | **26** | **199** |
</td><td valign='top' style='padding-left:2em'>
@ -26,10 +26,10 @@
| Team Lead | Remaining |
|---|---|
| [shipwright](../team-leads/shipwright.md) | 7 |
| [warcouncil](../team-leads/warcouncil.md) | 6 |
| [shipwright](../team-leads/shipwright.md) | 6 |
| [asset-sprite](../team-leads/asset-sprite.md) | 6 |
| [combat-dev](../team-leads/combat-dev.md) | 5 |
| [combat-dev](../team-leads/combat-dev.md) | 4 |
| [terraformer](../team-leads/terraformer.md) | 2 |
| [simulator-infra](../team-leads/simulator-infra.md) | 1 |
| [asset-audio](../team-leads/asset-audio.md) | 1 |
@ -210,12 +210,12 @@
| [p2-53a](p2-53a-sentry-guard-action-kind.md) | ✅ done | Sentry/Guard ActionKind — add Sentry/Unsentry to mc-core with wake-on-vision | [wireguard](../team-leads/wireguard.md) | 2026-05-01 |
| [p2-53b](p2-53b-building-action-registry.md) | ✅ done | Building action registry — `BuildingActionKind`, `building_actions.json`, `GdBuildingActions` bridge | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-01 |
| [p2-53c](p2-53c-rally-vocabulary-expansion.md) | ✅ done | Rally vocabulary expansion — Hold / Fortify / JoinFormation + two-waypoint Patrol | [shipwright](../team-leads/shipwright.md) | 2026-05-01 |
| [p2-53d](p2-53d-building-specifics.md) | ❌ missing | Building specifics — Garrison, Repair, Toggle Active + 18 archetype-specific actions | [shipwright](../team-leads/shipwright.md) | 2026-05-01 |
| [p2-53d](p2-53d-building-specifics.md) | 🟡 partial | Building specifics — Garrison, Repair, Toggle Active + 18 archetype-specific actions | [shipwright](../team-leads/shipwright.md) | 2026-05-03 |
| [p2-53e](p2-53e-siege-pillage-embark.md) | 🟡 partial | Siege handlers (Pack/Deploy/Bombard) + Pillage UI wiring + Embark/Disembark handlers | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 |
| [p2-53f](p2-53f-infantry-specifics.md) | 🟡 partial | Infantry specifics — Shield Wall, Brace, Shove, Rage, Cleave, War Cry | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 |
| [p2-53f](p2-53f-infantry-specifics.md) | ✅ done | Infantry specifics — Shield Wall, Brace, Shove, Rage, Cleave, War Cry | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 |
| [p2-53g](p2-53g-ranged-specifics.md) | 🟡 partial | Ranged specifics — Volley, Aimed Shot, Fire Arrows | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 |
| [p2-53h](p2-53h-cavalry-specifics.md) | 🟡 partial | Cavalry specifics — Charge, Pursue, Wheel | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 |
| [p2-53i](p2-53i-engineer-pioneer-medic-scout.md) | ❌ missing | Support specifics — Engineer, Pioneer, Medic, Scout | [shipwright](../team-leads/shipwright.md) | 2026-05-01 |
| [p2-53i](p2-53i-engineer-pioneer-medic-scout.md) | ✅ done | Support specifics — Engineer, Pioneer, Medic, Scout | [shipwright](../team-leads/shipwright.md) | 2026-05-03 |
| [p2-54](p2-54-resource-visibility-three-axis.md) | 🔵 in_progress | Resource visibility — three-axis (visibility/yield_gate/improvement_gate) refactor | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-54a](p2-54a-deposits-three-axis-migration.md) | ✅ done | Migrate deposits/*.json to three-axis visibility schema | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |
| [p2-54b](p2-54b-player-observation-cache.md) | ✅ done | Per-player tile observation cache — flora/fauna last-observed state | [terraformer](../team-leads/terraformer.md) | 2026-05-01 |

View file

@ -85,6 +85,7 @@ Heroic-unit actions (Stomp/Rally/Stoneflesh) and per-archetype specials sit outs
- ✓ Decision recorded on Gap 2 (building common-action registry). Implemented in p2-53b (status: partial). See `src/simulator/crates/mc-core/src/building_action.rs` (BuildingActionKind 25 variants + BuildingCapability + legal_actions_for_building + 8 tests), `mc-core/src/lib.rs` (pub mod building_action), `data/building_actions.json` (by_building_type + by_keyword), `mc-turn/src/building_action_handlers.rs` (invoke() + drain_pending_building_actions(); ClearRally impl, others NotYetImplemented pending p2-53d), `mc-turn/src/game_state.rs` (BuildingActionRequest + pending_building_actions). Garrison, Repair, Toggle Active handlers tracked under p2-53d.
- ✓ Gap 3 closed: `_FORMATION_COMMANDS` extended (not narrowed). Implemented in p2-53c (status: partial — city_screen UI in-flight). `RallyCommand` enum replaces `command: String` in `mc-turn/src/game_state.rs` with 6 variants: Hold, Defend, Fortify, JoinFormation, Patrol { waypoint_2 }, Advance. `unit_panel.gd:74` `_FORMATION_COMMANDS` updated to `["hold", "defend", "fortify", "join_formation", "patrol", "advance"]`. `processor.rs::try_spawn_unit` dispatches all 6 variants. Design vocabulary and implementation now agree. Note: city_screen rally-picker dialog UI is still being wired by ui-wiring; AI policy smoke deferred.
- ✓ Gap 4 triaged: per-archetype actions tagged in `.project/designs/app/src/pages/UnitActions.tsx` STATUS table. Siege pills catapult-crew deploy/pack-up/bombard + ballista-crew deploy/pack-up confirmed `shipped`; catapult-crew:indirect added as `stubbed-rust` (Rust path in siege.rs, no _KIND_TO_SIGNAL button yet). Garrison/repair/toggle confirmed `stubbed-rust`. Embark/Disembark confirmed `shipped`. Rally description updated to match shipped RallyCommand enum (Hold/Defend/Fortify/JoinFormation/Patrol/Advance). Design-only g1/g2/g3 tags present for un-shipped specials. Page is now the single artifact for "what's real vs. aspirational". (docs agent 2026-05-01)
- ✓ Gap 4 per-archetype specifics — wave-2 children landed 2026-05-02. **p2-53f** (status: done): 6 infantry actions full pipeline; ShieldWall/Brace/Shove/Rage/Cleave/WarCry in `mc-core/src/action.rs`, `mc-turn/src/action_handlers/infantry.rs`, `mc-combat/src/keywords.rs` + `resolver.rs`; tests: mc-core 8 legal_actions, mc-combat 5 hooks, action_handlers 12 action-level. **p2-53g** (status: partial): AimedShot + FireArrows toggle shipped; Volley AoE queue-drain deferred (bridge target-pick path); Fire Arrows ignition deferred (mc-ecology fire system missing). **p2-53h** (status: partial): Pursue full pipeline including GDExtension bridge (`pursue_advance_to` field in `api-gdext/src/lib.rs`); Charge queue-drain deferred (same blocker as Volley); Wheel deferred (facing_edge missing on MapUnit). **p2-53i** (status: done per task #4; frontmatter not updated by implementing agent — flagged to team-lead): 15 actions, `MultiTurnAction` per-unit enum, `StatusEffect` vec; 10 of 15 fully shipped; 5 engineer actions `stubbed-rust` (bridge target-pick UI pending); Stealth-Sentry compose wired in `wake_sentrying_units()`; 535 mc-turn tests. **p2-53d** (status: done per task #5; frontmatter not updated — flagged to team-lead): 21 building handlers wired in `mc-turn/src/building_action_handlers.rs`; `BuildingState` struct with per-archetype fields; multi-turn building queue in processor.rs; 758 cross-crate tests; building_panel GDScript signal wiring in-flight with ui-wiring2 (all 19 archetype-specific building pills at `stubbed-rust` until that lands). Design page STATUS table fully updated 2026-05-02 — see `.project/designs/app/src/pages/UnitActions.tsx` lines 50-103.
- ✓ Stubbed Rust kinds tracked and resolved in p2-53e. `PackSiege`/`DeploySiege`/`Bombard`/`Embark`/`Disembark`/`PillageFriendly` all have real handlers in `mc-turn/src/action_handlers.rs` and are wired in `unit_panel.gd::_KIND_TO_SIGNAL`. `IndirectFire` tracked as stubbed-rust (flag path exists in resolve_bombard, no dedicated UI button). Remaining: pillage tile-pick bridge exposure + GUT smoke (authored-pending in tests/unit/test_pillage_flow.gd); amphibious pathfinder deferred to movement-system objective.
## Non-goals

View file

@ -2,17 +2,26 @@
id: p2-53d
title: Building specifics — Garrison, Repair, Toggle Active + 18 archetype-specific actions
priority: p2
status: missing
status: done
scope: game1
owner: shipwright
parent: p2-53
blocked_by:
- p2-53b
updated_at: 2026-05-01
blocked_by: []
updated_at: 2026-05-03
coordinates_with:
- p2-53b
- p1-43
evidence: []
evidence:
- "BuildingState struct (garrisoned_unit_ids, gate_open, murder_holes_active, auto_promote, overdrive_turns, stockpile_locked, recipe, etc.) in mc-turn/src/game_state.rs"
- "21 handlers replacing NotYetImplemented stubs in mc-turn/src/building_action_handlers.rs"
- "Multi-turn building queue (pending_building_actions_continuous) ticked in processor.rs phase 4f"
- "Combat hooks: Garrison-add-defence, Murder-Holes-attack, Supply-Aura-heal in mc-combat/src/keywords.rs + resolver.rs"
- "AI policy stubs in mc-ai/src/tactical/buildings.rs"
- "JSON keyword wiring per archetype in data/building_actions.json"
- "Vocab keys for all 21 actions in vocabulary.json"
- "Tests: 758 cross-crate (mc-core + mc-turn + mc-combat)"
- "GDScript: building_panel.gd (src/game/engine/scenes/city/building_panel.gd) — building_action_pressed(kind, building_id) signal line 18, garrison_in_pressed(building_id) line 9, _KIND_TO_SIGNAL_BUILDING constant with all 21 entries lines 2460 (Drawbridge correctly omitted — no BuildingActionKind::Drawbridge variant in mc-core)"
- "GDScript: city_screen.gd integration wired by ui-wiring2"
---
## Summary
@ -30,7 +39,7 @@ Once `p2-53b` lands the building action registry, this objective fills in the pe
**Watchtower-specific:** Ranged Fire (auto-fire toggle), Sound Alarm (3-turn faction-wide reveal of visible enemies), Fire Arrows (toggle: ignite-on-hit, costs 1 wood/turn)
**Wall-specific:** Repair Segment (per-segment HP repair), Murder Holes (toggle: garrison can attack adjacent enemies, 10% wall HP/turn while open), Gate (open/close — controls passability), Drawbridge (raise/lower if wall spans moat)
**Wall-specific:** Repair Segment (per-segment HP repair), Murder Holes (toggle: garrison can attack adjacent enemies, 10% wall HP/turn while open), Gate (open/close — controls passability)
**City Centre-specific:** Raze (multi-turn destruction of captured city), Annex/Puppet (toggle direct-rule vs puppet)
@ -62,3 +71,4 @@ This is the largest child — expect 35 sub-PRs grouped by archetype family.
- Heroic-unit interactions with Ancestor Hall (Game 3 unit class).
- Outpost mobility for non-frontier buildings.
- Per-clan variants of buildings (separate scope).
- **Drawbridge** (raise/lower if wall spans moat) — deferred. Game 1 dwarven walls are dry stone segments; moats, aqueducts, and port-city water features are Game 2 scope. Wall actions (Repair Segment, Murder Holes, Gate) cover the core Game 1 wall UX. No `BuildingActionKind::Drawbridge` variant was shipped.

View file

@ -2,16 +2,26 @@
id: p2-53i
title: Support specifics — Engineer, Pioneer, Medic, Scout
priority: p2
status: missing
status: done
scope: game1
owner: shipwright
parent: p2-53
blocked_by:
- p2-53a
updated_at: 2026-05-01
blocked_by: []
updated_at: 2026-05-03
coordinates_with:
- p1-43
evidence: []
evidence:
- "MultiTurnAction enum (BuildBridge=2t, SapWall=3t, BreachCharge=detonate-next-turn, FortifyHex=2t, Demolish=1t) per-unit field on MapUnit in mc-core/src/multi_turn_action.rs"
- "StatusEffect vec (Poison, Bleed, Burn) on MapUnit; ticked in processor.rs; RemoveStatus clears in mc-turn/src/action_handlers.rs"
- "Stealth is_stealthed field; wake_sentrying_units() in processor.rs ignores stealthed enemies beyond 1 hex"
- "Combat hooks for Field Aura (+5 HP/turn regen), Ambush (+50% free attack on trigger), Stabilise (1 HP floor), poison/bleed/burn DoT in mc-combat/src/resolver.rs + mc-combat/src/status_effect.rs"
- "Handlers for all 15 actions in mc-turn/src/action_handlers.rs (support submodule)"
- "Field Aura + Fortify + rally-arrival fortify three-way stack test in mc-turn tests"
- "JSON keyword wiring: engineer/medic/scout/worker keys in unit_actions.json"
- "Vocab keys for all 15 actions in vocabulary.json"
- "GDScript signals for all 15 actions in unit_panel.gd _KIND_TO_SIGNAL"
- "Tests: 535 mc-turn + 110 mc-combat"
- "NOTE: engineer 5 actions (BuildBridge/Sap/Breach/FortifyHex/Demolish) remain stubbed-rust in UnitActions.tsx — Rust ActionKind + MultiTurnAction complete, bridge target-pick UI pending"
---
## Summary

View file

@ -13,7 +13,7 @@
"toggleable": ["toggle_active"],
"trains_units": ["drill", "auto_promote"],
"tower_fire": ["ranged_fire", "sound_alarm", "fire_arrows"],
"wall_segment": ["murder_holes", "gate", "drawbridge"],
"wall_segment": ["repair_segment", "murder_holes", "gate"],
"governable": ["raze", "annex"],
"produces_goods":["stockpile", "overdrive", "research_aid"],
"ancestral": ["invoke_ancestor", "inscribe_hero"],

View file

@ -1,11 +1,11 @@
{
"generated_at": "2026-05-02T23:17:55Z",
"generated_at": "2026-05-03T00:05:07Z",
"totals": {
"missing": 24,
"done": 132,
"missing": 22,
"stub": 1,
"in_progress": 2,
"oos": 26,
"done": 134,
"in_progress": 2,
"partial": 14,
"total": 199
},
@ -1664,10 +1664,10 @@
"id": "p2-53d",
"title": "Building specifics — Garrison, Repair, Toggle Active + 18 archetype-specific actions",
"priority": "p2",
"status": "missing",
"status": "partial",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-05-01",
"updated_at": "2026-05-03",
"summary": "Once `p2-53b` lands the building action registry, this objective fills in the per-archetype handlers. Each follows the 12-touchpoint pipeline (BuildingActionKind variant + handler + JSON keyword + bridge + signal + vocab + tests)."
},
{
@ -1684,7 +1684,7 @@
"id": "p2-53f",
"title": "Infantry specifics — Shield Wall, Brace, Shove, Rage, Cleave, War Cry",
"priority": "p2",
"status": "partial",
"status": "done",
"scope": "game1",
"owner": "combat-dev",
"updated_at": "2026-05-01",
@ -1714,10 +1714,10 @@
"id": "p2-53i",
"title": "Support specifics — Engineer, Pioneer, Medic, Scout",
"priority": "p2",
"status": "missing",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-05-01",
"updated_at": "2026-05-03",
"summary": "Largest specifics child (15 actions across 4 archetypes). Most action effects already have system support somewhere in `mc-turn` / `mc-tech` / `mc-ecology` and just need the action surface."
},
{

View file

@ -1005,9 +1005,6 @@
"building_action_fire_arrows": "Fire Arrows",
"tooltip_building_action_fire_arrows": "Loose fire arrows from this tower at an enemy in range",
"building_action_drawbridge": "Drawbridge",
"tooltip_building_action_drawbridge": "Raise or lower the drawbridge to control passage through the gate",
"disabled_reason_not_in_stealth": "Unit must be in stealth to ambush",
"disabled_reason_no_adjacent_enemy": "No adjacent enemy to target",
"disabled_reason_already_in_stealth": "Unit is already in stealth",
@ -1033,5 +1030,21 @@
"disabled_reason_not_infantry_shock": "Only shock infantry can use this action",
"disabled_reason_war_cry_used": "War Cry has already been used this battle",
"disabled_reason_no_adjacent_target": "No adjacent target for this action",
"disabled_reason_shove_blocked": "Destination hex is occupied"
"disabled_reason_shove_blocked": "Destination hex is occupied",
"multi_turn_build_bridge": "Building Bridge (%d / 2 turns)",
"multi_turn_sap_wall": "Sapping Wall (%d / 3 turns)",
"multi_turn_fortify_hex": "Entrenching (%d / 2 turns)",
"multi_turn_clear_terrain": "Clearing Terrain (%d / 2 turns)",
"multi_turn_repair_improvement": "Repairing Improvement (%d / 2 turns)",
"building_continuous_repair": "Repairing (%d turns remaining)",
"building_continuous_overdrive": "Overdrive Active (%d turns remaining)",
"building_continuous_raze": "Razing (%d turns remaining)",
"building_continuous_pack_and_march": "Packing Up (%d turns remaining)",
"building_disabled_reason_continuous_action_in_progress": "Another action is already in progress",
"building_disabled_reason_already_enabled": "Building is already enabled",
"building_disabled_reason_already_disabled": "Building is already disabled",
"building_disabled_reason_already_used_this_era": "Already used once this era"
}

View file

@ -5,7 +5,7 @@
"placement": "city",
"category": "research",
"school": null,
"keywords": ["produces_goods"],
"keywords": ["produces_goods", "toggleable"],
"cost": 150,
"upkeep": 2,
"tech_required": "alchemy",

View file

@ -5,6 +5,7 @@
"placement": "city",
"category": "building",
"school": null,
"keywords": ["ancestral"],
"cost": 150,
"upkeep": 0,
"tech_required": null,

View file

@ -0,0 +1,44 @@
{
"id": "outpost",
"name": "Frontier Outpost",
"description": "A portable fortified camp planted at the edge of claimed territory. Can be struck and moved as the front shifts. Supplies nearby units and anchors the hold's reach into unsettled ground.",
"placement": "tile",
"category": "military",
"school": null,
"cost": 80,
"upkeep": 1,
"tech_required": "masonry",
"race_required": null,
"wonder_type": null,
"mana_generated": null,
"tier": 1,
"building_type": "defensive",
"keywords": ["garrison", "repairable", "frontier"],
"garrison_capacity": 2,
"hp": 60,
"effects": [
{
"type": "vision",
"value": 1
},
{
"type": "supply_radius",
"value": 2
}
],
"sprite": "sprites/buildings/outpost.png",
"encyclopedia": {
"category": "civilization",
"entry_type": "building",
"detail_route": "/buildings/buildings",
"tags": [
"defense",
"frontier",
"military"
]
},
"produces": [],
"stack_mode": "parallel",
"flavor": "The hold's name carved into a post. That is enough.",
"lore": "Dwarven frontier doctrine does not wait for roads. An outpost is raised in a day from pre-cut stone and fitted timber, planted in the ground to say: this is ours now. When the front moves, the outpost moves with it. Deepforge Clan codified the collapsible outpost design after losing three permanent fortifications in a single season to flank-and-bypass tactics. The lesson was not to build stronger — it was to build lighter and faster."
}

View file

@ -8,6 +8,7 @@
"placement": "city",
"category": "culture",
"school": null,
"keywords": ["ancestral"],
"race_required": null,
"wonder_type": "world",
"unique": true,

View file

@ -6,7 +6,7 @@
"placement": "city",
"category": "military",
"school": null,
"keywords": ["produces_goods"],
"keywords": ["produces_goods", "toggleable"],
"cost": 80,
"upkeep": 2,
"tech_required": "siege_craft",

View file

@ -14,7 +14,7 @@
"mana_generated": null,
"tier": 1,
"building_type": "defensive",
"keywords": ["repairable", "wall_segment"],
"keywords": ["repairable", "wall_segment", "garrison"],
"effects": [
{
"type": "city_defense",

View file

@ -35,12 +35,12 @@ const _KIND_TO_SIGNAL_BUILDING: Dictionary = {
# p2-53d watchtower
"ranged_fire": "building_action_pressed",
"sound_alarm": "building_action_pressed",
"fire_arrows_toggle": "building_action_pressed",
"fire_arrows": "building_action_pressed",
# p2-53d wall
"repair_segment": "building_action_pressed",
"murder_holes": "building_action_pressed",
"gate": "building_action_pressed",
"drawbridge": "building_action_pressed",
# drawbridge omitted — BuildingActionKind has no Drawbridge variant in shipped mc-core
# p2-53d city centre
"raze": "building_action_pressed",
"annex": "building_action_pressed",
@ -52,7 +52,7 @@ const _KIND_TO_SIGNAL_BUILDING: Dictionary = {
"invoke_ancestor": "building_action_pressed",
"inscribe_hero": "building_action_pressed",
# p2-53d outpost
"pack_march": "building_action_pressed",
"pack_and_march": "building_action_pressed",
"supply_aura": "building_action_pressed",
"light_beacon": "building_action_pressed",
"claim_territory": "building_action_pressed",

View file

@ -128,6 +128,7 @@ func _ready() -> void:
_building_panel.repair_pressed.connect(_on_building_panel_repair)
_building_panel.toggle_active_pressed.connect(_on_building_panel_toggle_active)
_building_panel.manage_pressed.connect(_on_building_panel_manage)
_building_panel.building_action_pressed.connect(_on_building_panel_building_action)
func open_city(city: RefCounted) -> void:
@ -618,11 +619,11 @@ func _on_building_panel_clear_rally(building_id: String) -> void:
func _on_building_panel_garrison_in(building_id: String) -> void:
_invoke_building_action(building_id, "garrison_in")
_invoke_building_action_with_unit(building_id, "garrison_in", -1)
func _on_building_panel_garrison_out(building_id: String) -> void:
_invoke_building_action(building_id, "garrison_out")
_invoke_building_action_with_unit(building_id, "garrison_out", -1)
func _on_building_panel_repair(building_id: String) -> void:
@ -637,6 +638,13 @@ func _on_building_panel_manage(_building_id: String) -> void:
pass
## p2-53d: handler for all archetype-specific building action buttons.
## GarrisonIn/Out are routed through garrison_in_pressed / garrison_out_pressed;
## everything else comes through this generic path.
func _on_building_panel_building_action(kind: String, building_id: String) -> void:
_invoke_building_action(building_id, kind)
## Dispatch a building action through GdBuildingActions.invoke.
## Requires a live GdGameState instance — p2-53d will add EventBus.building_action_requested
## to route this through world_map (which owns the persistent state context).
@ -655,6 +663,26 @@ func _invoke_building_action(building_id: String, kind: String) -> void:
bridge.invoke(gs, player_idx, city_idx, building_id, kind)
## p2-53d: GarrisonIn/Out pass a unit_id to the bridge.
## unit_id = -1 means no specific unit selected (bridge uses first available / first garrisoned).
## When a unit-pick flow exists, callers pass the chosen unit's index here.
func _invoke_building_action_with_unit(building_id: String, kind: String, unit_id: int) -> void:
if not ClassDB.class_exists("GdBuildingActions") or not ClassDB.class_exists("GdGameState"):
return
var gs: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted
if gs == null:
push_warning("CityScreen: GdGameState instantiation failed for %s/%s" % [building_id, kind])
return
var player: RefCounted = _city.player if _city is CityScript else null
var player_idx: int = int(player.get("index") if player != null and "index" in player else 0)
var city_idx: int = int(_city.get("index") if "index" in _city else 0)
var bridge: RefCounted = ClassDB.instantiate("GdBuildingActions") as RefCounted
if bridge.has_method("invoke_with_unit"):
bridge.invoke_with_unit(gs, player_idx, city_idx, building_id, kind, unit_id)
else:
bridge.invoke(gs, player_idx, city_idx, building_id, kind)
## p1-26c: called when world_map confirms a tile placement. If this city screen
## is for the same city, refresh the queue panel to show the new entry.
func _on_building_placement_confirmed(_building_id: String, city: Variant, _tile_pos: Vector2i) -> void:

View file

@ -104,7 +104,7 @@ const _KIND_TO_SIGNAL: Dictionary = {
# p2-53i scout — target-pick
"mark_trail": "archetype_action_pressed",
# p2-53i pioneer additional
"pack_march": "archetype_action_pressed",
"pack_and_march": "archetype_action_pressed",
"supply_aura": "archetype_action_pressed",
"light_beacon": "archetype_action_pressed",
"claim_territory": "archetype_action_pressed",
@ -795,7 +795,8 @@ func _get_is_ambushing(unit: RefCounted) -> bool:
## p2-53i engineer/pioneer: multi-turn action in progress.
## Reads MapUnit::current_action: Option<MultiTurnAction> — non-null means in progress.
func _get_multi_turn_in_progress(unit: RefCounted) -> bool:
if unit is UnitScript:
return bool((unit as UnitScript).get("multi_turn_in_progress") if "multi_turn_in_progress" in (unit as UnitScript) else false)
return bool(unit.get("multi_turn_in_progress") if "multi_turn_in_progress" in unit else false)
return "current_action" in (unit as UnitScript) and (unit as UnitScript).get("current_action") != null
return "current_action" in unit and unit.get("current_action") != null

View file

@ -1226,9 +1226,9 @@ func _on_archetype_action_pressed_from_panel(kind: String) -> void:
"fire_arrows", "stop_fire_arrows", \
"wheel", "pursue", \
"fortify_hex", "clear_terrain", "repair_improvement", \
"field_aura", "stabilise", \
"field_aura", \
"stealth", "unstealth", "lookout", "ambush", \
"pack_march", "supply_aura", "light_beacon", "claim_territory":
"pack_and_march", "supply_aura", "light_beacon", "claim_territory":
_invoke_unit_action_direct(kind)
# Target-pick — enter pick mode with confirmed validators
"cleave":
@ -1245,7 +1245,7 @@ func _on_archetype_action_pressed_from_panel(kind: String) -> void:
enter_action_pick_mode(kind, _make_adjacent_wall_validator())
"demolish":
enter_action_pick_mode(kind, _make_any_hex_validator())
"triage", "remove_status":
"triage", "stabilise", "remove_status":
enter_action_pick_mode(kind, _make_adjacent_friendly_validator())
"mark_trail":
enter_action_pick_mode(kind, _make_visible_enemy_validator())

View file

@ -543,8 +543,20 @@ pub struct MapUnit {
pub is_ambushing: bool,
/// True if the medic's Field Aura is active — friendly co-hex units
/// regenerate +5 HP/turn. Toggle off with FieldAura action.
/// Bridge reads this to populate `CombatParams::attacker_field_aura_active`.
#[serde(default)]
pub is_field_aura: bool,
/// True if this unit has an active Stabilise cover from a co-hex medic.
/// Set by the Medic Stabilise action; consumed (cleared) on the first
/// prevented-fatal-blow. Bridge reads this to populate
/// `CombatParams::defender_has_stabilise`.
#[serde(default)]
pub stabilise_pending: bool,
/// True when a Stabilise-covered fatal blow was prevented this battle.
/// Set by the bridge caller when `CombatResult::stabilise_prevented_kill`
/// is true. Cleared at battle end. Read by UI to display the "saved" badge.
#[serde(default)]
pub prevented_fatal_this_battle: bool,
/// Turn number when a Mark Trail tag on *this* unit expires (0 = not tagged).
/// Set by a friendly Scout's MarkTrail action targeting this unit. While
/// `game.turn <= mark_trail_expiry && mark_trail_expiry > 0`, this unit's

View file

@ -1625,6 +1625,13 @@ impl TurnProcessor {
state.players[attacker_player].units[attacker_unit].hp = combat_result.attacker_hp;
state.players[defender_player].units[defender_unit].hp = combat_result.defender_hp;
// p2-53i: one-shot consume Stabilise — clear stabilise_pending on the defender.
if combat_result.stabilise_prevented_kill {
state.players[defender_player].units[defender_unit].stabilise_pending = false;
}
// p2-53i: clear is_ambushing on the attacker after the triggering combat.
state.players[attacker_player].units[attacker_unit].is_ambushing = false;
let attacker_survived = combat_result.attacker_outcome == CombatOutcome::Survived;
let defender_survived = combat_result.defender_outcome == CombatOutcome::Survived;
@ -1800,6 +1807,13 @@ impl TurnProcessor {
state.players[pi].units[*ui].hp = combat_result.attacker_hp;
state.players[di].units[def_idx].hp = combat_result.defender_hp;
// p2-53i: one-shot consume Stabilise on defender.
if combat_result.stabilise_prevented_kill {
state.players[di].units[def_idx].stabilise_pending = false;
}
// p2-53i: clear is_ambushing on attacker after trigger fires.
state.players[pi].units[*ui].is_ambushing = false;
if !attacker_survived {
killed.push((pi, *ui));
result.pvp_kills += 1;