magicciv/.project/objectives/p2-53e-siege-pillage-embark.md
Natalie 8f5ee66edf feat(objectives): update p2 milestone progress
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-02 21:25:01 -04:00

12 KiB
Raw Blame History

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
p1-20
p2-53
p2-53a
MapUnit::is_deployed, is_embarked: mc-turn/src/game_state.rs (both serde(default))
UnitCapability siege gates: mc-core/src/action.rs (DeploySiege/PackSiege/Bombard legal_actions + disabled reasons)
handle_deploy_siege, handle_pack_siege: mc-turn/src/action_handlers.rs (invoke() arms wired)
mc-combat/src/siege.rs: resolve_bombard(indirect_fire: bool), embarked_defence_penalty; BombardTarget/BombardResult
AI decide_siege_action: mc-ai/src/tactical/movement.rs
unit_panel.gd: siege_action_pressed signal; pack_siege/deploy_siege/bombard in _KIND_TO_SIGNAL
pillage_friendly in unit_panel.gd _KIND_TO_SIGNAL → pillage_pressed signal; world_map.gd _confirm_pillage handler
embark/disembark handlers: mc-turn/src/action_handlers.rs:323-353; unit_panel.gd _KIND_TO_SIGNAL
vocabulary.json: all siege/pillage/embark/disembark vocab keys
cargo test -p mc-core -p mc-turn -p mc-combat -p mc-ai: 0 failed
UnitActions.tsx: siege pills catapult-crew deploy/pack-up/bombard + ballista-crew deploy/pack-up confirmed shipped; catapult-crew:indirect added as stubbed-rust (no _KIND_TO_SIGNAL button); embark/disembark confirmed shipped (docs agent 2026-05-01)
Pathfinder amphibious water cost: IMPLEMENTED — terrain_movement_cost_for_unit(biome, is_amphibious) added to mc-turn/src/processor.rs; MapUnit::is_amphibious: bool (serde(default)) added to game_state.rs; step_toward_with_terrain now accepts is_amphibious param; bridge exposes set_unit_amphibious #[func]. Test: amphibious_pathfinder_ocean_passable. p2-53 closeout 2026-05-03.
Pillage drain phase: SHIPPED — process_pillage_requests added to mc-turn/src/processor.rs (Phase 5a-pillage, before bombard). Validates player_index + unit_index + u16 coord range; calls GameState::pillage_improvement; severable improvements marked pillaged=true, non-severable removed. TurnResult::improvements_pillaged counter added to combat_event.rs. p2-53 closeout 2026-05-03.
Pillage GUT smoke: SHIPPED — test_pillage_flow.gd rewritten with 3 real assert tests using new GdGameState::pending_pillage_request_count, has_tile_improvement, seed_tile_improvement bridge methods. Tests: queue drains after step; non-severable improvement preserved when worker unit_index invalid (validates skip path); pillage with no improvement is noop. cargo test -p mc-turn pillage: 7 passed (3 new + 4 existing). p2-53 closeout 2026-05-03.
Pillage Rust unit tests: SHIPPED — pillage_request_removes_non_severable_improvement, pillage_request_marks_severable_improvement, pillage_request_with_invalid_unit_skipped in processor.rs::tests. cargo test -p mc-turn --lib: 199 passed, 0 failed, 1 ignored. p2-53 closeout 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; sets is_deployed = true. Disabled reason MustPackFirst if already deployed.
  • PackSiege — only legal when is_deployed. Sets is_deployed = false and spends 1 movement. Disabled reason MustDeployFirst if 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 on BombardOrder.

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: bool with #[serde(default)]. — mc-turn/src/game_state.rs
  • UnitCapability::is_deployed: bool and legal_actions gates: DeploySiege requires !is_deployed; PackSiege requires is_deployed; Bombard requires is_deployed; Move/Attack/RangedAttack disabled when is_deployed. — mc-core/src/action.rs
  • DisabledReason::MustDeployFirst and MustPackFirst (SiegeMustDeployFirst/SiegeMustPackFirst) gating wired. Also added AlreadyDeployed, NotDeployed variants. — mc-core/src/action.rs
  • handle_deploy_siege, handle_pack_siege in action_handlers.rs. invoke() match arms wired (replaces Err(WrongTerrain) stub). — mc-turn/src/action_handlers.rs
  • mc-combat/src/siege.rs has resolve_bombard(attacker_attack, target, indirect_fire). Damage formula: attack * 2; +100% vs structures; 10% for arcing/indirect. BombardTarget, BombardResult types exported. — mc-combat/src/siege.rs
  • IndirectFire gates on keywords: ["arcing"] only (passed as indirect_fire: bool from caller); ballistas (line-trajectory) pass false. — mc-combat/src/siege.rs, mc-ai/src/tactical/mod.rs
  • AI policy in mc-ai/src/tactical/movement.rsdecide_siege_action issues 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 in unit_panel.gd; pack_siege/deploy_siege/bombard in _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_actions matrix (deployed × movement × keyword), handle_deploy_siege mutates flag, resolve_bombard damages walls correctly, resolve_bombard indirect fire penalty. All pass (cargo test -p mc-core -p mc-turn -p mc-combat -p mc-ai — 0 failed).

Pillage

  • pillage_friendly added to unit_panel.gd::_KIND_TO_SIGNAL mapping to pillage_pressed signal. — src/game/engine/scenes/hud/unit_panel.gd
  • world_map.gd handler: enters tile-pick mode; on confirm, removes improvement from GDScript tile entity and emits EventBus.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_pressed connected in _connect_signals. Note: GdGameState::pillage_improvement wrapper not yet exposed by combat-actions; confirm handler guards with has_method pending 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.gd rewritten with 3 real assertions: queue drains after step(), improvement preserved when worker unit_index invalid (skip-path validation), pillage with no improvement is noop. New bridge helpers pending_pillage_request_count, has_tile_improvement, seed_tile_improvement added to GdGameState. Plus process_pillage_requests drain phase wired in processor.rs Phase 5a-pillage with 3 new Rust unit tests. cargo test -p mc-turn --lib: 199 passed, 0 failed.

Embark/Disembark

  • MapUnit::is_embarked: bool with #[serde(default)]. — mc-turn/src/game_state.rs
  • legal_actions gates: Embark requires amphibious + adjacent water hex + not already embarked; Disembark requires amphibious + on water + adjacent land hex + currently embarked. UnitCapability extended with adjacent_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_terrain updated to thread unit.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/disembark mapping + 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 confirmed shipped; catapult-crew:indirect added as stubbed-rust (Rust path in siege.rs, no _KIND_TO_SIGNAL button); embark/disembark confirmed shipped. — 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

  1. 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.
  2. Indirect Fire damage parity — same as direct Bombard? Recommend 10% (the trade for ignoring LoS).