Compare commits
4
Commits
f4cce28454
...
v0.7.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76eb5e3a79 | ||
|
|
f4a53e2d4f | ||
|
|
806fe78688 | ||
|
|
6a3066d763 |
No files matched your search
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user