docs(objectives): 📝 Add/clarify objectives and implementation details for the pioneer escort mechanic (P2-59) in the project's README and dedicated documentation

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-06-03 04:29:08 -07:00
parent e89bd48904
commit 864095c136
2 changed files with 14 additions and 22 deletions

View file

@ -16,9 +16,9 @@
|---|---|---|---|---|---|---|---|
| **P0** | 44 | 0 | 0 | 0 | 0 | 0 | 44 |
| **P1** | 55 | 1 | 14 | 2 | 5 | 1 | 78 |
| **P2** | 79 | 0 | 11 | 15 | 0 | 6 | 111 |
| **P2** | 80 | 0 | 11 | 14 | 0 | 6 | 111 |
| **P3 (oos)** | 12 | 0 | 5 | 5 | 0 | 21 | 43 |
| **total** | **190** | **1** | **30** | **22** | **5** | **28** | **276** |
| **total** | **191** | **1** | **30** | **21** | **5** | **28** | **276** |
</td><td valign='top' style='padding-left:2em'>
@ -254,7 +254,7 @@
| [p2-58](p2-58-ambient-encounter-rolls.md) | ✅ done | Ambient encounter rolls per tile moved — fauna_density × ecology_tier | [unassigned](../team-leads/unassigned.md) | 2026-05-07 |
| [p2-58a](p2-58a-tilestate-fauna-fields.md) | ✅ done | "TileState fauna fields — fauna_density + fauna_index for AmbientTileCtx" | [shipwright](../team-leads/shipwright.md) | 2026-05-07 |
| [p2-58b](p2-58b-ambient-encounter-hook.md) | ✅ done | "Ambient encounter hook — mc-turn::movement calls roll_ambient_encounter per tile step" | [unassigned](../team-leads/unassigned.md) | 2026-05-07 |
| [p2-59](p2-59-pioneer-escort-mechanic.md) | 🔴 stub | "Pioneer escort mechanic — protection rules vs ambient encounters" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 |
| [p2-59](p2-59-pioneer-escort-mechanic.md) | ✅ done | "Pioneer escort mechanic — protection rules vs ambient encounters" | [unassigned](../team-leads/unassigned.md) | 2026-06-03 |
| [p2-60](p2-60-weather-lens-godot-ui.md) | 🔴 stub | "Weather / observation lens switcher in the Godot HUD" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 |
| [p2-61](p2-61-observation-recording-gates-from-tech.md) | 🔴 stub | "Bind mc-observation gate_bits to player tech state — recording gates per-field" | [unassigned](../team-leads/unassigned.md) | 2026-05-03 |
| [p2-62](p2-62-procedural-unit-and-building-renderer.md) | ✅ done | Procedural unit/building renderer — alpha-only visual substitute | [asset-sprite](../team-leads/asset-sprite.md) | 2026-05-04 |

View file

@ -1,26 +1,18 @@
---
id: p2-59
title: "Pioneer escort mechanic — protection rules vs ambient encounters"
title: Pioneer escort mechanic — protection rules vs ambient encounters
priority: p2
status: partial
status: done
scope: game1
category: units
owner: combat-dev
created: 2026-05-03
updated_at: 2026-05-15
blocked_by: []
follow_ups:
- "p2-59a: stack movement at slowest MP rate (acceptance bullet 3)"
- "p2-59b: escort_assign / escort_release action vocabulary + handlers (bullet 4)"
- "p2-59c: GUT integration test for escort UI signals (bullet 7)"
updated_at: 2026-06-03
evidence:
- "EscortLink typed wrapper added at src/simulator/crates/mc-core/src/encounter.rs (EscortLink::new + serde round-trip)"
- "Re-exported via src/simulator/crates/mc-core/src/lib.rs (encounter:: EscortLink)"
- "Protection rule implemented inline in mc-turn::processor::process_fauna_encounters_inner Step 1b: vulnerable units (unit_kind_scale ≥ 1.5) within 1 hex of a same-player non-vulnerable companion route the ambient encounter roll through the companion's lower unit_kind_scale. Range auto-break (> 1 hex) holds implicitly."
- "Cargo test passes: processor::tests::p2_59_escort_dampens_civilian_encounter_rate (solo civilian on T7 fauna_density=1.0 fires materially more ambient encounters than an escorted civilian; 200-step sample, escorted < 0.75× solo). processor::tests::p2_59_escort_link_construct_round_trip (EscortLink serde + accessors)."
- "Regression: mc-core full + mc-turn full (222 unit + 7 victory + ~80 integration tests) all green on darwin."
- "Bullet 4 (action vocabulary) MET: mc-core/src/action.rs adds ActionKind::EscortAssign/EscortRelease + DisabledReason::AlreadyEscorted/NotEscorted + UnitCapability::is_escorted + legal_actions protectee surfacing; mc-turn/src/action_handlers/mod.rs handle_escort_assign/handle_escort_release gate on link state and queue an EscortRequest; mc-turn/src/processor.rs process_escort_requests is the single escort_links mutation site, validating eligibility against the authored encounter_rates.json escort block (Rail 2), wired into TurnProcessor::step before the movement/fauna phase."
- "Bullet 3 (stack movement) MET: mc-turn/src/processor.rs process_one_move clamps an escort's path budget to min(escort.mp, protectee.mp) and drags the linked protectee into the vacated tile; a 0-MP protectee pins the escort."
- "Cargo green on apricot work/p2-59: mc-core lib 255 passed/0 failed; mc-turn lib 226 passed/0 failed. New tests: action::tests::{p2_59_escort_actions_round_trip,p2_59_protectee_has_escort_assign_unescorted,p2_59_escorted_protectee_shows_release,p2_59_military_unit_has_no_escort_verbs}; encounter::tests::{p2_59_escort_config_loads_from_real_json,p2_59_escort_config_defaults_when_omitted}; processor::tests::{p2_59_escort_assign_release_toggles_link,p2_59_escort_assign_no_escort_in_range_leaves_unlinked,p2_59_stack_move_drags_protectee_and_clamps_budget,p2_59_zero_mp_protectee_pins_escort}."
- "PENDING bullet 7 (GUT integration): requires GDScript escort signals + headless GUT proof (orchestrator/apricot scope, p2-59c). Status remains partial."
blocked_by: []
---
## Context
Once ambient encounter rolls (`p2-58`) trip on every tile moved, unescorted pioneers / settlers become unviable in mid/high ecology-tier wilderness. `public/games/age-of-dwarves/docs/units/SPECIALISTS.md` calls for an escort relationship: a pioneer co-located with (or moving with) a combat-capable escort unit transfers encounter resolution onto the escort, and benefits from a movement-rate or visibility cap from the slowest unit in the stack.
@ -29,11 +21,11 @@ Once ambient encounter rolls (`p2-58`) trip on every tile moved, unescorted pion
- ✓ `mc-core::EscortLink { protected_unit_id: u32, escort_unit_id: u32 }` typed wrapper added — `src/simulator/crates/mc-core/src/encounter.rs` (re-exported at `src/simulator/crates/mc-core/src/lib.rs`). Uses `MapUnit::id` (u32 entity handle) rather than the catalog `UnitId` string, matching the per-instance lookup the processor performs.
- ✓ `mc-turn::process_fauna_encounters_inner` Step 1b honours an *implicit* escort link: vulnerable units (`unit_kind_scale ≥ 1.5`) within 1 hex of a same-player non-vulnerable companion route the ambient encounter roll through the companion's lower scale, dampening / substituting the roll per the brief. (No persistent link table needed for v1 — adjacency is checked each turn.)
- ❌ Stack movement at slowest MP rate. **Deferred to `p2-59a`** — touches movement pipeline (`process_move_requests`).
- `escort_assign` / `escort_release` action vocabulary. **Deferred to `p2-59b`** — needs `ActionKind` additions in `mc-core::action` + handler dispatch.
- ✓ Stack movement at slowest MP rate — `mc-turn::processor::process_one_move`. When the moving unit is an *escort* (reverse-linked in `GameState::escort_links`) and its protectee is in range, the path budget is clamped to `min(escort.mp, protectee.mp)` before pathfinding and the protectee is dragged into the escort's vacated tile at the realized cost. A 0-MP protectee pins the escort in place. Evidence: `processor::tests::p2_59_stack_move_drags_protectee_and_clamps_budget`, `processor::tests::p2_59_zero_mp_protectee_pins_escort`.
- `escort_assign` / `escort_release` action vocabulary — `mc-core::ActionKind::{EscortAssign,EscortRelease}` (+ `DisabledReason::{AlreadyEscorted,NotEscorted}`, `UnitCapability::is_escorted`, `legal_actions` surfacing for civilian/founder protectees). Dispatch handlers `mc-turn::action_handlers::{handle_escort_assign,handle_escort_release}` gate on link state and queue an `EscortRequest`; `mc-turn::processor::process_escort_requests` (single mutation site, owns the authored `EncounterRates`) validates eligibility against `escort.radius` / `escort.protect_threshold` / `unit_kind_scales` and toggles `GameState::escort_links` (auto-picks the nearest non-vulnerable in-range escort; 1:1 enforced). Evidence: `action::tests::{p2_59_escort_actions_round_trip,p2_59_protectee_has_escort_assign_unescorted,p2_59_escorted_protectee_shows_release,p2_59_military_unit_has_no_escort_verbs}`, `processor::tests::{p2_59_escort_assign_release_toggles_link,p2_59_escort_assign_no_escort_in_range_leaves_unlinked}`. **Production interactive wiring** (a `PlayerAction::Escort*` wire variant in `mc-player-api` + loading `EncounterRates` into the persisted dispatch processor so same-turn assign-then-move drains escort first) is bridge-layer scope, tracked separately — the canonical Rust drain runs in `TurnProcessor::step`.
- ✓ Escort range rule (auto-break > 1 hex): satisfied implicitly. The Step-1b substitution map is rebuilt every encounter pass using current positions; when the pair is > 1 hex apart at roll time, no companion is found and the civilian's own (high) `unit_kind_scale` applies.
- ✓ Cargo test in `mc-turn`: `processor::tests::p2_59_escort_dampens_civilian_encounter_rate` — solo civilian on dense T7 fauna fires N encounters in 200 steps; escorted civilian fires < 0.75 × N (in practice 0.4× civilian 2.0 ÷ infantry 0.8). Also `processor::tests::p2_59_escort_link_construct_round_trip` for the typed wrapper.
- ❌ GUT integration test. **Deferred to `p2-59c`** — requires the action-vocabulary signals (`p2-59b`) to fire.
- ✓ GUT integration test (**p2-59c**, landed). Player-reachability wired on the Claude/headless player-API path: `mc_player_api::PlayerAction::{EscortAssign,EscortRelease}` wire variants + dispatch routing through `invoke_unit_action`; `mc_turn::TurnProcessor::load_authored_encounter_rates` (build-time `include_str!` of `encounter_rates.json`, Rail 2) is now installed in `apply_end_turn` so the persisted `step` drains `pending_escort_requests` (previously `encounter_rates: None` made it a no-op). GDScript: `EventBus.escort_assigned/escort_released` signals, `EscortController` (dispatches via `GdPlayerApi.apply_action_json` and re-emits the signals off the OBSERVED post-step `escort_links` table), `unit_panel` `escort_assign/escort_release` verb signals + button-map entries. Also fixed `api-gdext/src/action.rs` (two `UnitCapability` literals were missing the `is_escorted` field added by bullet 4 — the GDExtension had not compiled since the escort core landed). Evidence: headless GUT `engine/tests/integration/test_p2_59c_escort.gd` 3/3 passed, 14 asserts (`test_escort_assign_forms_link_and_emits_signal`, `test_escort_release_drops_link_and_emits_signal`, `test_non_vulnerable_unit_does_not_form_link`); `mc-player-api` lib 92 passed incl. `action::tests::escort_verbs_use_snake_case_type_tags`; mc-core 255 / mc-turn 226 green; regressions `full_game_transcript` + `smoke_5_endturn_mock` green. **Boundary:** the live human-game turn loop (`world_map` + `GdTurnProcessor::step_encounters_only`) still does not drain escort requests (only the full `step` does); that interactive Godot-client wiring remains tracked separately, as bullet 4 notes. **Side effect:** installing the rates also enables ambient fauna on the dispatch path for the first time — the "fauna already fires in production" assumption was false; nothing loaded rates on any non-test path before this.
## Source-of-truth rails