Compare commits

...
4 Commits
Author SHA1 Message Date
zwitschi 76eb5e3a79 Fix mouse look swallowing by the HUD
Full build / Import + headless tests (push) Successful in 35s
Full build / Full desktop build + optional release (push) Successful in 4m39s
Move guard look handling to the _input stage, recapture the cursor on
window-focus return and when clicking back into gameplay, and make HUD controls
mouse-transparent. Add regression tests for yaw/pitch and HUD mouse filters.
Verified under Xvfb with real relative mouse motion. Headless 237/237, Python
94/94, movement smoke 60 fps.
2026-11-01 20:38:36 +01:00
zwitschi f4a53e2d4f Add the first-shift tutorial design and state machine
Full build / Full desktop build + optional release (push) Skipped
Full build / Import + headless tests (push) Successful in 38s
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.
2026-11-01 20:19:18 +01:00
zwitschi 806fe78688 Clarify nested TODO completion rules
Full build / Full desktop build + optional release (push) Skipped
Full build / Import + headless tests (push) Successful in 37s
Describe level-2 milestones/themes, level-3 workstreams and the rule that
completed workstreams stay under their parent until the whole milestone is
done. Add a milestone-shaped recommended format and align the changelog/tag
steps with nested sections.
2026-11-01 20:16:23 +01:00
zwitschi 6a3066d763 Clarify nested task grouping in the agent checklist format 2026-11-01 20:14:53 +01:00
16 changed files with 362 additions and 34 deletions

No files matched your search

+41 -16
View File
@@ -44,7 +44,13 @@ Do not assume that a task is unimplemented without checking the code and documen
All planned implementation work must be represented in `TODO.md`.
- Use headings to group related work into sections.
- Use checkbox items for each concrete task.
- Use level-2 headings (`##`) for milestones or stable themes. A milestone should state its goal
and an explicit exit criterion; a theme groups small related work that does not need a
milestone.
- Use level-3 headings (`###`) for workstreams inside a milestone (for example, `0.8.1
First-shift tutorial`). Keep the hierarchy to three levels: milestone → workstream → checkbox.
- Use checkbox items for each concrete task, normally under a level-3 workstream. Direct
checkboxes under a level-2 section are acceptable for small, single-workstream sections.
- Keep tasks small enough to complete and review independently.
- Add new tasks when discoveries expand the scope.
- When starting a task, claim it for the agent workflow by changing `[ ]` to `[-]`. Keep it `[-]`
@@ -61,11 +67,20 @@ Recommended format:
```md
# TODO
## Core Gameplay
## Milestone 0.8.0 — First hour and content depth
- [ ] Add player movement controls.
- [-] Implement NPC approach behavior. (claimed; implementation/verification in progress)
- [x] Define escalation levels.
### 0.8.1 First-shift tutorial
- [x] Write the tutorial beat sheet.
- [-] Implement the tutorial state machine. (claimed; implementation/verification in progress)
- [ ] Add the tutorial smoke test.
### 0.8.2 Campaign shell
- [ ] Define campaign data.
- [ ] Persist campaign progress.
**Exit:** a first-time player completes a shift without external documentation.
## Documentation
@@ -74,16 +89,26 @@ Recommended format:
### 3. Move Completed Sections to `DONE.md`
When every task in a `TODO.md` section is complete:
`TODO.md` uses nested sections. Apply these completion rules:
1. Verify the work and update the checkbox items to `[x]`.
2. Move the entire completed section, including its heading and checklist, to `DONE.md`.
3. Remove that section from `TODO.md`.
4. Add a short completion note or date when useful.
1. **Level-3 workstream:** verify the work, mark every checkbox `[x]`, and add a short completion
note. Leave the completed workstream under its parent milestone; do not move an isolated
level-3 section to `DONE.md`.
2. **Level-2 milestone/theme:** it is complete only when its direct checkboxes and **all** of its
level-3 workstreams are complete. Verify the whole milestone, record its capabilities in
`docs/development-status.md` when it represents shipped functionality, and write the matching
`CHANGELOG.md` entry.
3. Move the **entire level-2 section**, including all level-3 headings and checklists, to
`DONE.md`. Remove it from `TODO.md` and add a completion date/note.
4. **Planning/overview sections** such as a gap analysis, roadmap overview, dependencies or
deferred ideas are not completion milestones. Do not move them merely because their checkboxes
are empty or complete; update or supersede them as the roadmap changes.
5. Do not move partially completed milestones. Leave them in `TODO.md` until every nested task is
complete, or split the unfinished work into a new smaller level-2 milestone first.
Do not move partially completed sections. Leave them in `TODO.md` until all of their tasks are complete, or split the section into smaller sections first.
When a completed section represents shipped functionality, also record the new capability in `docs/development-status.md` so the implemented-features list stays accurate, and write a matching entry in `CHANGELOG.md` (see "Maintain `CHANGELOG.md`" below).
When a completed milestone represents shipped functionality, also record the new capability in
`docs/development-status.md` so the implemented-features list stays accurate, and write a matching
entry in `CHANGELOG.md` (see "Maintain `CHANGELOG.md`" below).
### 4. Maintain `MEMORY.md`
@@ -154,7 +179,7 @@ The root-level `CHANGELOG.md` records the user-facing changes for every release.
- Describe changes in user-facing terms, not as raw commit messages.
- Add the entry when its work ships. If the version number is not yet known, record it under `Unreleased` and move it into a numbered version when the version is tagged.
When a full level-2 section in `TODO.md` is implemented, completed, and moved to `DONE.md`:
When a full executable level-2 milestone/theme in `TODO.md` is implemented, completed, and moved to `DONE.md`:
1. Verify the work and mark every checkbox in the section `[x]`.
2. Use the section heading and its completed items to describe the changes and write a `CHANGELOG.md` entry under the next appropriate version.
@@ -172,9 +197,9 @@ For each task:
4. Run the relevant tests, checks, or manual verification.
5. Update documentation if the change creates lasting context: add features to `docs/development-status.md`, adjust the README links or other `docs/` files as needed, and record decisions/gotchas in `MEMORY.md`.
6. Mark the task complete.
7. Move its section to `DONE.md` when the entire section is complete (after recording its `CHANGELOG.md` entry).
7. Keep completed level-3 workstreams in place until the parent milestone is complete; then move the entire level-2 milestone to `DONE.md` after recording its `CHANGELOG.md` entry.
8. Commit the changes to the version control system with a descriptive message.
9. If a section from `TODO.md` has been fully completed, ensure its `CHANGELOG.md` entry is in place, then create a new version or tag in the version control system to mark the milestone.
9. If a level-2 milestone from `TODO.md` has been fully completed, ensure its `CHANGELOG.md` entry is in place, then create a new version or tag in the version control system to mark the milestone.
10. Push the committed changes to the shared repository if applicable.
11. Note the new version or tag created in the version control system in any relevant documentation or handoff summary.
+11
View File
@@ -4,6 +4,17 @@ All notable user-facing changes to Gig Line. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use
Semantic Versioning.
## [0.7.3] — 2026-11-01
### Fixed
- Mouse look now reads relative motion at the `_input` stage, so a full-screen
HUD or focused control cannot swallow it while the cursor is captured.
- Mouse capture is restored after window/compositor focus returns, and clicking
back into a gameplay window recaptures it instead of opening the radial menu.
- The HUD is mouse-transparent; its presentation controls no longer block
gameplay mouse input.
## [0.7.2] — 2026-11-01
### Added
+2
View File
@@ -42,6 +42,8 @@ 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.
- **Mouse look (v0.7.3):** guard look is handled in `GuardController._input()` (before GUI), not `_unhandled_input`; focus/click-back recaptures the cursor and the HUD is made mouse-transparent recursively. Regression tests cover yaw/pitch and HUD filters.
- **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 -1
View File
@@ -172,7 +172,7 @@ Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [AD
## Versioning and Delivery
`config/version` in `project.godot` is the single version source (currently `0.7.2`). Releases are tagged `v<version>`; the CI pipeline builds and publishes the standard artifacts plus the optional Desktop HQ zip. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands.
`config/version` in `project.godot` is the single version source (currently `0.7.3`). Releases are tagged `v<version>`; the CI pipeline builds and publishes the standard artifacts plus the optional Desktop HQ zip. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands.
## Design Direction
+4 -3
View File
@@ -38,9 +38,9 @@ precision walk, the Joke Shop alley, the TV Studio arrival, and the prop lineup.
> Downloads are public (no sign-in required) since the host's instance-wide
> `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31.
>
> **Latest: v0.7.2** — a cost-aware OpenRouter screenshot judge, an optional
> Forward+ Desktop HQ build with volumetric fog/SDFGI/flashlight, distinct Joke
> Shop and TV Studio geometry, and fitted NPC gear.
> **Latest: v0.7.3** — mouse-look input reliability fixes, plus the v0.7.2
> cost-aware OpenRouter judge, optional Forward+ Desktop HQ build, distinct
> venues and fitted NPC gear.
> Releases are built automatically when a `vX.Y.Z` tag is pushed
> (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use
> `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline
@@ -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
+1 -1
View File
@@ -37,7 +37,7 @@ This document follows the structure of the arc42 architecture documentation temp
- **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible.
- **PATCH** — bug fixes and small polish that do not change behavior.
The single source of truth for the version is `config/version` in `project.godot` (currently `0.7.2`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md).
The single source of truth for the version is `config/version` in `project.godot` (currently `0.7.3`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md).
## 3. Context and Scope
+3 -2
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: **237 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`)
@@ -40,7 +40,8 @@ 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.
- Input/reliability fixes: selecting a venue releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status. Mouse look now reads at `_input`, restores capture on window focus/click-back, and the HUD is mouse-transparent.
- 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.
+1 -1
View File
@@ -12,7 +12,7 @@ config_version=5
config/name="Gig Line"
config/description="Workplace security simulation about observation, access verification, and proportionate escalation."
config/version="0.7.2"
config/version="0.7.3"
run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.3", "GL Compatibility")
+28 -6
View File
@@ -115,14 +115,36 @@ static func _ensure_gamepad_bindings() -> void:
press.pressed = false
InputMap.action_add_event(action, press)
func _unhandled_input(event: InputEvent) -> void:
## Mouse look is read at the `_input` stage, before GUI/Control handling. A
## full-screen HUD or a focused control must never be able to swallow relative
## mouse motion while the game is in captured mode.
func _input(event: InputEvent) -> void:
if input_locked:
return
if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
rotate_y(-event.relative.x * look_sensitivity)
var pitch_sign := 1.0 if invert_y else -1.0
head.rotate_x(pitch_sign * event.relative.y * look_sensitivity)
head.rotation.x = clampf(head.rotation.x, deg_to_rad(-80.0), deg_to_rad(80.0))
if event is InputEventMouseMotion:
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
return
_apply_mouse_look(event.relative)
get_viewport().set_input_as_handled()
elif event is InputEventMouseButton and event.pressed and Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
# Recover capture if the window lost it (alt-tab, compositor focus,
# clicking back into the game) without opening the radial menu.
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
get_viewport().set_input_as_handled()
## Pure look update, kept separate so the rotation math is testable.
func _apply_mouse_look(relative: Vector2) -> void:
rotate_y(-relative.x * look_sensitivity)
var pitch_sign := 1.0 if invert_y else -1.0
head.rotate_x(pitch_sign * relative.y * look_sensitivity)
head.rotation.x = clampf(head.rotation.x, deg_to_rad(-80.0), deg_to_rad(80.0))
func _notification(what: int) -> void:
# A window/compositor focus change can silently release capture. Restore it
# when gameplay owns the input layer; menus set input_locked instead.
if what == NOTIFICATION_APPLICATION_FOCUS_IN or what == NOTIFICATION_WM_WINDOW_FOCUS_IN:
if is_inside_tree() and not input_locked:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _physics_process(_delta: float) -> void:
if focus_suppress_s > 0.0:
+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())
+11
View File
@@ -356,6 +356,17 @@ func _init() -> void:
debug_label.add_theme_font_size_override("font_size", 11)
debug_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
add_child(debug_label)
_ignore_mouse(self)
## HUD controls are presentation-only. Leaving the full-screen containers on the
## default MOUSE_FILTER_STOP can swallow mouse motion/buttons before the guard's
## captured-look input sees them. Make every non-gameplay HUD control inert to
## the mouse; modal menus are separate CanvasLayers and keep their own filters.
func _ignore_mouse(node: Node) -> void:
for child in node.get_children():
if child is Control:
(child as Control).mouse_filter = Control.MOUSE_FILTER_IGNORE
_ignore_mouse(child)
func set_shift_state(coordinator: Object, scenario: Resource) -> void:
top_label.text = scenario.display_name if scenario else "Gig Line"
+38
View File
@@ -0,0 +1,38 @@
extends RefCounted
## Regression tests for the first-person mouse-look path and non-blocking HUD.
const GameManager := preload("res://src/game/game_manager.gd")
const GuardController := preload("res://src/game/guard_controller.gd")
const Hud := preload("res://src/game/ui/hud.gd")
func test_mouse_look_rotates_yaw_and_pitch() -> bool:
var root := Node3D.new()
var gm: Node = GameManager.new()
var guard: GuardController = gm.call("_build_guard")
guard._ready()
var yaw0 := guard.rotation.y
var pitch0 := guard.head.rotation.x
guard._apply_mouse_look(Vector2(100, 50))
var a := TestUtils.check(guard.rotation.y < yaw0 - 0.1, "horizontal motion yaws the guard")
var b := TestUtils.check(guard.head.rotation.x < pitch0 - 0.1, "vertical motion pitches the head")
guard._apply_mouse_look(Vector2(0, 100000))
var c := TestUtils.check(absf(rad_to_deg(guard.head.rotation.x)) <= 80.01,
"pitch stays clamped")
guard.free()
gm.free()
root.free()
return a and b and c
func test_hud_does_not_capture_mouse_input() -> bool:
var hud: Hud = Hud.new()
var root := hud.get_child(0) as Control
var a := TestUtils.check(root != null and root.mouse_filter == Control.MOUSE_FILTER_IGNORE,
"full-screen HUD root ignores mouse input")
var labels := hud.find_children("*", "Label", true, false)
var blocking := 0
for node in labels:
if (node as Control).mouse_filter != Control.MOUSE_FILTER_IGNORE:
blocking += 1
var b := TestUtils.check(blocking == 0, "HUD labels do not swallow mouse motion")
hud.free()
return a and b
+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