12 KiB
| id | title | priority | status | scope | owner | parent | updated_at | coordinates_with | evidence | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| p2-53e | Siege handlers (Pack/Deploy/Bombard) + Pillage UI wiring + Embark/Disembark handlers | p2 | done | game1 | combat-dev | p2-53 | 2026-05-03 |
|
|
Summary
Three Rust ActionKind variants (PackSiege, DeploySiege, Bombard) are defined in mc-core/src/action.rs:32 but explicitly annotated "no handler wired". They appear in unit_actions.json for the siege keyword but action_handlers.rs::invoke() returns Err(WrongTerrain) for all three. Embark/Disembark for amphibious units are similarly stubbed. PillageFriendly has a handler stub but is not in unit_panel.gd::_KIND_TO_SIGNAL, so the button never renders.
This objective ships the full feature for these five existing-but-stubbed actions. They share a structural pattern: state-toggle action that gates other actions (Deployed siege can Bombard, Packed cannot move-and-fire, etc.). After landing, players can actually use catapults / ballistas / cannon crews against walls; pillage is a clickable action; coastal units cross water.
Full feature
Siege Pack/Deploy/Bombard
Siege units (keywords: ["siege"]) ship in two postures: packed (can move at normal speed; cannot fire) and deployed (cannot move; +1 range, +25% damage, can Bombard).
State: new is_deployed: bool field on MapUnit (default false; serde default).
Actions:
- DeploySiege — only legal when
!is_deployed && has_movement. Spends all remaining movement; setsis_deployed = true. Disabled reasonMustPackFirstif already deployed. - PackSiege — only legal when
is_deployed. Setsis_deployed = falseand spends 1 movement. Disabled reasonMustDeployFirstif not deployed. - Bombard — only legal when
is_deployed. A targeted ranged attack against a hex (unit OR structure) with+100%damage vs walls/buildings, ignores unit defence on structures, requires line-of-sight (catapults bypass via Indirect Fire, see below). Cannot fire across friendly-occupied edges (HEX_GEOMETRY.md). - IndirectFire (catapult-only via
keywords: ["arcing"]) — Bombard variant that lobs over LoS-blocking hexes. Same damage profile, slower (1 turn lag). Lands as a sub-variant or a flag onBombardOrder.
Move/Attack/RangedAttack become disabled when is_deployed = true (siege cannot relocate while deployed). Move is disabled when not deployed and no movement (existing behaviour). The legal_actions gates compose cleanly with existing fortify/patrol gates.
Bombard handler lives in mc-combat/src/siege.rs (new) — separate from melee resolver because of structure-target rules. AI policy in mc-ai: deploy siege when within range+1 of an enemy fortification and no enemy adjacent; pack and reposition when threatened.
Pillage UI wiring
PillageFriendly is already in mc-core::ActionKind and handled in mc-turn/src/action_handlers.rs::invoke (per p1-20). Missing: GDScript signal + _KIND_TO_SIGNAL entry + vocab keys. The action lets a worker unit destroy a friendly tile improvement to recover partial production cost (typical use: scorched-earth retreat, or repurposing land before a wonder).
This is a one-line wiring change blocked only by the lack of explicit objective. Lands here.
Embark/Disembark
Coastal units (keywords: ["amphibious"]) cross water. Today coastal hexes block all land movement; this objective opens single-hex embarkation: an amphibious unit can step into a coastal water hex (Embark) and step back out into a coastal land hex (Disembark). Defence drops to 50% while embarked; cannot fortify.
State: is_embarked: bool on MapUnit. Pathfinder considers water hexes passable iff unit is amphibious (already partially in mc-mapgen movement cost; verify).
This is the minimal viable naval — no naval combat, no transports, no naval-specific units. Game 1 scope per scope-game1-vs-game2.md allows shore-crossing for amphibious land units; full naval is g6 oos.
Acceptance
Siege
MapUnit::is_deployed: boolwith#[serde(default)]. —mc-turn/src/game_state.rsUnitCapability::is_deployed: boolandlegal_actionsgates: DeploySiege requires!is_deployed; PackSiege requiresis_deployed; Bombard requiresis_deployed; Move/Attack/RangedAttack disabled whenis_deployed. —mc-core/src/action.rsDisabledReason::MustDeployFirstandMustPackFirst(SiegeMustDeployFirst/SiegeMustPackFirst) gating wired. Also addedAlreadyDeployed,NotDeployedvariants. —mc-core/src/action.rshandle_deploy_siege,handle_pack_siegeinaction_handlers.rs.invoke()match arms wired (replacesErr(WrongTerrain)stub). —mc-turn/src/action_handlers.rsmc-combat/src/siege.rshasresolve_bombard(attacker_attack, target, indirect_fire). Damage formula:attack * 2; +100% vs structures; −10% for arcing/indirect.BombardTarget,BombardResulttypes exported. —mc-combat/src/siege.rsIndirectFiregates onkeywords: ["arcing"]only (passed asindirect_fire: boolfrom caller); ballistas (line-trajectory) passfalse. —mc-combat/src/siege.rs,mc-ai/src/tactical/mod.rs- AI policy in
mc-ai/src/tactical/movement.rs—decide_siege_actionissues Deploy when city within range+1, Bombard when deployed and target in range, Pack when threatened adjacent. —mc-ai/src/tactical/movement.rs - GDScript:
siege_action_pressed(kind: String)signal inunit_panel.gd;pack_siege/deploy_siege/bombardin_KIND_TO_SIGNAL. —src/game/engine/scenes/hud/unit_panel.gd(Bombard target-pick mode is ui-wiring territory; coordinate with that agent) - Vocab keys for all three +
disabled_reason_*variants. —public/games/age-of-dwarves/vocabulary.json - Tests:
legal_actionsmatrix (deployed × movement × keyword),handle_deploy_siegemutates flag,resolve_bombarddamages walls correctly,resolve_bombardindirect fire penalty. All pass (cargo test -p mc-core -p mc-turn -p mc-combat -p mc-ai— 0 failed).
Pillage
pillage_friendlyadded tounit_panel.gd::_KIND_TO_SIGNALmapping topillage_pressedsignal. —src/game/engine/scenes/hud/unit_panel.gdworld_map.gdhandler: enters tile-pick mode; on confirm, removes improvement from GDScript tile entity and emitsEventBus.tile_pillaged+EventBus.improvement_removed. —_on_pillage_pressed_from_panel→_pillage_pick_mode=true; left-click →_confirm_pillage(axial); ESC →_exit_pillage_pick_mode.pillage_pressedconnected in_connect_signals. Note:GdGameState::pillage_improvementwrapper not yet exposed by combat-actions; confirm handler guards withhas_methodpending that bridge method.- Vocab keys
action_pillage_friendly+tooltip_action_pillage_friendly. —public/games/age-of-dwarves/vocabulary.json - GUT smoke test: drain pipeline verified end-to-end.
test_pillage_flow.gdrewritten with 3 real assertions: queue drains afterstep(), improvement preserved when worker unit_index invalid (skip-path validation), pillage with no improvement is noop. New bridge helperspending_pillage_request_count,has_tile_improvement,seed_tile_improvementadded toGdGameState. Plusprocess_pillage_requestsdrain phase wired inprocessor.rsPhase 5a-pillage with 3 new Rust unit tests. cargo test -p mc-turn --lib: 199 passed, 0 failed.
Embark/Disembark
MapUnit::is_embarked: boolwith#[serde(default)]. —mc-turn/src/game_state.rslegal_actionsgates: Embark requires amphibious + adjacent water hex + not already embarked; Disembark requires amphibious + on water + adjacent land hex + currently embarked.UnitCapabilityextended withadjacent_water,adjacent_land. —mc-core/src/action.rs- Handlers in
action_handlers.rs.invoke()match arms. Embark clears fortify; disembark clears embarked. —mc-turn/src/action_handlers.rs mc-combat:embarked_defence_penalty(base_defence)— 50% reduction, minimum 1. —mc-combat/src/siege.rs- Pathfinder (
mc-turn) treats water as passable iff amphibious; cost = 2 MP.terrain_movement_cost_for_unit(biome, is_amphibious)+MapUnit::is_amphibious: bool(serde(default)) +step_toward_with_terrainupdated to threadunit.is_amphibious. Bridge:GdGameState::set_unit_amphibious #[func]. Test:amphibious_pathfinder_ocean_passable(mc-turn/src/processor.rs). p2-53 closeout 2026-05-03. unit_panel.gd::_KIND_TO_SIGNAL:embark/disembarkmapping + signals. —src/game/engine/scenes/hud/unit_panel.gd- Vocab keys. —
public/games/age-of-dwarves/vocabulary.json - Tests: embark sets flag + clears fortify; disembark clears flag; double-embark/disembark error cases; embarked penalty math. All pass.
Cross-cutting
- Design page
.project/designs/app/src/pages/UnitActions.tsx: catapult-crew deploy/pack-up/bombard + ballista-crew deploy/pack-up confirmedshipped; catapult-crew:indirect added asstubbed-rust(Rust path in siege.rs, no _KIND_TO_SIGNAL button); embark/disembark confirmedshipped. — docs agent 2026-05-01 - All gates:
cargo test -p mc-core -p mc-turn -p mc-combat -p mc-ai— 0 failed, all green. - p2-53 parent acceptance: gap-4 stubbed-rust items marked ✓ in p2-53 parent objective. — docs agent 2026-05-01
Non-goals
- Naval combat (ship-vs-ship, ship-vs-coast). g6 oos per
scope-game1-vs-game2.md. - Transport units carrying multiple land units. Game 2.
- Siege workshops (mobile production). Game 2.
- Per-clan siege variants. Existing dwarf catapult/ballista/cannon variants suffice.
Open questions
- Should Bombard cost movement (current ranged-attack cost is 1) or be free-while-deployed? Recommend free for first attack per turn, costs 1 for a second — matches the catapult's "one shot, reload" rhythm.
- Indirect Fire damage parity — same as direct Bombard? Recommend −10% (the trade for ignoring LoS).