Compare commits

..
5 Commits
Author SHA1 Message Date
zwitschi 4bc6390e9f Release 0.9.5 texture and material refinement
Full build / Import + headless tests (push) Successful in 46s
Full build / Full desktop build + optional release (push) Successful in 4m54s
2026-11-02 21:04:48 +01:00
zwitschi fb3da52438 Refine environment surfaces and skyline haze
Full build / Import + headless tests (push) Successful in 42s
Full build / Full desktop build + optional release (push) Skipped
2026-11-02 20:38:58 +01:00
zwitschi d7220c1a0a Refine NPC fabric UVs and remove baked AO blotches
Full build / Import + headless tests (push) Successful in 37s
Full build / Full desktop build + optional release (push) Skipped
2026-11-02 20:28:25 +01:00
zwitschi 31574b8d82 Raise NPC texture maps to 256
Full build / Import + headless tests (push) Successful in 43s
Full build / Full desktop build + optional release (push) Skipped
2026-11-02 20:16:30 +01:00
zwitschi b60f67548d Record the v0.9.4 release build
Full build / Full desktop build + optional release (push) Skipped
Full build / Import + headless tests (push) Successful in 43s
2026-11-02 19:58:40 +01:00
71 changed files with 331 additions and 105 deletions

No files matched your search

+13
View File
@@ -4,6 +4,19 @@ All notable user-facing changes to Gig Line. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use
Semantic Versioning. Semantic Versioning.
## [0.9.5] — 2026-11-02
### Changed
- NPC albedo, normal and ORM maps are now 256² instead of 128², giving the
face, fabric and gear materials more detail at portrait distance.
- NPC fabric maps now repeat four times across the body and skip the baked
per-character AO, removing the large blotchy patches from shirts, jackets and
trousers at portrait distance.
- The Walk of Fame band now uses the paver/terrazzo texture instead of a flat
dark strip, the neighbour awning keeps its cream stripe at close range, and the
distant skyline fades into horizon haze instead of reading as a crisp cut-out.
## [0.9.4] — 2026-11-02 ## [0.9.4] — 2026-11-02
### Changed ### Changed
+1
View File
@@ -1878,3 +1878,4 @@ and `npc_proportions.py` pass, the full suite is 262/262 and the 24-NPC smoke is
unexplained regressions. Accepted limits: hand items use the forward-offset open palm rather than unexplained regressions. Accepted limits: hand items use the forward-offset open palm rather than
a finger-curl rig; fabric folds stay out of the low-poly target; the >26 m impostor remains a a finger-curl rig; fabric folds stay out of the low-poly target; the >26 m impostor remains a
two-quad billboard; evening readability is represented by the day/night/LATE final-context set. two-quad billboard; evening readability is represented by the day/night/LATE final-context set.
Tagged `v0.9.4`; Gitea run 916 passed checks and the full build and published seven assets.
+39 -13
View File
@@ -63,10 +63,32 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
`character_override` because it has no spawned NPC role. **Signed off `character_override` because it has no spawned NPC role. **Signed off
2026-11-02:** 96 final-context captures, no P0; lanyard/helmet P1 fixes; the 2026-11-02:** 96 final-context captures, no P0; lanyard/helmet P1 fixes; the
backpack straps, caps, low-poly hands/faces and optional close-range backpack straps, caps, low-poly hands/faces and optional close-range
flashlight wash were accepted or deferred. The 24-NPC budget is 6.93 ms, the flashlight wash were accepted or deferred. The 24-NPC budget is 6.94 ms, the
gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean
portrait measured a 0.732 vision gap against the photoreal concept. No portrait measured a 0.732 vision gap against the photoreal concept. No
release build, version bump or tag was created at this gate. release build, version bump or tag was created at this gate.
- **NPC texture resolution (0.9.5, 2026-11-02):** `tools/npc_pipeline.py` now generates 256²
albedo/normal/ORM maps instead of 128² (AI shirt/trousers are resized to 256). The base and all
eight library GLBs plus AO maps were rebuilt; texture files total ~0.5 MB, eight GLBs ~5.75 MB,
and the 24-NPC smoke is 6.94 ms.
- **0.9.5 texture/UI audit (2026-11-02):** all 21 screen-art textures are
1280×720 and referenced by their screens; access cards are 256×128 and HUD
icons 64×64, with no oversized or unused runtime texture. The 24-NPC smoke now
reports **168 meshes / 131,616 triangles / 6.94 ms** (50 ms budget). Compressed
payloads are picks **24.55 MB**, UI art **22.67 MB**, NPC maps **0.48 MB**.
Legacy root textures remain only for `generate_textures.py` and the asset test.
- **Environment surface audit (0.9.5, 2026-11-02):** `capture_realism.gd` gained
`surfaces` (Hollywood pavers/asphalt/limestone/facade) and `surfaces_joke`
(neighbour awning, canopy, pavers) stages with the HUD hidden. The Walk of Fame
band now uses `T_CONCRETE` as dark terrazzo; the neighbour awning uses a neutral
tint (`0.62`) and 4× UV so the red/cream stripes survive; the skyline shader
fades its lower 42 m toward a per-phase haze colour. Brick/asphalt picks needed
no replacement. No seams or broken UV scales found.
- **NPC fabric UV/AO (0.9.5, 2026-11-02):** cloth/pants use
`Materials.NPC_FABRIC_UV_SCALE = Vector3(4,4,1)`, and `NpcAvatar` skips the baked per-character
AO on those two slots. The 256² AO was magnifying into large blotches; the fabric now reads as a
fine weave. Skin/hair/gear keep baked AO. Test `test_fabric_uv_scale_avoids_the_baked_ao_pattern`
guards this. Current suite is 263/263.
- **NPC visual baseline (0.9.4, 2026-11-02):** `src/test/capture_npc_baseline.gd` is the - **NPC visual baseline (0.9.4, 2026-11-02):** `src/test/capture_npc_baseline.gd` is the
neutral-studio 8-variant × 6-view baseline (front, three-quarter, profile, back, full body, neutral-studio 8-variant × 6-view baseline (front, three-quarter, profile, back, full body,
crowd). It uses a fixed orbit camera, mid-grey studio backdrop, key/fill lights, no HUD/crowd/animation; crowd). It uses a fixed orbit camera, mid-grey studio backdrop, key/fill lights, no HUD/crowd/animation;
@@ -152,16 +174,20 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
for every new `Button`; CheckButton/OptionButton keep the engine theme. Timeline for every new `Button`; CheckButton/OptionButton keep the engine theme. Timeline
Previous/Next disable at the ends; F1 tutorial skip is keyboard-only by design. Previous/Next disable at the ends; F1 tutorial skip is keyboard-only by design.
- **Version and roadmap renumber (2026-11-02):** source was **0.9.3** at the UI - **Version and roadmap renumber (2026-11-02):** source was **0.9.3** at the UI
polish milestone, now **0.9.4** after the NPC pass; the polish milestone, **0.9.4** after the NPC pass, now **0.9.5** after the texture
completed map/NPC work and UI polish are the patch content. Next work is and material pass. Next work is **0.9.6 integration**, then the **0.10.0**
**0.9.4 NPC**, **0.9.5 textures**, **0.9.6 integration**, then the **0.10.0**
minor release candidate. `TODO.md` plans only 0.x patch/minor increments; no minor release candidate. `TODO.md` plans only 0.x patch/minor increments; no
major-version scope is defined yet. The v0.9.4 tag is the release trigger; no major-version scope is defined yet. The `v0.9.5` tag is the release trigger; no
tag was created for 0.9.3. tag was created for 0.9.3.
- **Priority change (2026-11-02):** the map optimization, pre-build NPC revisit, - **Priority change (2026-11-02):** the map optimization, pre-build NPC revisit,
0.9.3 UI polish and 0.9.4 NPC visual optimization are complete and in `DONE.md`. 0.9.3 UI polish, 0.9.4 NPC visual optimization and 0.9.5 texture/material pass
0.9.5 textures and 0.9.6 integration/0.10.0 release work are next. The current are complete and in `DONE.md`. 0.9.6 integration and 0.10.0 release work are
source version is 0.9.4; the v0.9.4 tag triggers the CI build. next. The current source version is 0.9.5.
- **Release v0.9.4 (2026-11-02):** commit `a57b86c` tagged `v0.9.4`; Gitea Actions
run 916 passed checks and the full build and published seven assets (Linux,
Windows, macOS, NSIS setup, AppImage, Web, Desktop HQ). Release page:
`https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.4`. Do not
reuse the tag; future fixes need a new patch tag.
- **Hollywood landmark map (2026-11-02):** the default venue is the real El Capitan - **Hollywood landmark map (2026-11-02):** the default venue is the real El Capitan
Entertainment Centre block at 6840 Hollywood Blvd, not a generic brick entrance. Its Entertainment Centre block at 6840 Hollywood Blvd, not a generic brick entrance. Its
reference/mapping notes live in `docs/hollywood-boulevard-map.md`; the map-budget test reference/mapping notes live in `docs/hollywood-boulevard-map.md`; the map-budget test
@@ -184,7 +210,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
`JokeGround_*`; `set_night_mode()` owns their energies and the hill tint. `JokeGround_*`; `set_night_mode()` owns their energies and the hill tint.
The latest local realism judge reports a 0.454 reference gap on the night entrance; The latest local realism judge reports a 0.454 reference gap on the night entrance;
the concept is photoreal while the map keeps stylized materials, so texture/foliage the concept is photoreal while the map keeps stylized materials, so texture/foliage
detail stays with the postponed 0.9.5 pass rather than the map milestone. detail stays with the 0.9.5 texture pass rather than the map milestone.
- **Docs validation (2026-11-02):** `python3 tools/check_docs.py` requires a README - **Docs validation (2026-11-02):** `python3 tools/check_docs.py` requires a README
only in directories containing tracked files, verifies local links and requires only in directories containing tracked files, verifies local links and requires
every tracked image in a folder to be linked from its README. Gitignored every tracked image in a folder to be linked from its README. Gitignored
@@ -253,7 +279,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
gpt-5-image-mini rejects (HTTP 400); pass `--request-size 1024` for 1:1 roles. gpt-5-image-mini rejects (HTTP 400); pass `--request-size 1024` for 1:1 roles.
`generate --role tile` already applies `make_seamless`, so a separate `generate --role tile` already applies `make_seamless`, so a separate
`seamless` pass is redundant. Added to `tools/npc_pipeline.py`: `prepare` `seamless` pass is redundant. Added to `tools/npc_pipeline.py`: `prepare`
(layers AI shirt→cloth, trousers→pants albedos at 128px) and `palette` (layers AI shirt→cloth, trousers→pants albedos at 256px) and `palette`
(writes `assets/textures/npc/palette.json` via `model_pipeline.dominant_colors`). (writes `assets/textures/npc/palette.json` via `model_pipeline.dominant_colors`).
`write_slot_textures()` now skips the albedo for slots with an AI override `write_slot_textures()` now skips the albedo for slots with an AI override
(normal/ORM stay procedural). Blender `build` re-run: GLB audit ok, 1204 tris / (normal/ORM stay procedural). Blender `build` re-run: GLB audit ok, 1204 tris /
@@ -798,7 +824,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
- Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI. - Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
- Access loadout is implemented; do not re-fix `override_access` as if it were still unread. - Access loadout is implemented; do not re-fix `override_access` as if it were still unread.
- `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`. - `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`.
- Headless suite is **262/262**; Python tooling is **101/101**; screenshot compare is **21 good / 0 bad** and the automatic judge is **21/21 match, 0 diff**. Remaining visual follow-ups are in `TODO.md`. - Headless suite is **263/263**; Python tooling is **101/101**; screenshot compare is **21 good / 0 bad** and the automatic judge is **21/21 match, 0 diff**. Remaining visual follow-ups are in `TODO.md`.
- Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args). - Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args).
- GDScript inference trap that broke the main scene: a `var` typed as a base class - GDScript inference trap that broke the main scene: a `var` typed as a base class
(`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a (`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a
@@ -822,8 +848,8 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
`PLAN.md` describes intended design; `README.md` and `DONE.md` describe what shipped. As of `PLAN.md` describes intended design; `README.md` and `DONE.md` describe what shipped. As of
2026-11-02 the playable build covers the three venues, tutorials/campaign/checkpoints, the 0.9.3 2026-11-02 the playable build covers the three venues, tutorials/campaign/checkpoints, the 0.9.3
UI/accessibility pass, the 0.9.4 NPC visual optimization, and the Blender + realism asset pipeline. `TODO.md` holds the remaining UI/accessibility pass, the 0.9.4 NPC visual optimization, the 0.9.5 texture/material pass, and the Blender + realism asset pipeline. `TODO.md` holds the remaining
0.9.4–0.10.0 work. The project uses a custom headless runner, not GUT, and Gitea Actions, not 0.9.6–0.10.0 work. The project uses a custom headless runner, not GUT, and Gitea Actions, not
GitHub Actions. GitHub Actions.
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu --> <!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
+1 -1
View File
@@ -176,7 +176,7 @@ Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [AD
## Versioning and Delivery ## Versioning and Delivery
`config/version` in `project.godot` is the single version source (currently `0.9.4`). 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.9.5`). 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 ## Design Direction
+12 -10
View File
@@ -38,12 +38,13 @@ precision walk, the Joke Shop's Sunset Strip frontage, the TV Studio arrival, an
> Downloads are public (no sign-in required) since the host's instance-wide > Downloads are public (no sign-in required) since the host's instance-wide
> `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31. > `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31.
> >
> **Latest published: v0.9.4** — the NPC visual optimization pass: face/head > **Latest published: v0.9.5** — the texture and material refinement pass:
> depth, hands and gear placement, shoes and boots, headgear clearance, and the > 256² NPC maps, a four-times fabric weave without baked AO blotches, repair of
> 8,000/8,000 character mesh budget on top of the Hollywood/Joke Shop maps and > the Walk of Fame terrazzo band and awning stripe, and per-phase skyline haze,
> on top of the v0.9.4 NPC visual optimization, the Hollywood/Joke Shop maps and
> the v0.9.3 UI/accessibility polish. > the v0.9.3 UI/accessibility polish.
> >
> **Current source: v0.9.4** — tagged and built from the same commit. > **Current source: v0.9.5** — tagged and built from the same commit.
> Releases are built automatically when a `vX.Y.Z` tag is pushed > Releases are built automatically when a `vX.Y.Z` tag is pushed
> (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use > (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use
> `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline > `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline
@@ -121,18 +122,19 @@ Full setup, every run/test/smoke command, and the controls table are in
- [`docs/development-status.md`](docs/development-status.md) — Implemented features and what's next. - [`docs/development-status.md`](docs/development-status.md) — Implemented features and what's next.
- [`docs/smoke-checklist.md`](docs/smoke-checklist.md) — Manual smoke-test checklist. - [`docs/smoke-checklist.md`](docs/smoke-checklist.md) — Manual smoke-test checklist.
- [`docs/blender-assets.md`](docs/blender-assets.md) — Blender asset library: characters, gear, props, attach points. - [`docs/blender-assets.md`](docs/blender-assets.md) — Blender asset library: characters, gear, props, attach points.
- [`docs/npc-visual-review.md`](docs/npc-visual-review.md) — 0.9.4 NPC baseline, acceptance criteria and ranked visual gaps. - [`docs/npc-visual-review.md`](docs/npc-visual-review.md) — 0.9.4 NPC baseline, acceptance criteria and ranked visual gaps; 0.9.5 texture comparison is in `docs/blender_previews/sheet_texture_refinement_0.9.5.jpg`.
- [`docs/npc-proportion-audit.md`](docs/npc-proportion-audit.md) — NPC proportion measurements against the shared stylized guide. - [`docs/npc-proportion-audit.md`](docs/npc-proportion-audit.md) — NPC proportion measurements against the shared stylized guide.
- [`docs/adr/`](docs/adr/) — Architecture Decision Records. - [`docs/adr/`](docs/adr/) — Architecture Decision Records.
- [`TODO.md`](TODO.md) — Active tasks plus the 0.x gap analysis and milestone roadmap. - [`TODO.md`](TODO.md) — Active tasks plus the 0.x gap analysis and milestone roadmap.
## Development Status ## Development Status
v0.9.4 is the current source version: the completed NPC visual optimization v0.9.5 is the current source version: the completed texture and material
(face/head depth, hands and gear placement, shoes/boots, headgear clearance and refinement (256² NPC maps, a 4× fabric weave with baked-AO removal, repaired
the 8,000/8,000 mesh budget), on top of the v0.9.3 UI/accessibility polish Walk of Fame/awning surfaces and per-phase skyline haze), on top of the v0.9.4
pass and v0.9.2's Hollywood/Joke Shop map rebuilds. It builds on NPC visual optimization and the v0.9.3 UI/accessibility polish pass. It builds on
v0.9.1's Godot 4.7.2 baseline, v0.9.0's production-quality pass, v0.8's v0.9.2's Hollywood/Joke Shop map rebuilds, v0.9.1's Godot 4.7.2 baseline,
v0.9.0's production-quality pass, v0.8's
tutorial/campaign/resume work and the v0.7.x venue/gear/judge work. See [`docs/development-status.md`](docs/development-status.md) tutorial/campaign/resume work and the v0.7.x venue/gear/judge work. See [`docs/development-status.md`](docs/development-status.md)
for the implemented list and [`TODO.md`](TODO.md) for the 0.x roadmap. Manual for the implemented list and [`TODO.md`](TODO.md) for the 0.x roadmap. Manual
verification uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md). verification uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md).
+55 -30
View File
@@ -5,9 +5,9 @@ see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these
## Gap Analysis and 0.x Roadmap ## Gap Analysis and 0.x Roadmap
Baseline: **v0.9.4** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume, Baseline: **v0.9.5** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume,
difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete
escalation/access/radio loop, shift review, the Blender asset library, 262 headless + 101 Python escalation/access/radio loop, shift review, the Blender asset library, 263 headless + 101 Python
tests, a 21/21 clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong tests, a 21/21 clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong
release-candidate foundation, not yet a 0.10.0 release candidate. release-candidate foundation, not yet a 0.10.0 release candidate.
@@ -37,7 +37,7 @@ release-candidate foundation, not yet a 0.10.0 release candidate.
| Animation/presentation | Procedural pose layers, portrait captures, reduced hem clipping, 8,000/8,000 NPC cap | Thorough NPC silhouette/face/gear/pose/LOD optimization before visual QA | Close-up jank reduces perceived quality | Complete (0.9.4) | | Animation/presentation | Procedural pose layers, portrait captures, reduced hem clipping, 8,000/8,000 NPC cap | Thorough NPC silhouette/face/gear/pose/LOD optimization before visual QA | Close-up jank reduces perceived quality | Complete (0.9.4) |
| Audio/accessibility | Procedural ambience/SFX, volume controls, captions/reduce-motion settings, searchable Help | Full accessibility/localization acceptance review, audio caption coverage and P0/P1 fixes | Public release cannot claim accessible play | 0.10.0 | | Audio/accessibility | Procedural ambience/SFX, volume controls, captions/reduce-motion settings, searchable Help | Full accessibility/localization acceptance review, audio caption coverage and P0/P1 fixes | Public release cannot claim accessible play | 0.10.0 |
| Localization | English JSON table, validation and input-generated Help | Pseudo-locale/layout audit and translator workflow sign-off | Text expansion/clipping at release | 0.10.0 | | Localization | English JSON table, validation and input-generated Help | Pseudo-locale/layout audit and translator workflow sign-off | Text expansion/clipping at release | 0.10.0 |
| Performance/platform | 24-NPC headless budget (6.9 ms), Compatibility/Web/HQ paths | Target hardware matrix, Web/HQ manual profiling, frame-time/memory evidence | Platform-specific drops or startup failures | 0.9.6 / 0.10.0 | | Performance/platform | 24-NPC headless budget (6.94 ms), Compatibility/Web/HQ paths | Target hardware matrix, Web/HQ manual profiling, frame-time/memory evidence | Platform-specific drops or startup failures | 0.9.6 / 0.10.0 |
| Save lifecycle | Settings/campaign/history/checkpoint files, atomic writes | Migration fixtures from 0.8/0.9, fresh-install/upgrade verification, rollback instructions | Player progress loss on upgrade | 0.10.0 | | Save lifecycle | Settings/campaign/history/checkpoint files, atomic writes | Migration fixtures from 0.8/0.9, fresh-install/upgrade verification, rollback instructions | Player progress loss on upgrade | 0.10.0 |
| QA/CI | Headless suite, smokes, screenshot compare, 21/21 judge | Release-candidate smoke matrix, final gallery and release dry run | Regressions escape the release gate | 0.9.6 / 0.10.0 | | QA/CI | Headless suite, smokes, screenshot compare, 21/21 judge | Release-candidate smoke matrix, final gallery and release dry run | Regressions escape the release gate | 0.9.6 / 0.10.0 |
| Release/legal | Unsigned desktop/macOS builds, Web/HQ packages | Signing/notarization decision, checksums, AI-asset provenance, credits, privacy/body-camera and rating review | Public release blocked by trust/policy | 0.10.0 | | Release/legal | Unsigned desktop/macOS builds, Web/HQ packages | Signing/notarization decision, checksums, AI-asset provenance, credits, privacy/body-camera and rating review | Public release blocked by trust/policy | 0.10.0 |
@@ -45,48 +45,73 @@ release-candidate foundation, not yet a 0.10.0 release candidate.
| Future 0.x | Stable 0.x feature set | Live replay, more venues, VO/music, image-to-mesh props and seasonal content | Scope creep before release | 0.11.0+ | | Future 0.x | Stable 0.x feature set | Live replay, more venues, VO/music, image-to-mesh props and seasonal content | Scope creep before release | 0.11.0+ |
**Current priority:** map optimization, the **pre-build NPC revisit**, the **Current priority:** map optimization, the **pre-build NPC revisit**, the
**0.9.3 UI polish** and **0.9.4 NPC visual optimization** passes are complete **0.9.3 UI polish**, **0.9.4 NPC visual optimization** and **0.9.5 texture
(all moved to `DONE.md` on 2026-11-02). 0.9.5 texture refinement, 0.9.6 refinement** passes are complete (all moved to `DONE.md` on 2026-11-02).
integration and the 0.10.0 release candidate are queued. The source version is 0.9.6 integration and the 0.10.0 release candidate are queued. The source
now 0.9.4. version is now 0.9.5.
## Postponed — Patch 0.9.5 — Texture and material refinement ## Patch 0.9.5 — Texture and material refinement
Goal: improve close-range surface and character texture quality while keeping GL Goal: improve close-range surface and character texture quality while keeping GL
Compatibility/Web parity, deterministic assets and offline CI. Compatibility/Web parity, deterministic assets and offline CI.
### Environment textures and materials ### Environment textures and materials
- [ ] Audit the v2 picks (brick, sidewalk pavers, dry/wet asphalt, awning canvas, - [x] Audit the v2 picks (brick, sidewalk pavers, dry/wet asphalt, awning canvas,
day/dusk/night skylines) at close range and in the entrance/street shots for day/dusk/night skylines) at close range and in the entrance/street shots for
seams, tiling, scale, contrast and normal-map strength. seams, tiling, scale, contrast and normal-map strength. (New `surfaces` and
- [ ] Refine brick mortar/wear, paver joints/stains, asphalt aggregate/wet response `surfaces_joke` capture stages; no seam or UV-scale failures found.)
- [x] Refine brick mortar/wear, paver joints/stains, asphalt aggregate/wet response
and awning weave using the existing image/asset tooling; never overwrite a and awning weave using the existing image/asset tooling; never overwrite a
pick, use `-vN` candidates and promote validated results. pick, use `-vN` candidates and promote validated results. (The Walk of Fame
- [ ] Check UV scales and material roughness/metallic profiles; keep day/night band now uses the paver/terrazzo texture; the neighbour awning keeps its
phase behavior and the wet-street transition intact. cream stripe via a neutral tint and 4× UV. Brick/asphalt picks held up.)
- [ ] Review distant skyline haze and foliage/planter surfaces against the concept - [x] Check UV scales and material roughness/metallic profiles; keep day/night
references; fix obvious flatness without regressing readability. phase behavior and the wet-street transition intact. (Profiles and UV
scales passed; dry/wet asphalt and phase switching unchanged.)
- [x] Review distant skyline haze and foliage/planter surfaces against the concept
references; fix obvious flatness without regressing readability. (Added
world-height haze fade to the skyline shader; foliage/planters stayed
stylized and readable, so no mesh change.)
### Character and prop textures ### Character and prop textures
- [ ] Audit NPC skin, cloth, pants, hair and gear albedo/normal/AO maps in portraits; - [x] Audit NPC skin, cloth, pants, hair and gear albedo/normal/AO maps in portraits;
refine fabric weave, skin variation and fixed detail materials. refine fabric weave, skin variation and fixed detail materials. (256² maps rebuilt; fabric
- [ ] Re-bake AO where geometry changes; verify the 256² maps and material wiring. now uses a 4× UV weave and skips baked per-character AO, removing the portrait blotches;
- [ ] Audit library props and UI/card/icon textures at their actual screen sizes; skin/hair/gear keep AO. Comparison `sheet_npc_baseline_compare-texuv.jpg`.)
remove blurry/oversized or unused textures where safe. - [x] Re-bake AO where geometry changes; verify the 256² maps and material wiring.
- [ ] Run texture audits, import, and the full asset test suite; record texture memory (No character geometry changed in the texture pass; asset/base audits pass.)
and triangle/draw-call impact. - [x] Audit library props and UI/card/icon textures at their actual screen sizes;
remove blurry/oversized or unused textures where safe. (All 21 screen-art
textures are 1280×720 and referenced; access cards are 256×128, HUD icons
64×64; no oversized or unused runtime texture found. Legacy root textures
remain only as the `generate_textures.py`/asset-test fallback.)
- [x] Run texture audits, import, and the full asset test suite; record texture memory
and triangle/draw-call impact. (Godot 263/263, Python 101/101; asset and base
audits pass; 24 NPCs = 168 meshes / 131,616 triangles / 6.94 ms. Compressed
payloads: picks 24.55 MB, UI art 22.67 MB, NPC maps 0.48 MB.)
### Visual verification ### Visual verification
- [ ] Run `python3 tools/scene_concepts.py realism` for the affected stages with the - [x] Run `python3 tools/scene_concepts.py realism` for the affected stages with the
judge enabled; record metric gaps and model/spend. judge enabled; record metric gaps and model/spend. (Run
- [ ] Re-run `tools/check_screenshots.sh` and the screen-design judge; clear all `docs/screens/realism/2026-11-02-tex` with `z-ai/glm-5.3-flash`, one pass per
`diff` verdicts. shot; mean `after` overall 1.8, lighting 2.3, materials 1.8, texture 1.2,
- [ ] Capture before/after close-ups for brick, pavement, asphalt, character cloth backdrop 2.0, characters 1.2, Pillow gap 0.306. The judge is optional and not
and skyline; document accepted trade-offs. a CI gate; the report stays local.)
- [ ] Update asset docs and the changelog/status with the promoted texture versions. - [x] Re-run `tools/check_screenshots.sh` and the screen-design judge; clear all
`diff` verdicts. (21 good / 0 bad; `screen_designs.py judge` = 21/21 match,
0 review, 0 diff.)
- [x] Capture before/after close-ups for brick, pavement, asphalt, character cloth
and skyline; document accepted trade-offs. (Committed
`docs/blender_previews/sheet_texture_refinement_0.9.5.jpg`: Walk of Fame
pavers, asphalt aggregate, limestone facade, day skyline haze and NPC cloth;
asphalt/limestone picks intentionally held, no replacement.)
- [x] Update asset docs and the changelog/status with the promoted texture versions.
(README/PLAN/architecture/status/MEMORY and the 0.9.5 changelog entry now
carry the texture results; the 0.9.5 section moved to `DONE.md` after the
release build.)
## Postponed — Patch 0.9.6 — Integration, performance and judge gate ## Postponed — Patch 0.9.6 — Integration, performance and judge gate
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 897 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+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. - **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible.
- **PATCH** — bug fixes and small polish that do not change behavior. - **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.9.4`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-stable, 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.9.5`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-stable, 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. Context and Scope
+37 -12
View File
@@ -104,8 +104,11 @@ python3 tools/npc_pipeline.py palette
``` ```
`prepare` resizes the AI fabric tiles (kept locally in `assets/textures/npc/ai/`, `prepare` resizes the AI fabric tiles (kept locally in `assets/textures/npc/ai/`,
which is gitignored) onto the `cloth`/`pants` albedos; `build` bakes them into the which is gitignored) onto the 256² `cloth`/`pants` albedos; `build` bakes them into
`.glb` and regenerates the procedural normal + packed ORM maps. `palette` writes the `.glb` and regenerates the procedural normal + packed ORM maps at the same
256² size. Runtime fabric materials repeat the weave 4× (`NPC_FABRIC_UV_SCALE`)
and skip the per-character baked AO, which otherwise magnified into large
blotches; skin, hair and fixed gear keep AO. `palette` writes
`assets/textures/npc/palette.json` using `model_pipeline.dominant_colors`. `assets/textures/npc/palette.json` using `model_pipeline.dominant_colors`.
## Blender asset library (loaded by the game) ## Blender asset library (loaded by the game)
@@ -232,17 +235,39 @@ godot --path . --script res://src/test/capture_npc_baseline.gd -- \
python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline
``` ```
`capture_realism.gd` runs seven stages (day, portrait, evening, late, Joke Shop day, Joke Shop night, studio); `capture_realism.gd` runs nine stages in this order: day, Hollywood surface close-ups,
`--stages=day,late` limits it while tuning; `--flashlight` turns on the guard lamp for portrait, evening, late, Joke Shop day, Joke Shop surface close-ups, Joke Shop night,
night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one NPC on open sidewalk studio; `--stages=day,late` limits it while tuning; `--flashlight` turns on the guard
and frames the face, which is the only capture that can judge avatar detail lamp for night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one
(`--portrait-type=N` picks a role). The generated realism runs under NPC on open sidewalk and frames the face, which is the only capture that can judge
avatar detail (`--portrait-type=N` picks a role). For the 0.9.5 texture audit, run
the surface stages directly; the HUD is hidden for them:
```bash
godot --path . --script res://src/test/capture_realism.gd \
-- --out=res://tmp/surfaces --stages=surfaces,surfaces_joke
```
The generated realism runs under
`docs/screens/realism/` are **local, gitignored QA artifacts**; commit only the `docs/screens/realism/` are **local, gitignored QA artifacts**; commit only the
resulting decisions/docs, not the run folders. The texture-adoption run compared resulting decisions/docs, not the run folders. The 0.9.5 texture/UI audit checked actual screen sizes: all screen art is
the pre-adoption captures with the current build: on the shots both sets captured, 1280×720, access cards are 256×128 and HUD icons are 64×64; no runtime texture
the judge's like-for-like means rose on every sub-score (overall 0.8 → 1.0, was oversized or unused. The 24-NPC smoke now reports 168 meshes / 131,616
backdrop 0.6 → 1.6) and the Pillow metric gap fell 0.444 → 0.348. The character triangles / 6.94 ms. Compressed payloads are 24.55 MB of surface picks, 22.67 MB
pass added the portrait shot and recorded the new faces, hands and sleeve cuffs. of screen art and 0.48 MB of NPC maps.
The 0.9.5 realism judge ran once per shot with `z-ai/glm-5.3-flash`
(`python3 tools/scene_concepts.py realism --run docs/screens/realism/2026-11-02-tex
--judge`). Its mean `after` scores were overall 1.8, lighting 2.3, materials 1.8,
texture detail 1.2, backdrop 2.0 and characters 1.2; the Pillow metric gap was
0.306. The judge still ranks the painted concept targets above the stylized build,
particularly lit night facades and dense backdrop detail. That is expected: the
pass validated close-range surfaces and promoted the texture repairs, not a
photoreal renderer. The 0.9.5 before/after close-up sheet is
[`blender_previews/sheet_texture_refinement_0.9.5.jpg`](blender_previews/sheet_texture_refinement_0.9.5.jpg);
the texture-adoption run itself was the earlier 0.8-style candidate comparison,
and its report remains local/gitignored.
The Forward+ A/B compared the Compatibility entrance with the optional Desktop HQ The Forward+ A/B compared the Compatibility entrance with the optional Desktop HQ
preset. The gear-fit run checked the sunglasses, bag and helmet against measured preset. The gear-fit run checked the sunglasses, bag and helmet against measured
head/skirt bounds with `python3 tools/blender_assets.py fit`; it also fixed the head/skirt bounds with `python3 tools/blender_assets.py fit`; it also fixed the
+4 -2
View File
@@ -90,8 +90,10 @@ All eight use the `npc_base` contract, so each one can replace
`test_character_joints_line_up_with_npc_base` checks this within 1 cm, so the `test_character_joints_line_up_with_npc_base` checks this within 1 cm, so the
animation proxies in `_build_skinned_body()` stay valid. animation proxies in `_build_skinned_body()` stay valid.
- material slots `skin`, `cloth`, `pants`, `accessory`, `hair` carry the same - material slots `skin`, `cloth`, `pants`, `accessory`, `hair` carry the same
neutral `assets/textures/npc/<slot>_{albedo,normal}.png` maps, so neutral `assets/textures/npc/<slot>_{albedo,normal}.png` maps at 256², so
`Materials.npc_slot()` tints them the same way it tints `npc_base`. `Materials.npc_slot()` tints them the same way it tints `npc_base`. Cloth and
pants repeat the weave 4× (`NPC_FABRIC_UV_SCALE`) and intentionally skip the
per-character baked AO; skin, hair and fixed gear keep it.
- budget ≤ **8,000 triangles and ≤ 8,000 glTF vertices** for the eight library - budget ≤ **8,000 triangles and ≤ 8,000 glTF vertices** for the eight library
variants (vertex count after seam splits; `CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in variants (vertex count after seam splits; `CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in
`tools/blender_assets.py`, raised from 6,000/5,000 on 2026-11-02). The frozen `tools/blender_assets.py`, raised from 6,000/5,000 on 2026-11-02). The frozen
+3
View File
@@ -52,6 +52,9 @@ Committed contact sheets and local Blender renders for the character, gear and p
| <img src="sheet_npc_hands_compare.jpg" width="280" alt="sheet_npc_hands_compare.jpg"> | [`sheet_npc_hands_compare.jpg`](sheet_npc_hands_compare.jpg) | | <img src="sheet_npc_hands_compare.jpg" width="280" alt="sheet_npc_hands_compare.jpg"> | [`sheet_npc_hands_compare.jpg`](sheet_npc_hands_compare.jpg) |
| <img src="sheet_npc_hands_grip_compare.jpg" width="280" alt="sheet_npc_hands_grip_compare.jpg"> | [`sheet_npc_hands_grip_compare.jpg`](sheet_npc_hands_grip_compare.jpg) | | <img src="sheet_npc_hands_grip_compare.jpg" width="280" alt="sheet_npc_hands_grip_compare.jpg"> | [`sheet_npc_hands_grip_compare.jpg`](sheet_npc_hands_grip_compare.jpg) |
| <img src="sheet_npc_baseline_compare-shoes.jpg" width="280" alt="sheet_npc_baseline_compare-shoes.jpg"> | [`sheet_npc_baseline_compare-shoes.jpg`](sheet_npc_baseline_compare-shoes.jpg) | | <img src="sheet_npc_baseline_compare-shoes.jpg" width="280" alt="sheet_npc_baseline_compare-shoes.jpg"> | [`sheet_npc_baseline_compare-shoes.jpg`](sheet_npc_baseline_compare-shoes.jpg) |
| <img src="sheet_npc_baseline_compare-tex256.jpg" width="280" alt="sheet_npc_baseline_compare-tex256.jpg"> | [`sheet_npc_baseline_compare-tex256.jpg`](sheet_npc_baseline_compare-tex256.jpg) |
| <img src="sheet_npc_baseline_compare-texuv.jpg" width="280" alt="sheet_npc_baseline_compare-texuv.jpg"> | [`sheet_npc_baseline_compare-texuv.jpg`](sheet_npc_baseline_compare-texuv.jpg) |
| <img src="sheet_texture_refinement_0.9.5.jpg" width="280" alt="sheet_texture_refinement_0.9.5.jpg"> | [`sheet_texture_refinement_0.9.5.jpg`](sheet_texture_refinement_0.9.5.jpg) |
| <img src="sheet_npc_gear.jpg" width="280" alt="sheet_npc_gear.jpg"> | [`sheet_npc_gear.jpg`](sheet_npc_gear.jpg) | | <img src="sheet_npc_gear.jpg" width="280" alt="sheet_npc_gear.jpg"> | [`sheet_npc_gear.jpg`](sheet_npc_gear.jpg) |
| <img src="sheet_props.jpg" width="280" alt="sheet_props.jpg"> | [`sheet_props.jpg`](sheet_props.jpg) | | <img src="sheet_props.jpg" width="280" alt="sheet_props.jpg"> | [`sheet_props.jpg`](sheet_props.jpg) |
| <img src="shoulder_bag.png" width="280" alt="shoulder_bag.png"> | [`shoulder_bag.png`](shoulder_bag.png) | | <img src="shoulder_bag.png" width="280" alt="shoulder_bag.png"> | [`shoulder_bag.png`](shoulder_bag.png) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

+7 -2
View File
@@ -5,7 +5,7 @@ Implemented so far:
- Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified) - Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified)
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem` - Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
- Three authored scenarios (El Capitan Entertainment Centre / Hollywood Boulevard, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools - Three authored scenarios (El Capitan Entertainment Centre / Hollywood Boulevard, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
- Headless unit tests: **262 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: **263 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 - 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 - 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`) - 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`)
@@ -42,6 +42,11 @@ Implemented so far:
- NPC neck/shoulder transition (0.9.4 work): the body graph now uses a fuller neck base, a slightly tapered neck top and a lower shoulder root, giving the head/neck/shoulder line a trapezius slope instead of a thin post. The full character library was rebuilt with a final degenerate-face cleanup; the baseline comparison is `sheet_npc_baseline_compare-body.jpg`. - NPC neck/shoulder transition (0.9.4 work): the body graph now uses a fuller neck base, a slightly tapered neck top and a lower shoulder root, giving the head/neck/shoulder line a trapezius slope instead of a thin post. The full character library was rebuilt with a final degenerate-face cleanup; the baseline comparison is `sheet_npc_baseline_compare-body.jpg`.
- NPC face-depth pass (0.9.4 work): sclera are recessed and capped by a thin upper lid, and the separate jaw/cheek spheres are replaced by a chin plate and cheekbones sculpted into the head mesh. The full library was rebuilt; the comparison is `sheet_npc_baseline_compare-face.jpg`. - NPC face-depth pass (0.9.4 work): sclera are recessed and capped by a thin upper lid, and the separate jaw/cheek spheres are replaced by a chin plate and cheekbones sculpted into the head mesh. The full library was rebuilt; the comparison is `sheet_npc_baseline_compare-face.jpg`.
- NPC torso/hip transition (0.9.4 work): the pelvis and chest are slightly wider than the waist, giving shirts and jackets a readable taper into the hips without changing the build multipliers or the skeleton. The comparison is `sheet_npc_baseline_compare-torso.jpg`. - NPC torso/hip transition (0.9.4 work): the pelvis and chest are slightly wider than the waist, giving shirts and jackets a readable taper into the hips without changing the build multipliers or the skeleton. The comparison is `sheet_npc_baseline_compare-torso.jpg`.
- Release v0.9.4 (2026-11-02): the tagged commit `a57b86c` passed the Gitea checks and full build and published seven artifacts (Linux, Windows, macOS, NSIS setup, AppImage, Web and Desktop HQ). Release page: <https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.4>.
- Environment texture pass (0.9.5 work): `capture_realism.gd` now has hidden-HUD `surfaces`/`surfaces_joke` close-up stages. The Walk of Fame band uses the paver texture as dark terrazzo, the neighbour awning keeps its cream stripe, and the skyline shader blends its lower 42 m toward a per-phase haze colour. Brick, asphalt, limestone and paver UV scales/profiles passed without replacement.
- Texture/prop audit (0.9.5 work): all 21 screen-art textures are 1280×720 and referenced; access cards are 256×128 and HUD icons 64×64. No oversized or unused runtime texture was found; the legacy root textures remain only as the texture-generator/asset-test fallback. The 24-NPC smoke now reports 168 meshes, 131,616 triangles and 6.94 ms (50 ms budget); compressed payload is 24.55 MB of surface picks, 22.67 MB of screen art and 0.48 MB of NPC maps.
- NPC texture pass (0.9.5 work): the NPC base and all eight library characters now use 256² albedo/normal/ORM maps instead of 128². Cloth and pants use `NPC_FABRIC_UV_SCALE` (4× weave) and skip the baked per-character AO, removing the large blotchy patches in portraits; skin, hair and gear keep baked AO. The base and eight GLBs plus AO maps were rebuilt; the 24-NPC smoke remains green.
- Texture visual verification (0.9.5 work): the nine-stage realism run used `z-ai/glm-5.3-flash` once per shot; mean `after` scores were overall 1.8, lighting 2.3, materials 1.8, texture detail 1.2, backdrop 2.0 and characters 1.2, with a Pillow metric gap of 0.306. The judge remains an optional review aid, not a CI gate. The committed before/after close-up sheet is [`blender_previews/sheet_texture_refinement_0.9.5.jpg`](blender_previews/sheet_texture_refinement_0.9.5.jpg); it records that the paver/Walk of Fame and NPC fabric repairs are visible, while asphalt, limestone and skyline picks held without a replacement. `tools/check_screenshots.sh` passed 21 good / 0 bad and the screen-design judge passed 21/21 match with 0 review.
- NPC visual optimization (0.9.4, complete 2026-11-02): all eight variants passed the portrait/readability, proportion, gear, foot/shoe, headgear and final-context gates. The library uses an 8,000-triangle / 8,000-glTF-vertex cap (up from 6,000/5,000; `npc_base` fallback remains 6,000/4,000); current characters measure 4,482–5,474 triangles and 4,260–5,494 vertices. The 48-frame baseline and 96-frame final-context review pass, the hand/gear offset and shoes are in, and the accepted stylized limits are documented in `npc-visual-review.md`. - NPC visual optimization (0.9.4, complete 2026-11-02): all eight variants passed the portrait/readability, proportion, gear, foot/shoe, headgear and final-context gates. The library uses an 8,000-triangle / 8,000-glTF-vertex cap (up from 6,000/5,000; `npc_base` fallback remains 6,000/4,000); current characters measure 4,482–5,474 triangles and 4,260–5,494 vertices. The 48-frame baseline and 96-frame final-context review pass, the hand/gear offset and shoes are in, and the accepted stylized limits are documented in `npc-visual-review.md`.
- NPC face-detail pass (0.9.4 work): ala wings and a smaller nose bridge/tip, one rounded mouth slot, and a darker ear concha; all eight rebuilt. The comparison is `sheet_npc_baseline_compare-face-detail.jpg`. - NPC face-detail pass (0.9.4 work): ala wings and a smaller nose bridge/tip, one rounded mouth slot, and a darker ear concha; all eight rebuilt. The comparison is `sheet_npc_baseline_compare-face-detail.jpg`.
- NPC headgear clearance (0.9.4 work): the 96-frame final-context review after the face-detail pass confirms the cap, firefighter helmet, hood and sunglasses clear the refined brow and eyes; no gear rebuild was needed. - NPC headgear clearance (0.9.4 work): the 96-frame final-context review after the face-detail pass confirms the cap, firefighter helmet, hood and sunglasses clear the refined brow and eyes; no gear rebuild was needed.
@@ -71,6 +76,6 @@ Implemented so far:
- Keyboard/gamepad focus pass (0.9.3 work): `UiFocus` rings wrap through visible/enabled controls, hidden Continue and disabled timeline actions are skipped, each modal has a default focus, and A/B are bound to `ui_accept`/`ui_cancel` for pad-only navigation. Recorded in `docs/ui-copy.md`. - Keyboard/gamepad focus pass (0.9.3 work): `UiFocus` rings wrap through visible/enabled controls, hidden Continue and disabled timeline actions are skipped, each modal has a default focus, and A/B are bound to `ui_accept`/`ui_cancel` for pad-only navigation. Recorded in `docs/ui-copy.md`.
- Button-state pass (0.9.3 work): a shared `UiStyles` helper now gives every action button explicit normal, hover, pressed, focus and disabled styles; the timeline disables Previous/Next at its boundaries, while hidden continue states and the keyboard-only tutorial skip remain intentional. Recorded in `docs/ui-copy.md`. - Button-state pass (0.9.3 work): a shared `UiStyles` helper now gives every action button explicit normal, hover, pressed, focus and disabled styles; the timeline disables Previous/Next at its boundaries, while hidden continue states and the keyboard-only tutorial skip remain intentional. Recorded in `docs/ui-copy.md`.
- Dialogue pass (0.9.3 work): NPC reason answers now use the authored `reason` context, with an in-character answer for every shared archetype; Hollywood and Studio gained door-staff lines; radio cancellation, tutorial Ask terminology, help, start hints and campaign reset copy were clarified. Audit recorded in `docs/ui-copy.md`. - Dialogue pass (0.9.3 work): NPC reason answers now use the authored `reason` context, with an in-character answer for every shared archetype; Hollywood and Studio gained door-staff lines; radio cancellation, tutorial Ask terminology, help, start hints and campaign reset copy were clarified. Audit recorded in `docs/ui-copy.md`.
- Pre-build NPC review (2026-11-02): all eight library characters were staged at portrait, conversation and crowd distance on Hollywood and the Joke Shop, in day and night lighting, with the normal crowd hidden (`src/test/capture_npc_review.gd`). No P0 issues were found. Two P1 gear-fit issues were fixed with runtime attach offsets: the authorized-guest lanyard badge now clears the jacket and the firefighter helmet sits above the eyes. Backpack straps, caps, hands and the intentionally low-poly faces were accepted for the stylized target; close-range flashlight wash remains an optional-lighting follow-up. The 24-NPC budget stayed at 6.93 ms, the gallery passed 13 good / 0 bad and the screen-design judge stayed 13/13, and a clean portrait scored a 0.732 vision gap against the photoreal concept. - Pre-build NPC review (2026-11-02): all eight library characters were staged at portrait, conversation and crowd distance on Hollywood and the Joke Shop, in day and night lighting, with the normal crowd hidden (`src/test/capture_npc_review.gd`). No P0 issues were found. Two P1 gear-fit issues were fixed with runtime attach offsets: the authorized-guest lanyard badge now clears the jacket and the firefighter helmet sits above the eyes. Backpack straps, caps, hands and the intentionally low-poly faces were accepted for the stylized target; close-range flashlight wash remains an optional-lighting follow-up. The 24-NPC budget stayed at 6.94 ms, the gallery passed 13 good / 0 bad and the screen-design judge stayed 13/13, and a clean portrait scored a 0.732 vision gap against the photoreal concept.
Current gaps for the 0.x release line: deeper authored content/dialogue, animation and audio production, localization/accessibility expansion, 24-NPC/platform profiling, external playtesting, and signed/notarized release packaging. The full gap analysis and milestone plan live in [`TODO.md`](../TODO.md); the current architecture and documentation ownership map are in [`docs/architecture.md`](architecture.md). Current gaps for the 0.x release line: deeper authored content/dialogue, animation and audio production, localization/accessibility expansion, 24-NPC/platform profiling, external playtesting, and signed/notarized release packaging. The full gap analysis and milestone plan live in [`TODO.md`](../TODO.md); the current architecture and documentation ownership map are in [`docs/architecture.md`](architecture.md).
+9 -4
View File
@@ -93,7 +93,7 @@ pass every applicable item before a library rebuild is accepted.
authored grip points. authored grip points.
8. **Crowd and performance:** the silhouette remains readable at the 8 m crowd 8. **Crowd and performance:** the silhouette remains readable at the 8 m crowd
frame and the LOD/impostor tiers match the full mesh; the 24-NPC budget stays frame and the LOD/impostor tiers match the full mesh; the 24-NPC budget stays
within the existing 6.93 ms baseline (no regression beyond the measured within the existing 6.94 ms baseline (no regression beyond the measured
variance), and no new per-frame material or mesh churn is introduced. variance), and no new per-frame material or mesh churn is introduced.
Asset-space `fit` values are measurements of the untouched GLBs, not a pass/fail Asset-space `fit` values are measurements of the untouched GLBs, not a pass/fail
@@ -195,6 +195,8 @@ The refit backpack measures 380 triangles / 406 vertices.
shaping is the accepted stylized construction. shaping is the accepted stylized construction.
- The >26 m impostor remains a two-quad billboard; the LOD tier/hysteresis and - The >26 m impostor remains a two-quad billboard; the LOD tier/hysteresis and
near/far swap are covered by `test_avatar_mesh_budget_and_lod_swap`. near/far swap are covered by `test_avatar_mesh_budget_and_lod_swap`.
- NPC fabric textures are 256² with a 4× UV repeat; cloth/pants skip the baked
per-character AO to avoid large portrait blotches. Skin, hair and gear keep AO.
- Evening readability is represented by the day/night/LATE final-context set; - Evening readability is represented by the day/night/LATE final-context set;
LATE is the venue night variant. LATE is the venue night variant.
@@ -222,11 +224,14 @@ The face-detail pass is [`sheet_npc_baseline_compare-face-detail.jpg`](blender_p
The hand pass is [`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg); The hand pass is [`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg);
the forward-grip pass is the forward-grip pass is
[`sheet_npc_hands_grip_compare.jpg`](blender_previews/sheet_npc_hands_grip_compare.jpg). [`sheet_npc_hands_grip_compare.jpg`](blender_previews/sheet_npc_hands_grip_compare.jpg).
Keep the baseline sheet as the before artifact and add a new comparison sheet The 0.9.5 texture pass adds
rather than replacing it. [`sheet_texture_refinement_0.9.5.jpg`](blender_previews/sheet_texture_refinement_0.9.5.jpg),
which pairs the close-range surface and cloth comparisons. Keep the baseline
sheet as the before artifact and add a new comparison sheet rather than
replacing it.
## Next implementation order ## Next implementation order
0.9.4 is closed. The next visual work belongs to the 0.9.5 texture pass and the 0.9.4 and the 0.9.5 texture pass are closed. The next visual work belongs to the
0.10.0 acceptance review; keep the NPC baseline/final-context commands as the 0.10.0 acceptance review; keep the NPC baseline/final-context commands as the
regression gate for any future character edit. regression gate for any future character edit.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 KiB

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 KiB

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 KiB

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 KiB

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 830 KiB

After

Width:  |  Height:  |  Size: 841 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 529 KiB

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

+1 -1
View File
@@ -12,7 +12,7 @@ config_version=5
config/name="Gig Line" config/name="Gig Line"
config/description="Workplace security simulation about observation, access verification, and proportionate escalation." config/description="Workplace security simulation about observation, access verification, and proportionate escalation."
config/version="0.9.4" config/version="0.9.5"
run/main_scene="res://scenes/main.tscn" run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.7", "GL Compatibility") config/features=PackedStringArray("4.7", "GL Compatibility")
+6
View File
@@ -13,6 +13,10 @@ extends RefCounted
## asphalt replaced the flat first-generation tiles. Normal maps carry the mortar, ## asphalt replaced the flat first-generation tiles. Normal maps carry the mortar,
## paver joints and aggregate that Compatibility cannot get from screen space. ## paver joints and aggregate that Compatibility cannot get from screen space.
const PICK_DIR := "res://assets/textures/ai/game_ready/v2/picks/" const PICK_DIR := "res://assets/textures/ai/game_ready/v2/picks/"
## The 256² fabric maps are authored for a fine weave; repeating them 4×
## across the body keeps the pattern readable instead of magnifying one cell
## into a large blotch.
const NPC_FABRIC_UV_SCALE := Vector3(4.0, 4.0, 1.0)
const T_BRICK := preload(PICK_DIR + "brick_facade.png") const T_BRICK := preload(PICK_DIR + "brick_facade.png")
const T_CONCRETE := preload(PICK_DIR + "sidewalk_pavers.png") const T_CONCRETE := preload(PICK_DIR + "sidewalk_pavers.png")
const T_ASPHALT := preload(PICK_DIR + "asphalt.png") const T_ASPHALT := preload(PICK_DIR + "asphalt.png")
@@ -49,6 +53,8 @@ static func npc_slot(source: Material, tint: Color) -> StandardMaterial3D:
if material == null: if material == null:
material = StandardMaterial3D.new() material = StandardMaterial3D.new()
material.albedo_color = tint material.albedo_color = tint
if material.resource_name in ["cloth", "pants"]:
material.uv1_scale = NPC_FABRIC_UV_SCALE
return material return material
## Wire a baked AO map (red channel) into every surface of an imported mesh. ## Wire a baked AO map (red channel) into every surface of an imported mesh.
+4 -1
View File
@@ -483,7 +483,10 @@ func _apply_outfit_materials(npc_type: int, outfit: Dictionary, cloth: Color,
if not tints.has(slot): if not tints.has(slot):
continue continue
var material := Materials.npc_slot(source, tints[slot]) var material := Materials.npc_slot(source, tints[slot])
if ao != null: # The per-character baked AO is applied to skin, hair and fixed gear.
# On the 256² fabric maps it magnified into large patches at portrait
# distance, so cloth/pants keep their ORM weave and rely on lighting.
if ao != null and slot not in ["cloth", "pants"]:
material.ao_enabled = true material.ao_enabled = true
material.ao_texture = ao material.ao_texture = ao
material.ao_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_RED material.ao_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_RED
+26 -7
View File
@@ -17,10 +17,14 @@ const T_SKYLINE_NIGHT := preload(PICK_DIR + "skyline_night.png")
## Height of each backdrop plane (metres); the bases sit on the ground plane. ## Height of each backdrop plane (metres); the bases sit on the ground plane.
const SKYLINE_HEIGHT := 90.0 const SKYLINE_HEIGHT := 90.0
const SKYLINE_BY_PHASE := { const SKYLINE_BY_PHASE := {
Rules.Phase.EARLY: {"texture": T_SKYLINE_DAY, "tint": Color(0.98, 1.0, 1.04)}, Rules.Phase.EARLY: {"texture": T_SKYLINE_DAY, "tint": Color(0.98, 1.0, 1.04),
Rules.Phase.MIDDAY: {"texture": T_SKYLINE_DAY, "tint": Color(1.02, 1.04, 1.08)}, "haze": Color(0.62, 0.71, 0.82), "haze_strength": 0.36},
Rules.Phase.EVENING: {"texture": T_SKYLINE_DUSK, "tint": Color(0.9, 0.92, 1.0)}, Rules.Phase.MIDDAY: {"texture": T_SKYLINE_DAY, "tint": Color(1.02, 1.04, 1.08),
Rules.Phase.LATE: {"texture": T_SKYLINE_NIGHT, "tint": Color(1.5, 1.55, 1.65)}, "haze": Color(0.66, 0.75, 0.85), "haze_strength": 0.34},
Rules.Phase.EVENING: {"texture": T_SKYLINE_DUSK, "tint": Color(0.9, 0.92, 1.0),
"haze": Color(0.55, 0.43, 0.40), "haze_strength": 0.30},
Rules.Phase.LATE: {"texture": T_SKYLINE_NIGHT, "tint": Color(1.5, 1.55, 1.65),
"haze": Color(0.07, 0.09, 0.14), "haze_strength": 0.22},
} }
## steps, building facade, restricted door, sidewalk, pavement, and street edge. ## steps, building facade, restricted door, sidewalk, pavement, and street edge.
## Surfaces use the shared PBR profiles in `src/game/materials.gd`; street ## Surfaces use the shared PBR profiles in `src/game/materials.gd`; street
@@ -648,8 +652,10 @@ func _build_sunset_neighbours() -> void:
# Ground-floor shopfront and a red canvas awning. # Ground-floor shopfront and a red canvas awning.
_add_box(west, Vector3(8.6, 2.4, 0.12), Vector3(-22.6, 1.5, west_front + 0.08), _add_box(west, Vector3(8.6, 2.4, 0.12), Vector3(-22.6, 1.5, west_front + 0.08),
Color(0.08, 0.10, 0.12), false, null, Vector2.ONE, "glass") Color(0.08, 0.10, 0.12), false, null, Vector2.ONE, "glass")
# Keep the cream stripe: tinting the whole atlas dark red collapsed the
# canvas into a flat band at portrait distance.
_add_box(west, Vector3(9.2, 0.22, 1.3), Vector3(-22.6, 3.0, west_front + 0.65), _add_box(west, Vector3(9.2, 0.22, 1.3), Vector3(-22.6, 3.0, west_front + 0.65),
Color(0.48, 0.10, 0.08), true, T_AWNING, Vector2(3, 0.5), "awning") Color(0.62, 0.62, 0.62), true, T_AWNING, Vector2(4, 1), "awning")
# Balcony rails on the upper floor. # Balcony rails on the upper floor.
for x in [-24.8, -21.2, -17.6]: for x in [-24.8, -21.2, -17.6]:
_add_box(west, Vector3(2.4, 0.08, 0.85), Vector3(x, 5.6, west_front + 0.55), _add_box(west, Vector3(2.4, 0.08, 0.85), Vector3(x, 5.6, west_front + 0.55),
@@ -1379,8 +1385,10 @@ func _build_walk_of_fame() -> void:
var walk := Node3D.new() var walk := Node3D.new()
walk.name = "WalkOfFame" walk.name = "WalkOfFame"
root.add_child(walk) root.add_child(walk)
# Reuse the paver map as a dark terrazzo: a flat colour read as a black
# strip at close range, while the texture supplies the small aggregate.
_add_box(walk, Vector3(30.0, 0.035, 1.75), Vector3(0, 0.198, 9.15), _add_box(walk, Vector3(30.0, 0.035, 1.75), Vector3(0, 0.198, 9.15),
Color(0.20, 0.18, 0.17), false, null, Vector2.ONE, "stone") Color(0.34, 0.30, 0.27), false, T_CONCRETE, Vector2(18, 1.05), "concrete")
var star_mesh := _star_mesh(0.40, 0.17) var star_mesh := _star_mesh(0.40, 0.17)
var brass := Materials.flat(Color(0.70, 0.55, 0.24), "metal") var brass := Materials.flat(Color(0.70, 0.55, 0.24), "metal")
var coral := Materials.flat(Color(0.72, 0.30, 0.28), "stone") var coral := Materials.flat(Color(0.72, 0.30, 0.28), "stone")
@@ -1595,6 +1603,8 @@ func _build_skyline_backdrop() -> void:
mat.shader = shader mat.shader = shader
mat.set_shader_parameter("sky_texture", SKYLINE_BY_PHASE[_phase]["texture"]) mat.set_shader_parameter("sky_texture", SKYLINE_BY_PHASE[_phase]["texture"])
mat.set_shader_parameter("tint", SKYLINE_BY_PHASE[_phase]["tint"]) mat.set_shader_parameter("tint", SKYLINE_BY_PHASE[_phase]["tint"])
mat.set_shader_parameter("haze_color", SKYLINE_BY_PHASE[_phase]["haze"])
mat.set_shader_parameter("haze_strength", SKYLINE_BY_PHASE[_phase]["haze_strength"])
mat.set_shader_parameter("repeats", 2.0) mat.set_shader_parameter("repeats", 2.0)
_skyline_mat = mat _skyline_mat = mat
var mesh := PlaneMesh.new() var mesh := PlaneMesh.new()
@@ -1633,11 +1643,18 @@ render_mode unshaded, cull_disabled, fog_disabled;
uniform sampler2D sky_texture : source_color, repeat_enable, filter_linear_mipmap; uniform sampler2D sky_texture : source_color, repeat_enable, filter_linear_mipmap;
uniform vec4 tint : source_color = vec4(1.0); uniform vec4 tint : source_color = vec4(1.0);
uniform vec4 haze_color : source_color = vec4(0.62, 0.71, 0.82, 1.0);
uniform float haze_strength = 0.36;
uniform float repeats = 2.0; uniform float repeats = 2.0;
void fragment() { void fragment() {
vec4 panorama = texture(sky_texture, vec2(UV.x * repeats, UV.y)); vec4 panorama = texture(sky_texture, vec2(UV.x * repeats, UV.y));
ALBEDO = panorama.rgb * tint.rgb; // Atmospheric perspective: blend the lower part of the panorama toward
// the horizon colour so the towers sit in the distance instead of reading
// as a crisp cardboard cut-out.
float world_y = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).y;
float horizon = clamp(1.0 - world_y / 42.0, 0.0, 1.0);
ALBEDO = mix(panorama.rgb * tint.rgb, haze_color.rgb, horizon * haze_strength);
ALPHA = panorama.a * tint.a; ALPHA = panorama.a * tint.a;
ALPHA_SCISSOR_THRESHOLD = 0.5; ALPHA_SCISSOR_THRESHOLD = 0.5;
} }
@@ -2241,6 +2258,8 @@ func set_phase(phase: int) -> void:
if _skyline_mat != null: if _skyline_mat != null:
_skyline_mat.set_shader_parameter("sky_texture", entry["texture"]) _skyline_mat.set_shader_parameter("sky_texture", entry["texture"])
_skyline_mat.set_shader_parameter("tint", entry["tint"]) _skyline_mat.set_shader_parameter("tint", entry["tint"])
_skyline_mat.set_shader_parameter("haze_color", entry["haze"])
_skyline_mat.set_shader_parameter("haze_strength", entry["haze_strength"])
var night: bool = phase == Rules.Phase.EVENING or phase == Rules.Phase.LATE var night: bool = phase == Rules.Phase.EVENING or phase == Rules.Phase.LATE
set_night_mode(night) set_night_mode(night)
if _street_mat != null: if _street_mat != null:
+58 -13
View File
@@ -62,6 +62,18 @@ const SHOTS_JOKE_DAY := [
const SHOTS_STUDIO := [ const SHOTS_STUDIO := [
{"name": "studio_door", "pos": Vector3(0, 0.1, 6.2), "yaw": 0.0, "pitch": 2.0}, {"name": "studio_door", "pos": Vector3(0, 0.1, 6.2), "yaw": 0.0, "pitch": 2.0},
] ]
## Surface close-ups for the 0.9.5 texture audit; HUD is hidden for this stage.
const SHOTS_SURFACES := [
{"name": "surface_pavers", "pos": Vector3(0, 0.1, 10.5), "yaw": 0.0, "pitch": -55.0},
{"name": "surface_asphalt", "pos": Vector3(0, 0.1, 14.0), "yaw": 180.0, "pitch": -42.0},
{"name": "surface_limestone", "pos": Vector3(0, 0.1, 2.0), "yaw": 0.0, "pitch": 8.0},
{"name": "surface_facade_wide", "pos": Vector3(-2.0, 0.1, 13.2), "yaw": 12.0, "pitch": 20.0},
]
const SHOTS_SURFACES_JOKE := [
{"name": "surface_awning", "pos": Vector3(-22.6, 0.1, 2.6), "yaw": 0.0, "pitch": 14.0},
{"name": "surface_canopy", "pos": Vector3(0, 0.1, 7.5), "yaw": 0.0, "pitch": 24.0},
{"name": "surface_pavers_joke", "pos": Vector3(0, 0.1, 10.0), "yaw": 0.0, "pitch": -55.0},
]
## A face-on portrait: staged relative to a live NPC by `_stage_portrait()`. ## A face-on portrait: staged relative to a live NPC by `_stage_portrait()`.
const SHOTS_PORTRAIT := [ const SHOTS_PORTRAIT := [
{"name": "npc_portrait", "pos": Vector3.ZERO, "yaw": 0.0, "pitch": 0.0, {"name": "npc_portrait", "pos": Vector3.ZERO, "yaw": 0.0, "pitch": 0.0,
@@ -69,13 +81,16 @@ const SHOTS_PORTRAIT := [
] ]
## Stage list: shift time selects the phase, `venue` switches the scenario first. ## Stage list: shift time selects the phase, `venue` switches the scenario first.
const STAGES := [ const STAGES := [
{"name": "day", "elapsed": 0.0, "venue": "", "shots": SHOTS_DAY, "night": false}, {"name": "day", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_DAY, "night": false},
# Portrait next, while the day crowd is still on the street. {"name": "surfaces", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_SURFACES, "night": false},
{"name": "portrait", "elapsed": 0.0, "venue": "", "shots": SHOTS_PORTRAIT, # Portrait before the venue switch: re-staging a close-up after a rebuild is
# not reliable, and this keeps the day crowd on Hollywood.
{"name": "portrait", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_PORTRAIT,
"night": false}, "night": false},
{"name": "evening", "elapsed": 620.0, "venue": "", "shots": SHOTS_EVENING, "night": true}, {"name": "evening", "elapsed": 620.0, "venue": "hollywood_building", "shots": SHOTS_EVENING, "night": true},
{"name": "late", "elapsed": 760.0, "venue": "", "shots": SHOTS_LATE, "night": true}, {"name": "late", "elapsed": 760.0, "venue": "hollywood_building", "shots": SHOTS_LATE, "night": true},
{"name": "joke_shop_day", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_JOKE_DAY, "night": false}, {"name": "joke_shop_day", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_JOKE_DAY, "night": false},
{"name": "surfaces_joke", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_SURFACES_JOKE, "night": false},
{"name": "joke_shop", "elapsed": 700.0, "venue": "joke_shop", "shots": SHOTS_JOKE, "night": true}, {"name": "joke_shop", "elapsed": 700.0, "venue": "joke_shop", "shots": SHOTS_JOKE, "night": true},
{"name": "studio", "elapsed": 700.0, "venue": "tv_studio", "shots": SHOTS_STUDIO, "night": true}, {"name": "studio", "elapsed": 700.0, "venue": "tv_studio", "shots": SHOTS_STUDIO, "night": true},
] ]
@@ -166,6 +181,7 @@ func _process(delta: float) -> bool:
return false return false
func _begin_stage() -> void: func _begin_stage() -> void:
_clear_capture_pause()
_stage += 1 _stage += 1
while _stage < STAGES.size() and not _only_stages.is_empty() \ while _stage < STAGES.size() and not _only_stages.is_empty() \
and not _only_stages.has(str(STAGES[_stage]["name"])): and not _only_stages.has(str(STAGES[_stage]["name"])):
@@ -182,6 +198,7 @@ func _begin_stage() -> void:
gm.hud.clear_log() gm.hud.clear_log()
gm.hud.set_target_label("") gm.hud.set_target_label("")
gm.hud.set_action_hint("") gm.hud.set_action_hint("")
gm.hud.visible = not str(stage["name"]).begins_with("surfaces")
if _flashlight: if _flashlight:
var want := bool(stage["night"]) var want := bool(stage["night"])
if want != bool(gm.flashlight_on): if want != bool(gm.flashlight_on):
@@ -194,6 +211,7 @@ func _begin_stage() -> void:
print("capture_realism: stage %s at %d ms" % [stage["name"], Time.get_ticks_msec()]) print("capture_realism: stage %s at %d ms" % [stage["name"], Time.get_ticks_msec()])
func _run_queue(delta: float) -> void: func _run_queue(delta: float) -> void:
_clear_capture_pause()
if _queue.is_empty(): if _queue.is_empty():
_phase = "stage" _phase = "stage"
return return
@@ -226,9 +244,18 @@ func _run_queue(delta: float) -> void:
_queue.pop_front() _queue.pop_front()
_shot_frames = 0 _shot_frames = 0
## Capture staging must not inherit a pause overlay from the previous stage or a
## window-focus change; a paused frame hides the very surfaces the pass audits.
func _clear_capture_pause() -> void:
if gm == null:
return
var pause = gm.get("pause_overlay")
if pause != null and pause.visible:
gm.call("_close_pause")
## Frame a nearby NPC from the front, with the sun behind the camera so the face ## Frame a nearby NPC from the front, with the sun behind the camera so the face
## is lit. The subject's own yaw is re-written every frame by its FSM, so the ## is lit. The subject's FSM and idle head-turn are frozen so the still keeps the
## portrait turns the visual `Rig` child instead and stands on the sunlit side. ## face toward the lens; photo/film poses are skipped because their props cover it.
func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool: func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
var guard_pos: Vector3 = gm.guard.global_position var guard_pos: Vector3 = gm.guard.global_position
var light := _sun_direction() var light := _sun_direction()
@@ -239,7 +266,17 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
var npc := node as Node3D var npc := node as Node3D
if npc == null or _faces_covered(npc): if npc == null or _faces_covered(npc):
continue continue
if _portrait_type >= 0 and int(_profile_of(npc).get("npc_type")) != _portrait_type: var profile = _profile_of(npc)
if profile == null:
continue
if _portrait_type >= 0 and int(profile.get("npc_type")) != _portrait_type:
continue
# A film/photo pose holds a camera or phone in front of the face; keep the
# default portrait on an idle person so the texture pass sees skin.
var pose := ""
if npc.has_method("theater_pose"):
pose = str(npc.call("theater_pose"))
if _portrait_type < 0 and pose in ["photo", "film", "perform"]:
continue continue
var distance: float = npc.global_position.distance_to(guard_pos) var distance: float = npc.global_position.distance_to(guard_pos)
if distance > 25.0: if distance > 25.0:
@@ -265,9 +302,10 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
continue continue
other.set_physics_process(false) other.set_physics_process(false)
other.global_position += Vector3(0.0, 0.0, 140.0) other.global_position += Vector3(0.0, 0.0, 140.0)
# Freeze the subject: its FSM re-writes the yaw every physics tick, which # Freeze the subject: its FSM re-writes the yaw every physics tick, and the
# would turn it away from the camera again. # idle head-turn would still look away after the body is aimed at the camera.
best.set_physics_process(false) best.set_physics_process(false)
best.set_process(false)
# Clear standing spot on the sidewalk, away from the queue: the close-up is a # Clear standing spot on the sidewalk, away from the queue: the close-up is a
# character still, not a slice of the scene, and the crowd would block it. # character still, not a slice of the scene, and the crowd would block it.
var spot := Vector3(0.0, 0.0, 0.0) var spot := Vector3(0.0, 0.0, 0.0)
@@ -275,6 +313,11 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
spot = gm.guard.get("spawn_position") spot = gm.guard.get("spawn_position")
best.global_position = spot + _portrait_spot best.global_position = spot + _portrait_spot
best.rotation.y = 0.0 best.rotation.y = 0.0
var subject_head := best.get_node_or_null("Rig/Head") as Node3D
if subject_head == null:
subject_head = best.find_child("Head", true, false) as Node3D
if subject_head != null:
subject_head.rotation.y = 0.0
# Stand between the subject and the sun, so the face is front-lit. # Stand between the subject and the sun, so the face is front-lit.
var to_light := -light var to_light := -light
to_light = to_light.rotated(Vector3.UP, deg_to_rad(angle_deg)) to_light = to_light.rotated(Vector3.UP, deg_to_rad(angle_deg))
@@ -290,10 +333,12 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
# Face the camera: from the subject, the guard lies in the -look direction. # Face the camera: from the subject, the guard lies in the -look direction.
# Use the drawn rig so an FSM yaw on the root cannot fight the pose. # Use the drawn rig so an FSM yaw on the root cannot fight the pose.
var rig := best.find_child("Rig", true, false) as Node3D var rig := best.find_child("Rig", true, false) as Node3D
# NPC art faces +Z; yaw the root toward the guard so the face, not the
# back, is under the camera.
var face := atan2(-look.x, -look.z)
best.rotation.y = face
if rig != null: if rig != null:
rig.rotation.y = atan2(-look.x, -look.z) rig.rotation.y = 0.0
else:
best.rotation.y = atan2(-look.x, -look.z)
return true return true
+12 -3
View File
@@ -42,10 +42,19 @@ func _process(delta: float) -> bool:
if frame >= WARMUP_FRAMES + SAMPLE_FRAMES: if frame >= WARMUP_FRAMES + SAMPLE_FRAMES:
var average := sample_ms / SAMPLE_FRAMES var average := sample_ms / SAMPLE_FRAMES
var meshes := 0 var meshes := 0
var triangles := 0
for avatar in avatars: for avatar in avatars:
meshes += avatar.find_children("*", "MeshInstance3D", true, false).size() for node in avatar.find_children("*", "MeshInstance3D", true, false):
print("NPC budget: %d avatars, %d meshes, avg frame %.2f ms (budget %.0f ms)" % [ var mi := node as MeshInstance3D
COUNT, meshes, average, BUDGET_MS]) if mi == null or mi.mesh == null:
continue
meshes += 1
for surface in mi.mesh.get_surface_count():
var arrays := mi.mesh.surface_get_arrays(surface)
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
triangles += indices.size() / 3
print("NPC budget: %d avatars, %d meshes, %d triangles, avg frame %.2f ms (budget %.0f ms)" % [
COUNT, meshes, triangles, average, BUDGET_MS])
if average > BUDGET_MS: if average > BUDGET_MS:
printerr("SMOKE FAIL: frame time %.1f ms exceeds the %.0f ms budget" % [ printerr("SMOKE FAIL: frame time %.1f ms exceeds the %.0f ms budget" % [
average, BUDGET_MS]) average, BUDGET_MS])
+28
View File
@@ -3,6 +3,7 @@ extends RefCounted
## styles, queue slots, LOD tiers/labels, and the articulated rig structure. ## styles, queue slots, LOD tiers/labels, and the articulated rig structure.
const NpcAvatar := preload("res://src/game/npc_avatar.gd") const NpcAvatar := preload("res://src/game/npc_avatar.gd")
const Materials := preload("res://src/game/materials.gd")
const NpcProfile := preload("res://src/core/npc_profile.gd") const NpcProfile := preload("res://src/core/npc_profile.gd")
const Rules := preload("res://src/core/rules.gd") const Rules := preload("res://src/core/rules.gd")
@@ -333,6 +334,33 @@ func test_skinned_outfit_tints_the_material_slots() -> bool:
avatar.free() avatar.free()
return a and b and c and d return a and b and c and d
func test_fabric_uv_scale_avoids_the_baked_ao_pattern() -> bool:
var avatar := _avatar(Rules.NpcType.TOURIST, 44)
var mi := avatar.find_child("npc_base", true, false) as MeshInstance3D
var fabric := 0
var skin_ao := false
var problems: Array[String] = []
for index in mi.mesh.get_surface_count():
var source := mi.mesh.surface_get_material(index)
var material := mi.get_surface_override_material(index) as StandardMaterial3D
if source == null or material == null:
continue
var slot := str(source.resource_name)
if slot in ["cloth", "pants"]:
fabric += 1
if not material.uv1_scale.is_equal_approx(
Materials.NPC_FABRIC_UV_SCALE):
problems.append("%s uv=%s" % [slot, str(material.uv1_scale)])
if material.ao_enabled:
problems.append("%s still uses baked AO" % slot)
elif slot == "skin" and material.ao_enabled:
skin_ao = true
var a := TestUtils.check(fabric > 0 and problems.is_empty(),
"cloth/pants use the fine-UV scale and skip baked AO (%s)" % str(problems))
var b := TestUtils.check(skin_ao, "skin keeps its baked AO")
avatar.free()
return a and b
func test_forced_fallback_still_builds_the_procedural_rig() -> bool: func test_forced_fallback_still_builds_the_procedural_rig() -> bool:
var profile := NpcProfile.new() var profile := NpcProfile.new()
profile.id = "fallback_test" profile.id = "fallback_test"
+13 -4
View File
@@ -113,8 +113,13 @@ func test_hollywood_uses_the_real_landmark_layout() -> bool:
var banners := root.find_child("EventBanners", true, false) var banners := root.find_child("EventBanners", true, false)
var d := TestUtils.check(banners != null and banners.get_child_count() >= 9, var d := TestUtils.check(banners != null and banners.get_child_count() >= 9,
"three red event banners with their trim are present") "three red event banners with their trim are present")
var e := TestUtils.check(root.find_child("WalkOfFame", true, false) != null, var walk := root.find_child("WalkOfFame", true, false)
"the Hollywood Walk of Fame strip is built") var walk_band := walk.get_child(0) as MeshInstance3D if walk != null else null
var walk_material: Material = walk_band.mesh.surface_get_material(0) if walk_band != null else null
var e := TestUtils.check(walk != null and walk_material != null
and walk_material is StandardMaterial3D
and (walk_material as StandardMaterial3D).albedo_texture != null,
"the Hollywood Walk of Fame strip is built with the terrazzo texture")
var f := TestUtils.check(root.get_node_or_null("NeighbourBlocks/ElCapitanTheatre") != null var f := TestUtils.check(root.get_node_or_null("NeighbourBlocks/ElCapitanTheatre") != null
and root.get_node_or_null("NeighbourBlocks/EastRetailBlock") != null, and root.get_node_or_null("NeighbourBlocks/EastRetailBlock") != null,
"the El Capitan Theatre and east retail neighbours are built") "the El Capitan Theatre and east retail neighbours are built")
@@ -260,8 +265,12 @@ func test_skyline_backdrop_uses_alpha() -> bool:
var mat: ShaderMaterial = builder._skyline_mat var mat: ShaderMaterial = builder._skyline_mat
var b := TestUtils.check(mat != null and mat.shader != null, "skyline shader material captured") var b := TestUtils.check(mat != null and mat.shader != null, "skyline shader material captured")
var code := mat.shader.code if mat != null and mat.shader != null else "" var code := mat.shader.code if mat != null and mat.shader != null else ""
var c := TestUtils.check(code.contains("ALPHA") and code.contains("fog_disabled"), var c := TestUtils.check(code.contains("ALPHA") and code.contains("fog_disabled")
"backdrop shader is alpha-scissored and fog-free") and code.contains("haze_strength"),
"backdrop shader is alpha-scissored, fog-free and haze-blended")
var haze := float(mat.get_shader_parameter("haze_strength")) if mat != null else 0.0
var c2 := TestUtils.check(haze > 0.0, "skyline haze is enabled")
c = c and c2
var day: Texture2D = mat.get_shader_parameter("sky_texture") var day: Texture2D = mat.get_shader_parameter("sky_texture")
builder.set_phase(Rules.Phase.LATE) builder.set_phase(Rules.Phase.LATE)
var night: Texture2D = mat.get_shader_parameter("sky_texture") var night: Texture2D = mat.get_shader_parameter("sky_texture")
+1 -1
View File
@@ -46,7 +46,7 @@ BONES = [
] ]
MAX_TRIANGLES = 6000 MAX_TRIANGLES = 6000
MAX_VERTICES = 4000 MAX_VERTICES = 4000
TEX_SIZE = 128 TEX_SIZE = 256
AI_DIR = TEX_DIR / "ai" AI_DIR = TEX_DIR / "ai"
# Base material slots that get an AI fabric albedo (`prepare` layers them over # Base material slots that get an AI fabric albedo (`prepare` layers them over
# the procedural weave). Outer layers (jacket / vest / coat / dress) have no # the procedural weave). Outer layers (jacket / vest / coat / dress) have no