You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
87 lines
5.2 KiB
Markdown
87 lines
5.2 KiB
Markdown
# Excursion FSM: attractor movement (roadside + adventure)
|
|
|
|
This document describes the **server-authoritative** mini-excursion flow: the hero moves in **world coordinates** toward successive **attractors** instead of using a time-based perpendicular offset from the road spine.
|
|
|
|
## Terminology
|
|
|
|
| Name | `ExcursionPhase` | Meaning |
|
|
|------|------------------|---------|
|
|
| First exit into forest | `out` | Walk from the frozen road point to the first forest attractor. |
|
|
| Wilderness | `wild` | Heal / wander / encounters (depends on `ExcursionKind`). |
|
|
| Return leg | `return` | Walk back to the road (adventure) or to saved `StartX/Y` (roadside). |
|
|
|
|
Product language sometimes calls the return leg “out”; in code it is always **`return`**.
|
|
|
|
## Session kinds (`ExcursionKind`)
|
|
|
|
| Kind | Trigger | `HeroMovement` state |
|
|
|------|---------|----------------------|
|
|
| `roadside` | Low HP on road → `beginRoadsideRest` | `StateResting`, `RestKindRoadside` |
|
|
| `adventure` | Random roll while walking → `beginExcursion` | `StateWalking` with active `Excursion` |
|
|
| `town` | In-town tour (separate sub-FSM) | `StateInTown` |
|
|
|
|
Roadside and adventure share attractor stepping helpers; town uses its own tour phases (`TownTourPhase` in `model/excursion.go`).
|
|
|
|
## Kinematics
|
|
|
|
- **`CurrentX` / `CurrentY`** are the true world position during `out` / `wild` / `return`.
|
|
- Movement uses `stepTowardAttractor` (excursion speed from `refreshSpeed`) or `stepTowardWorldPoint` for town NPC/center walks, with arrival epsilon `ExcursionArrivalEpsilonWorld` (`tuning`).
|
|
- For attractor-based excursions, `displayOffset` is zero; `hero.position` / `hero_move` match world coords.
|
|
- **Legacy** JSON blobs without `excursion.kind` but with a non-empty phase are cleared on load (`applyExcursionFromBlob`) so old offset-only sessions are not resumed.
|
|
|
|
## Roadside (`roadside`)
|
|
|
|
1. **Start:** `StartX/Y` = road position; road progress frozen (`RoadFreezeWaypoint` / `RoadFreezeFraction`); first forest attractor from `pickExcursionForestAttractor` (depth from tuning).
|
|
2. **`out`:** Step toward attractor until within epsilon → **`wild`**.
|
|
3. **`wild`:** Regen `RoadsideRestHpPerS`; cap by random duration `[RoadsideRestMinMs, RoadsideRestMaxMs]` or early exit when `HP/MaxHP ≥ RoadsideRestExitHp` (default **0.85**).
|
|
4. **`return`:** Attractor = `StartX/Y`; on arrival → `endExcursion`, restore road progress, clear rest.
|
|
|
|
Persisted under `heroes.town_pause` → `excursion` (`ExcursionPersisted`).
|
|
|
|
## Adventure (`adventure`)
|
|
|
|
1. **Start:** `StartX/Y` on road; `AdventureEndsAt = now + uniform[AdventureDurationMinMs, AdventureDurationMaxMs]`; first `out` attractor like roadside (depth `AdventureDepthWorldUnits`).
|
|
2. **`out`:** Reach attractor → **`wild`**, schedule wander (`WanderNextAt`, `adventurePickWanderAttractor` within `AdventureWanderRadius`).
|
|
3. **`wild`:** While `now < AdventureEndsAt`: step toward current attractor; retarget on `WanderNextAt`; roll encounters; if `HP/MaxHP < LowHpThreshold` → `beginAdventureInlineRest` until `≥ AdventureRestTargetHp` (default **0.85**), then back to `wild`.
|
|
4. **Timer elapsed:** `tryBeginAdventureReturn`: if fighting, set `PendingReturnAfterCombat`; else `enterAdventureReturnToRoad` (attractor = closest point on **frozen** road polyline).
|
|
5. **After combat win:** `ResumeWalking` then `TryAdventureReturnAfterCombat(now)` — also handles timer elapsed while movement ticks were skipped during combat (checks `AdventureEndsAt`, not only the pending flag).
|
|
6. **`return`:** On arrival at road attractor → `endExcursion`, `excursion_end` WS when applicable.
|
|
|
|
## Persistence
|
|
|
|
- `TownPausePersisted.Excursion` holds kind, phase, freeze snapshot, `startX/Y`, attractor, `adventureEndsAt`, `wanderNextAt`, `pendingReturnAfterCombat`, etc.
|
|
- Engine persists when `TownPausePersistDue` signature changes (see `townPausePersistSignature` in `movement.go`).
|
|
|
|
## WebSocket (online)
|
|
|
|
Typical messages: `excursion_start`, `excursion_phase`, `hero_move`, `hero_state`, `excursion_end`. After admin “force return” on adventure, engine sends `excursion_phase` + `hero_move` (not immediate `excursion_end`).
|
|
|
|
## Tuning keys (reference)
|
|
|
|
| Key | Role |
|
|
|-----|------|
|
|
| `AdventureDurationMinMs` / `MaxMs` | Adventure `wild` window |
|
|
| `AdventureWanderRadius` | Random retarget radius around hero |
|
|
| `AdventureWanderRetargetMinMs` / `MaxMs` | Retarget interval |
|
|
| `ExcursionArrivalEpsilonWorld` | Arrival threshold (shared with town step-to-point) |
|
|
| `RoadsideRestExitHp` | Early end of roadside `wild` |
|
|
| `AdventureRestTargetHp` | End of adventure inline heal |
|
|
|
|
## Client
|
|
|
|
- `excursionPhase` and `excursionKind` (`roadside` \| `adventure`) on hero JSON; visuals follow `hero_move` world coordinates.
|
|
|
|
## Primary source files
|
|
|
|
| Area | File |
|
|
|------|------|
|
|
| Session model + persist DTO | `backend/internal/model/excursion.go` |
|
|
| FSM, stepping, persist blob | `backend/internal/game/movement.go` |
|
|
| Post-combat return | `backend/internal/game/engine.go`, `offline.go` |
|
|
| Defaults | `backend/internal/tuning/runtime.go` |
|
|
|
|
## Related docs
|
|
|
|
- [engine_unified_offline_online.md](./engine_unified_offline_online.md) — single simulation path.
|
|
- [spec-server-authoritative.md](./spec-server-authoritative.md) — WS envelopes and authority boundaries.
|