Add the first-shift tutorial design and state machine
Full build / Import + headless tests (push) Successful in 38s
Full build / Full desktop build + optional release (push) Skipped

Write the eight-beat tutorial sheet, its safe visitor script, persistence and
replay rules. Add a deterministic, skippable/restartable Tutorial RefCounted
state machine with headless tests. UI/GameManager wiring remains the next task.
Headless 235/235, Python 94/94.
This commit is contained in:
zwitschi committed 2026-11-01 20:19:18 +01:00
1 parent 806fe78688
commit f4a53e2d4f
8 files changed
+225 -5

No files matched your search

+1
View File
@@ -42,6 +42,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
- Shift length is the scenario's `shift_duration_s` (900 s in all three venues). Tests and the screenshot tool override elapsed time; they do not change the default.
- RNG: NPC reactions and event scheduling are seedable for deterministic tests.
- **Tutorial foundation (0.8.0 work):** `docs/tutorial.md` is the eight-beat design; `src/game/tutorial.gd` is a pure `RefCounted` state machine with skip/restart and tests. It is not yet wired into `GameManager`; when wiring it, add an explicit QA bypass because captures/smokes would otherwise see prompts.
- **Documentation ownership (2026-11-01):** `docs/architecture.md` §12 defines the source-of-truth map and consolidation rules. Obsolete/historical plan documents are removed rather than kept; current work belongs in `TODO.md`. README/development-status/Blender AO/Forward+ stale claims were corrected. The v1.0.0 gap register and 0.8/0.9/0.9.5/1.0 roadmap live in `TODO.md`.
- **Vision judge (v0.7.2):** `tools/openrouter_judge.py` reads `/api/v1/models`, filters image-input models with `response_format`, and ranks by advertised prompt/image/completion prices. `--model auto` (default) uses the cheapest (`z-ai/glm-5.3-flash` on 2026-11-01) with fallbacks; explicit `--model`/`GIG_LINE_JUDGE_MODEL` wins. Auth errors abort, provider/model errors fall through. Full screen judge 13/13 match, 0 diff; A/B judge ~$0.006.
- **Desktop HQ preset (v0.7.1):** `packaging/desktop_hq/override.cfg` switches a
+1
View File
@@ -108,6 +108,7 @@ Full setup, every run/test/smoke command, and the controls table are in
- [`CHANGELOG.md`](CHANGELOG.md) — User-facing changes by version.
- [`docs/architecture.md`](docs/architecture.md) — arc42-style architecture documentation.
- [`docs/gameplay.md`](docs/gameplay.md) — Gameplay systems, venues, and the escalation model.
- [`docs/tutorial.md`](docs/tutorial.md) — First-shift tutorial beats and rules.
- [`docs/asset-tooling.md`](docs/asset-tooling.md) — Asset pipelines, model tools, and screenshot gates.
- [`docs/getting-started.md`](docs/getting-started.md) — Setup, run/test commands, and controls.
- [`docs/development-status.md`](docs/development-status.md) — Implemented features and what's next.
+7 -4
View File
@@ -54,11 +54,14 @@ venues, and see enough authored variety that a second shift does not feel like a
### 0.8.1 First-shift tutorial
- [ ] Write the tutorial beat sheet: menu → movement/look → focus and target cycling → action
- [x] Write the tutorial beat sheet: menu → movement/look → focus and target cycling → action
wheel → access inspection → polite direction → formal warning/body camera → radio →
shift review. Keep it to 6–8 beats and one safe, compliant NPC.
- [ ] Add a tutorial state machine independent of the normal shift FSM. It must be skippable,
restartable and deterministic, and must not change the outcome of the live shift.
shift review. Eight beats and a safe compliant visitor are specified in
[`docs/tutorial.md`](docs/tutorial.md).
- [x] Add a tutorial state machine independent of the normal shift FSM. `Tutorial` in
`src/game/tutorial.gd` owns the eight beats, accepts only the current beat's action,
supports skip/restart and has deterministic headless tests. GameManager/UI wiring starts
with the contextual-prompt task.
- [ ] Add contextual prompts that advance on the performed action, not on a timer. Prompts must
not block normal input or repeat after the player has learned the control.
- [ ] Stage a safe encounter with one compliant visitor; no emergency, threat or crowd panic in
+2 -1
View File
@@ -5,7 +5,7 @@ Implemented so far:
- Godot 4.3 project scaffold, input map, procedural world builder
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
- Three authored scenarios (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
- Headless unit tests: **231 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
- Headless unit tests: **235 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
- Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event
- First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review
- Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`)
@@ -41,6 +41,7 @@ Implemented so far:
- Visual alignment: start/venue menu, pause, settings, shift review, radio, wand, inspection, timeline, HUD target chip/escalation bar, and radial menu now use or match their AI references in `assets/images/ui/`. The runtime art loader (`src/game/ui/art.gd`) falls back cleanly when a file is missing.
- Realism adoption (post-v0.6.0): v2 per-phase skylines and surface tiles with normal maps, retuned sun/exposure/fog, wet asphalt, denser foliage, a visible additive flashlight beam, and refreshed staging captures. `tools/check_screenshots.sh` is the repeatable capture/compare gate; judge is clean of `diff` verdicts.
- Input/reliability fixes: selecting a venue now releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status.
- Tutorial foundation (0.8.0 work): the eight-beat first-shift design and a deterministic, skippable/restartable `Tutorial` state machine exist; the in-game prompt/visitor wiring is the next step.
- Vision judge (v0.7.2): discovers the cheapest current OpenRouter image+JSON model, reports the model/fallbacks used, and clears the full screen-design judge (13/13 match, 0 diff).
- Desktop HQ (v0.7.1): optional Forward+ Windows/Linux zip with volumetric fog, SDFGI and a volumetric flashlight; the default Compatibility/Web builds and headless tests gate the effect off. A/B captures are generated locally under `docs/screens/realism/` (gitignored).
- Venue geometry (v0.7.0): the Joke Shop now builds as a narrow brick alley with a warm wall lantern, shutters, dumpster/AC clutter and no skyline; the TV Studio builds a stone facade, bulb canopy, glossy black car and barrier line. `WorldBuilder.VENUE_SPECS` drives facade/street/nav dimensions, and `GameManager` rebuilds only the world container when the venue changes.
+2
View File
@@ -16,6 +16,8 @@ A typical shift lasts 10–15 minutes. The player:
The game rewards calm resolutions rather than aggressive behavior. Emergency assistance and armed responders are rare last-resort systems with meaningful consequences.
The first shift starts with an in-game tutorial covering movement/look, focus cycling, the action wheel, access inspection, polite direction, formal warning/body camera, radio and shift review. Its beat sheet is in [`tutorial.md`](tutorial.md).
## Venues
### Hollywood Boulevard Building
+45
View File
@@ -0,0 +1,45 @@
# First-shift tutorial
Status: planned for v0.8.0. The tutorial teaches the real controls and the real
inspection/wheel/radio panels; it is not a fake UI or a separate combat mode.
## Rules
- One short, safe, compliant visitor. No emergency, threat, crowd panic or random
event can interrupt the tutorial.
- Prompts advance on the performed action, not on a timer. The player can keep
moving/looking while a prompt is visible.
- The tutorial is skippable at any point. Completion and skip are both persisted
separately; a skipped tutorial can be replayed from Help/Settings.
- The tutorial state machine is independent of the shift FSM. It observes the
same actions as normal play and must be deterministic for tests.
## Beat sheet
| # | Beat | Prompt | Advances when | Notes |
| --- | --- | --- | --- | --- |
| 1 | Arrive | "Walk forward and look around. The entrance is behind you." | The guard has moved at least 1 m and looked away/returned | Start at the normal guard post, facing the street. |
| 2 | Focus | "Look at the visitor. Press Tab or the mouse wheel to cycle targets." | A target is acquired and explicitly cycled | The visitor waits at the foot of the steps; no other guest is required. |
| 3 | Wheel | "Open the action wheel with Space or left click." | The radial wheel opens on the visitor | The wheel is the real de-escalation-first wheel. |
| 4 | Inspect | "Choose Inspect and read the access evidence." | The inspection panel is opened and the policy result is visible | Use a valid wristband so the tutorial can teach both evidence and a safe admit/deny decision. |
| 5 | Direction | "Close the panel and give a polite direction." | A Level 1 polite-direction action is applied | The visitor complies; there is no escalation requirement. |
| 6 | Warning | "Give a formal warning if the visitor persists. It arms the body camera." | A Level 2 warning is applied or the body camera is switched on | Show the REC overlay and the escalation cue. |
| 7 | Radio | "Open the assistance radio and submit a short report." | The assistance report is submitted successfully | A non-emergency assistance call; no responder is required to resolve the tutorial. |
| 8 | Review | "Finish the shift and review what happened." | The shift review opens | The tutorial ends; the real review remains available for inspection. |
## Visitor script
- Display name: `Visitor`.
- Intent: `enter` (approaches the door, then waits for interaction).
- Access: valid wristband evidence; the policy result should be unambiguous.
- Behavior: compliant at every escalation level; never becomes a threat, never
joins a crowd and never triggers an emergency.
- Failure handling: if the player denies admission incorrectly, the tutorial
explains the evidence but does not fail the run; the visitor remains safe and
the review records the decision.
## Persistence and replay
Settings gain `tutorial_completed` and `tutorial_skipped`. A first launch starts
the tutorial after venue selection. Skipping sets `tutorial_skipped`; Settings/
Help offers "Replay tutorial", which clears both flags for the next shift.
+118
View File
@@ -0,0 +1,118 @@
class_name Tutorial
extends RefCounted
## Deterministic first-shift tutorial state machine. It observes player actions
## but owns no UI or simulation nodes, so it can be tested headlessly and wired
## into GameManager without changing the shift FSM. `advance()` only accepts the
## event for the current beat; wrong actions are ignored.
signal beat_changed(index: int, beat: Dictionary)
signal completed(skipped: bool)
const BEATS := [
{
"id": "arrive",
"prompt": "Walk forward and look around. The entrance is behind you.",
"event": "moved",
},
{
"id": "focus",
"prompt": "Look at the visitor. Press Tab or the mouse wheel to cycle targets.",
"event": "cycled",
},
{
"id": "wheel",
"prompt": "Open the action wheel with Space or left click.",
"event": "wheel_opened",
},
{
"id": "inspect",
"prompt": "Choose Inspect and read the access evidence.",
"event": "inspected",
},
{
"id": "direction",
"prompt": "Close the panel and give a polite direction.",
"event": "polite_direction",
},
{
"id": "warning",
"prompt": "Give a formal warning if the visitor persists. It arms the body camera.",
"event": "formal_warning",
"alternate_event": "body_camera",
},
{
"id": "radio",
"prompt": "Open the assistance radio and submit a short report.",
"event": "radio_submitted",
},
{
"id": "review",
"prompt": "Finish the shift and review what happened.",
"event": "review_opened",
},
]
var index := -1
var complete := false
var skipped := false
func start() -> Dictionary:
index = 0
complete = false
skipped = false
var beat: Dictionary = BEATS[0]
beat_changed.emit(index, beat)
return beat
func current() -> Dictionary:
if index < 0 or index >= BEATS.size() or complete:
return {}
return BEATS[index]
func prompt() -> String:
return str(current().get("prompt", ""))
func advance(event: String) -> bool:
if complete:
return false
if index < 0:
start()
if index >= BEATS.size():
return false
var beat: Dictionary = BEATS[index]
var accepted: Array = [str(beat.get("event", ""))]
if beat.has("alternate_event"):
accepted.append(str(beat["alternate_event"]))
if event not in accepted:
return false
index += 1
if index >= BEATS.size():
complete = true
index = BEATS.size()
completed.emit(false)
else:
beat_changed.emit(index, BEATS[index])
return true
## Skip is recorded separately so Settings/Help can offer "Replay tutorial".
func skip() -> void:
if complete:
return
skipped = true
complete = true
index = BEATS.size()
completed.emit(true)
func restart() -> Dictionary:
return start()
func is_complete() -> bool:
return complete
func is_skipped() -> bool:
return skipped
## (current beat number, total beats); one-based for display, (total, total)
## after completion.
func progress() -> Vector2i:
return Vector2i(clampi(index + 1, 1, BEATS.size()), BEATS.size())
+49
View File
@@ -0,0 +1,49 @@
extends RefCounted
## Deterministic headless tests for the first-shift tutorial state machine.
const Tutorial := preload("res://src/game/tutorial.gd")
func test_tutorial_advances_only_on_the_expected_action() -> bool:
var tutorial := Tutorial.new()
tutorial.start()
var a := TestUtils.check(tutorial.current()["id"] == "arrive", "starts at arrival")
var b := TestUtils.check(not tutorial.advance("wheel_opened"), "wrong action is ignored")
var c := TestUtils.check(tutorial.advance("moved"), "movement advances")
var d := TestUtils.check(tutorial.current()["id"] == "focus", "moves to focus")
var e := TestUtils.check(tutorial.advance("cycled"), "target cycling advances")
var f := TestUtils.check(tutorial.current()["id"] == "wheel", "moves to the action wheel")
return a and b and c and d and e and f
func test_tutorial_runs_all_beats_and_completes() -> bool:
var tutorial := Tutorial.new()
tutorial.start()
var events := ["moved", "cycled", "wheel_opened", "inspected",
"polite_direction", "formal_warning", "radio_submitted", "review_opened"]
var ok := true
for event in events:
ok = ok and tutorial.advance(event)
ok = ok and tutorial.is_complete()
ok = ok and not tutorial.is_skipped()
ok = ok and tutorial.progress() == Vector2i(Tutorial.BEATS.size(), Tutorial.BEATS.size())
return TestUtils.check(ok, "all eight beats complete in order")
func test_tutorial_accepts_body_camera_for_warning() -> bool:
var tutorial := Tutorial.new()
tutorial.start()
for event in ["moved", "cycled", "wheel_opened", "inspected", "polite_direction"]:
tutorial.advance(event)
var a := TestUtils.check(tutorial.advance("body_camera"), "body camera satisfies warning")
var b := TestUtils.check(tutorial.current()["id"] == "radio", "advances to radio")
return a and b
func test_tutorial_skip_and_restart() -> bool:
var tutorial := Tutorial.new()
tutorial.start()
var a := TestUtils.check(tutorial.advance("moved"), "one beat completed")
tutorial.skip()
var b := TestUtils.check(tutorial.is_complete() and tutorial.is_skipped(), "skip completes tutorial")
var c := TestUtils.check(not tutorial.advance("moved"), "completed tutorial is inert")
tutorial.restart()
var d := TestUtils.check(not tutorial.is_complete() and not tutorial.is_skipped(), "restart clears flags")
var e := TestUtils.check(tutorial.current()["id"] == "arrive", "restart returns to beat one")
return a and b and c and d and e