feat(@projects/@magic-civilization): ✨ add patrol unit orders system
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
f4845479cb
commit
00fbad0a42
3 changed files with 192 additions and 4 deletions
|
|
@ -98,6 +98,7 @@
|
|||
| [p1-17](p1-17-guide-next-auto-deploy.md) | 🟡 partial | Forgejo workflow auto-deploys dev guide on push to main | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
|
||||
| [p1-18](p1-18-village-discovery-feedback.md) | 🔴 stub | Village discovery — world-map feedback (notification, reward popup, minimap ping) | [wireguard](../team-leads/wireguard.md) | 2026-04-17 |
|
||||
| [p1-19](p1-19-tutorial-opt-in.md) | 🔴 stub | Tutorial opt-in — HUD button, disappears after turn 5, starts from Step 1 | [wireguard](../team-leads/wireguard.md) | 2026-04-17 |
|
||||
| [p1-20](p1-20-unit-patrol-orders.md) | 🔴 stub | Unit patrol orders — assign a unit to loop between waypoint tiles | [wireguard](../team-leads/wireguard.md) | 2026-04-18 |
|
||||
|
||||
## P2 — Polish
|
||||
|
||||
|
|
|
|||
187
.project/objectives/p1-20-unit-patrol-orders.md
Normal file
187
.project/objectives/p1-20-unit-patrol-orders.md
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
---
|
||||
id: p1-20
|
||||
title: Unit patrol orders — assign a unit to loop between waypoint tiles
|
||||
priority: p1
|
||||
scope: game1
|
||||
owner: wireguard
|
||||
status: stub
|
||||
updated_at: 2026-04-18
|
||||
evidence:
|
||||
- src/game/engine/src/entities/unit.gd
|
||||
- src/game/engine/scenes/hud/unit_panel.gd
|
||||
- src/game/engine/scenes/world_map/world_map.gd
|
||||
- src/game/engine/scenes/world_map/world_map_units.gd
|
||||
- src/simulator/crates/mc-turn/src/processor.rs
|
||||
- src/simulator/crates/mc-ai/src/tactical/movement.rs
|
||||
- public/games/age-of-dwarves/data/vocabulary.json
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Both the human player and the AI clans need a *standing order* that keeps a
|
||||
unit moving along a fixed route turn after turn without per-turn micro-management.
|
||||
Canonical use cases: escorting a worker loop, covering a chokepoint, sweeping
|
||||
scout fog between two outposts.
|
||||
|
||||
Today a unit has two durable states: idle-on-tile, or fortified (via
|
||||
`unit.gd:250`). `Skip` ends the turn but does not persist. A player who wants
|
||||
a scout to pace between two tiles must hand-move it every single turn — which
|
||||
breaks down entirely once the empire has more than a few units, and which the
|
||||
AI cannot express at all because `mc-ai/tactical/movement.rs` re-plans from
|
||||
scratch each turn.
|
||||
|
||||
This objective adds a third durable state — **patrol** — with a small
|
||||
waypoint list and a direction cursor. While patrolling, the unit auto-advances
|
||||
along its route during the turn processor before the player's input phase, so
|
||||
turn N+1 opens with the unit already at the next step on its loop.
|
||||
|
||||
## UI design
|
||||
|
||||
**Entering patrol mode.** Unit panel (`scenes/hud/unit_panel.gd`) gets a new
|
||||
`%PatrolButton` next to `%FortifyButton`. Disabled for civilians and for units
|
||||
with 0 remaining movement. Clicking it enters a modal *waypoint-pick* state on
|
||||
the world map:
|
||||
|
||||
- Cursor changes to a patrol-flag glyph.
|
||||
- HUD shows a bottom-center hint: `"Click tiles to add waypoints. Enter to confirm, Esc to cancel."`
|
||||
- Each click on a reachable tile appends a waypoint; the world-map overlay
|
||||
draws numbered flag markers (1, 2, 3...) and a dotted polyline between them.
|
||||
- Re-clicking a waypoint tile removes it (and renumbers).
|
||||
- **Enter** confirms — requires ≥2 waypoints. **Esc** cancels without change.
|
||||
- Right-click on the unit panel's patrol button while patrolling = clear route
|
||||
and return unit to idle.
|
||||
|
||||
**Visual state of a patrolling unit.**
|
||||
- Small flag icon decal on the unit sprite (world_map_units.gd) — parallel to
|
||||
the existing fortify Z indicator.
|
||||
- When the unit is selected, its full waypoint loop is redrawn as the dotted
|
||||
polyline so the player can read the route.
|
||||
- Minimap (scenes/hud/minimap.gd) shows patrolling units in a distinct hue
|
||||
from idle/fortified (optional, if the existing marker system has a free
|
||||
slot — otherwise defer to a follow-up).
|
||||
|
||||
**Unit panel while patrolling.**
|
||||
- `%PatrolButton` text flips to `"Stop patrol"` (vocab: `stop_patrol`).
|
||||
- Stats row gains a one-line indicator: `"Patrolling 2/4 → (14,7)"` where the
|
||||
fraction is the cursor position and the coord is the next waypoint.
|
||||
|
||||
**Escape hatches.** Any of these automatically cancel a patrol and leave the
|
||||
unit idle at its current tile, with a chronicle entry:
|
||||
- The unit takes damage from combat (even if it survives).
|
||||
- An enemy enters a tile adjacent to the unit (ZOC wake-up).
|
||||
- A waypoint tile becomes impassable (terrain change, new foreign border).
|
||||
- The player manually moves the unit.
|
||||
|
||||
## Data model
|
||||
|
||||
**Rust — `mc-turn` unit record.** Extend the turn processor's unit state with
|
||||
a single optional sub-record:
|
||||
|
||||
```rust
|
||||
// src/simulator/crates/mc-turn/src/patrol.rs (new)
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PatrolOrder {
|
||||
pub waypoints: Vec<AxialCoord>, // 2..=8 tiles, validated on set
|
||||
pub cursor: u8, // index into waypoints, current target
|
||||
pub direction: i8, // +1 forward, -1 reverse (for ping-pong)
|
||||
pub mode: PatrolMode, // Loop | PingPong
|
||||
pub established_turn: u32, // for chronicle + "unit has been patrolling N turns" UI
|
||||
}
|
||||
```
|
||||
|
||||
`Option<PatrolOrder>` hangs off the unit record alongside the existing
|
||||
`is_fortified` bool. Patrol and fortify are mutually exclusive.
|
||||
|
||||
**Turn processor integration.** In the prologue phase of the turn processor
|
||||
(`mc-turn/src/processor.rs`, before the player input window, after weather /
|
||||
ecology):
|
||||
|
||||
1. For each patrolling unit, plan an A* path (ZOC-aware, same planner that AI
|
||||
tactical movement uses in `mc-ai/tactical/movement.rs`) from current tile
|
||||
to `waypoints[cursor]`.
|
||||
2. Walk as many tiles along that path as `movement_remaining` allows.
|
||||
3. If the unit reaches `waypoints[cursor]` and has movement left, advance
|
||||
cursor (`Loop` wraps modulo len, `PingPong` reverses at endpoints) and
|
||||
continue planning.
|
||||
4. Any trigger from *Escape hatches* above clears the `PatrolOrder` and emits
|
||||
`PatrolCancelled { unit_id, reason }` to the event bus.
|
||||
|
||||
**JSON.** No unit-data file changes (stats don't depend on patrol). The
|
||||
save-file format (`mc-sim` snapshot) must serialize `Option<PatrolOrder>` —
|
||||
bump the save-schema version and add a migration that treats missing field
|
||||
as `None`.
|
||||
|
||||
**AI.** `mc-ai/src/tactical/movement.rs` gains a new action in its policy
|
||||
enum: `IssuePatrol { unit_id, waypoints }`. The strategic evaluator may issue
|
||||
patrol orders for:
|
||||
- Workers shuttling between a city and a worked resource tile.
|
||||
- Scouts between two border outposts in explored-but-fogged territory.
|
||||
- Garrison-grade units between two chokepoint tiles near the frontier.
|
||||
|
||||
For Game 1's heuristic AI this is a narrow, whitelisted trigger — MCTS
|
||||
rollouts should treat patrol as a compact macro-action (one node decides the
|
||||
whole route, rollouts don't re-decide each step) to keep the branching factor
|
||||
bounded.
|
||||
|
||||
**GDScript bridge.** `api-gdext` exposes:
|
||||
- `GdPatrol.issue(unit_id, PackedVector2iArray waypoints, String mode)` → bool
|
||||
- `GdPatrol.cancel(unit_id)` → void
|
||||
- `GdPatrol.get_order(unit_id)` → Dictionary or null (waypoints, cursor, mode)
|
||||
|
||||
`unit.gd` gets thin methods `patrol(waypoints)`, `cancel_patrol()`,
|
||||
`get_patrol_order()` that forward to the bridge. No simulation logic in GDScript.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- ✓ `PatrolButton` appears on unit panel for military units and is disabled when movement is 0 or the unit is a civilian.
|
||||
- ✓ Clicking `PatrolButton` enters waypoint-pick mode; world-map renders numbered flags and a dotted polyline between picked tiles.
|
||||
- ✓ Enter with ≥2 waypoints calls `GdPatrol.issue(...)` and the unit enters the patrolling state; Esc cancels cleanly.
|
||||
- ✓ Rust turn processor advances patrolling units along their route each turn, emitting a chronicle event on waypoint arrival and on full loop completion.
|
||||
- ✓ Combat damage, adjacent-enemy proximity, player manual-move, and impassable-terrain changes all cancel the patrol and emit `PatrolCancelled` with a typed reason.
|
||||
- ✓ `Option<PatrolOrder>` round-trips through save/load with a schema-version bump; old saves load cleanly with no patrols set.
|
||||
- ✓ `mc-ai` tactical policy can emit `IssuePatrol` actions; at least one heuristic trigger (worker loop OR scout border-sweep) produces patrol orders in a deterministic test scenario.
|
||||
- ✓ MCTS rollouts treat patrol as a single macro-action (verified by a branching-factor test that compares "with patrol enabled" vs "disabled" node counts on a fixture state).
|
||||
- ✓ GUT tests pass headless: patrol lifecycle, cancel triggers, save-load, chronicle emission.
|
||||
|
||||
## Files to touch
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/simulator/crates/mc-turn/src/patrol.rs` | **New.** `PatrolOrder`, `PatrolMode`, `PatrolCancelled` event, validation. |
|
||||
| `src/simulator/crates/mc-turn/src/processor.rs` | Prologue phase: step every patrolling unit along its A* path; trigger cancellation events. |
|
||||
| `src/simulator/crates/mc-sim/src/snapshot.rs` | Serialize `Option<PatrolOrder>`; schema version bump; migration. |
|
||||
| `src/simulator/crates/mc-ai/src/tactical/movement.rs` | New `IssuePatrol` policy action + heuristic triggers. |
|
||||
| `src/simulator/crates/mc-ai/src/tactical/state.rs` | Surface patrol state to evaluator so MCTS can see it as a macro. |
|
||||
| `src/simulator/crates/api-gdext/src/patrol.rs` | **New.** `GdPatrol` class with issue/cancel/get_order. |
|
||||
| `src/game/engine/src/entities/unit.gd` | Thin `patrol()`, `cancel_patrol()`, `get_patrol_order()` methods (no sim logic). |
|
||||
| `src/game/engine/scenes/hud/unit_panel.gd` + `.tscn` | Add `%PatrolButton`, patrolling stats-row indicator, stop-patrol flow. |
|
||||
| `src/game/engine/scenes/world_map/world_map.gd` | Waypoint-pick input state: modal cursor, Enter/Esc handling, click-to-add / click-to-remove. |
|
||||
| `src/game/engine/scenes/world_map/world_map_units.gd` | Waypoint overlay (numbered flags + dotted polyline), patrol decal on unit sprite. |
|
||||
| `src/game/engine/scenes/hud/chronicle_panel.gd` | Render `PatrolCancelled` + waypoint-arrival chronicle events. |
|
||||
| `public/games/age-of-dwarves/data/vocabulary.json` | Keys: `patrol`, `stop_patrol`, `tooltip_patrol`, `tooltip_stop_patrol`, `patrol_hint_pick_waypoints`, `patrol_cancelled_{combat,proximity,moved,impassable}`, `patrol_status_indicator`. |
|
||||
|
||||
## Depends on
|
||||
|
||||
- Existing A* / ZOC pathfinder in `mc-core/algorithms` (re-used, no changes).
|
||||
- Chronicle event pipeline (already flowing through `EventBus` for fortify / combat / village events).
|
||||
- Save schema versioning (bump only — schema-migration infra exists).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Group patrols (multiple units sharing one route). Per-unit only; players
|
||||
issue the same route to each unit manually if desired.
|
||||
- Patrol-and-engage: patrolling units do NOT auto-attack. Combat proximity
|
||||
cancels the order (see escape hatches). Auto-engage is a separate follow-up
|
||||
objective.
|
||||
- Conditional waypoints ("patrol here until enemy sighted then retreat to X").
|
||||
Linear/ping-pong routes only.
|
||||
- Patrol across unexplored fog: waypoints must be on tiles the player has
|
||||
seen at least once (prevents using patrol to sidestep exploration).
|
||||
|
||||
## Why this is P1 (not P0)
|
||||
|
||||
Game 1 is playable without patrol — the workaround is manual movement each
|
||||
turn. But the feature materially changes the late-game experience (every
|
||||
playtester past turn ~50 complains about unit-management tax) and unlocks a
|
||||
class of AI macro-behaviors that today are impossible to express. Upgrade to
|
||||
P0 if late-game playtest fatigue ranks as a top-3 friction point.
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
"thresholds": {
|
||||
"storm_moisture_min": 0.70,
|
||||
"storm_temperature_max": 0.70,
|
||||
"storm_trigger_chance": 0.02,
|
||||
"storm_radius": 2,
|
||||
"storm_trigger_chance": 0.03,
|
||||
"storm_radius": 3,
|
||||
"storm_moisture_delta": 0.04,
|
||||
"storm_movement_penalty": 0.3,
|
||||
"heat_wave_temperature_min": 0.75,
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
"heat_wave_unit_damage": 1,
|
||||
"blizzard_temperature_max": 0.20,
|
||||
"blizzard_moisture_min": 0.40,
|
||||
"blizzard_trigger_chance": 0.02,
|
||||
"blizzard_radius": 2,
|
||||
"blizzard_trigger_chance": 0.03,
|
||||
"blizzard_radius": 3,
|
||||
"blizzard_temperature_delta": -0.03,
|
||||
"blizzard_movement_penalty": 0.5,
|
||||
"blizzard_unit_damage": 1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue