diff --git a/MEMORY.md b/MEMORY.md index 013203c..de19340 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -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 diff --git a/README.md b/README.md index 8dddcb0..d5bfd24 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/TODO.md b/TODO.md index 50fb226..b27f299 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/docs/development-status.md b/docs/development-status.md index dc4025d..354937f 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -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. diff --git a/docs/gameplay.md b/docs/gameplay.md index 9ce3a01..c5b7329 100644 --- a/docs/gameplay.md +++ b/docs/gameplay.md @@ -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 diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..2ef1515 --- /dev/null +++ b/docs/tutorial.md @@ -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. diff --git a/src/game/tutorial.gd b/src/game/tutorial.gd new file mode 100644 index 0000000..8717eef --- /dev/null +++ b/src/game/tutorial.gd @@ -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()) diff --git a/src/test/tests/test_tutorial.gd b/src/test/tests/test_tutorial.gd new file mode 100644 index 0000000..a3d72c8 --- /dev/null +++ b/src/test/tests/test_tutorial.gd @@ -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