Compare commits

...
22 Commits
Author SHA1 Message Date
zwitschi ad14410337 Expand the photo-driven Gaussian splatting map plan
Full build / Import + headless tests (push) Successful in 43s
Full build / Full desktop build + optional release (push) Skipped
2026-11-03 09:07:07 +01:00
zwitschi 876fb59882 Add unversioned local map development section 2026-11-03 09:01:46 +01:00
zwitschi 979c6ad789 Make character development an unversioned TODO section 2026-11-03 08:24:55 +01:00
zwitschi 1921bfae91 Document the character development toolchain in the architecture
Full build / Import + headless tests (push) Successful in 39s
Full build / Full desktop build + optional release (push) Skipped
2026-11-03 08:22:09 +01:00
zwitschi 78485259b7 Detail deferred MakeHuman local pipeline boundary 2026-11-03 08:00:47 +01:00
zwitschi fef3af44c0 Record NPC sourcing decision and crowd prototypes 2026-11-03 07:59:58 +01:00
zwitschi bfe3c5a701 Research NPC model sources and plan 0.11.0 expansion 2026-11-03 07:46:47 +01:00
zwitschi 46f1e41874 Define release-candidate balance acceptance targets
Full build / Import + headless tests (push) Successful in 43s
Full build / Full desktop build + optional release (push) Skipped
2026-11-03 07:40:45 +01:00
zwitschi f48d87c84f Record the 0.10.0-rc.1 candidate build
Full build / Import + headless tests (push) Successful in 46s
Full build / Full desktop build + optional release (push) Skipped
2026-11-03 07:36:03 +01:00
zwitschi a9658ecd81 Prepare the 0.10.0 release candidate 2026-11-03 07:33:20 +01:00
zwitschi b8507df346 Harden release publishing and document release blockers
Full build / Import + headless tests (push) Successful in 45s
Full build / Full desktop build + optional release (push) Skipped
2026-11-03 07:32:21 +01:00
zwitschi 60bf9a8ef6 Add save migration fixtures and failure-mode coverage 2026-11-03 07:29:42 +01:00
zwitschi b51bdc8efe Open the 0.10.0 release-candidate milestone
Full build / Import + headless tests (push) Successful in 39s
Full build / Full desktop build + optional release (push) Skipped
2026-11-03 06:20:36 +01:00
zwitschi a7b2852253 Record the v0.9.6 release build
Full build / Import + headless tests (push) Successful in 39s
Full build / Full desktop build + optional release (push) Skipped
2026-11-03 06:19:38 +01:00
zwitschi 4bb2896819 Verify the 0.9.6 integration checkpoint
Full build / Import + headless tests (push) Successful in 53s
Full build / Full desktop build + optional release (push) Successful in 4m27s
2026-11-03 06:13:07 +01:00
zwitschi f5b8ef2e70 Prepare the 0.9.6 integration checkpoint 2026-11-03 06:11:17 +01:00
zwitschi 0044f5f065 Record the v0.9.5 release build
Full build / Import + headless tests (push) Successful in 43s
Full build / Full desktop build + optional release (push) Skipped
2026-11-02 21:12:00 +01:00
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
91 changed files with 2129 additions and 206 deletions

No files matched your search

+38 -6
View File
@@ -4,6 +4,44 @@ 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.10.0-rc.1] — 2026-11-03
### Changed
- Prepared the release-candidate packages for Linux, Windows, macOS, Web and the
optional Desktop HQ zip. The Web/HQ archives now include the credits, privacy,
accessibility and known-limitations pages, and release publishing writes
SHA-256 checksums and rolls back partial uploads.
### Fixed
- Corrupt or partial save files now fall back to safe defaults without logging a
parse error, and a failed atomic replace leaves the previous valid settings
intact. Added migration coverage for 0.8-era profiles and 0.9.0 checkpoints
across all three venues.
## [0.9.6] — 2026-11-02
### Changed
- The UI, model and texture passes are now integrated into a verified checkpoint
build. The gallery, NPC portraits, realism captures, asset audits, smoke
matrix, 24-NPC budget and Web/Desktop HQ packages were regenerated and
re-checked; no gameplay or content changes were introduced.
## [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
### Changed
@@ -240,12 +278,6 @@ Semantic Versioning.
## [0.7.0] — 2026-11-01
### Changed
- Agent workflow now requires maintaining this changelog for every patch, minor, and major version.
## [Unreleased]
### Added
- Distinct venue geometry: the Joke Shop is now a narrow brick alley with a
+83
View File
@@ -0,0 +1,83 @@
# Credits and Provenance
Gig Line is a local, single-player security-shift game built with Godot 4 and
GDScript. This file records the asset provenance needed for the release review;
it is not a legal opinion or a claim that every provider licence has already
been cleared for public distribution.
## Game, code and design
- Game design, systems, scenarios, dialogue and UI code: the Gig Line project.
- Engine: [Godot Engine 4.7.2](https://godotengine.org/), MIT licence.
- The game bundles no third-party fonts; it uses Godot's default theme font.
- No third-party music, sound effects or voice recordings are bundled. Crowd
ambience and radio cues are generated procedurally at runtime.
## 3D characters, gear and props
The character, gear and prop library is generated by project-owned scripts
(`tools/blender_assets.py`, `tools/npc_pipeline.py`, `tools/model_pipeline.py`)
using Blender. The `.blend` working sources are developer artifacts and are not
shipped. The exported `.glb` files and baked AO maps are project assets; they do
not embed third-party models or purchased asset packs.
### Development-only research previews
Two review sheets under `docs/blender_previews/` compare source candidates for
the future NPC variety work. They are development documentation and are not
bundled with the game or its release packages. The underlying source models are
kept outside version control.
- [`sheet_npc_sources_research.jpg`](docs/blender_previews/sheet_npc_sources_research.jpg)
includes renders of **Quaternius Ultimate Modular Characters** (CC0 1.0,
<https://quaternius.com/>), **Kenney Blocky Characters 2.0** (CC0 1.0,
<https://kenney.nl/assets/blocky-characters>), and **CesiumMan** and
**RiggedFigure** (CC BY 4.0, © 2017 Cesium,
<https://github.com/KhronosGroup/glTF-Sample-Assets>).
- [`sheet_npc_crowd_prototypes.jpg`](docs/blender_previews/sheet_npc_crowd_prototypes.jpg)
contains renders of project-owned generator prototypes.
The CC-BY preview images are included only as development evidence. Any future
shipped derivative of CesiumMan or RiggedFigure must keep the attribution above.
The venue geometry is authored procedurally in `src/game/world_builder.gd` as a
stylized reconstruction. The Joke Shop is a fictional stand-in for the real
Sunset Strip block; the game does not claim endorsement by, or an exact replica
of, any real venue or tenant.
## AI-assisted images
Surface textures, skyline panoramas, UI art and concept references were produced
with image-generation calls from the project's tooling. The default model slugs
recorded in the source are:
- `bytedance-seed/seedream-4.5` — `tools/generate_textures.py`
- `openai/gpt-5-image-mini` — `tools/asset_pipeline.py`,
`tools/model_pipeline.py`, `tools/scene_concepts.py` and
`tools/screen_designs.py`
- Judging/review calls use the model selected by `tools/openrouter_judge.py` and
are development-only; they do not ship in the game.
The prompts and generation workflow live in those tools. Per-file generation
metadata is not stored with every image, so the exact provider, model version and
generation date of each committed bitmap cannot be reconstructed from the
repository alone. Before a public 0.10.0 release, the provider terms and
commercial-use/redistribution rights for those generated outputs must be
reviewed and recorded. **This is a release blocker, not an assumed clearance.**
Reference photographs linked from the venue documents are hosted on Wikimedia
Commons and were used for proportion/material study only. They are not included
in the game or its release packages. If any future derivative asset copies a
reference more directly, its attribution and licence must be added here.
## Audio
All runtime audio is procedural (crowd bed, radio crackle and interface cues).
There are no external audio files, so there is no separate music/SFX licence to
clear. See `docs/accessibility.md` for caption and volume behaviour.
## Third-party names
The game is a work of fiction. Real place names and public references are used
for orientation and satire/stand-in geography; no real person, venue or brand
endorses the game. Review the storefront and trademark language before release.
+155
View File
@@ -1878,3 +1878,158 @@ 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
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.
Tagged `v0.9.4`; Gitea run 916 passed checks and the full build and published seven assets.
## Patch 0.9.5 — Texture and material refinement
Goal: improve close-range surface and character texture quality while keeping GL
Compatibility/Web parity, deterministic assets and offline CI.
### Environment textures and materials
- [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
seams, tiling, scale, contrast and normal-map strength. (New `surfaces` and
`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
pick, use `-vN` candidates and promote validated results. (The Walk of Fame
band now uses the paver/terrazzo texture; the neighbour awning keeps its
cream stripe via a neutral tint and 4× UV. Brick/asphalt picks held up.)
- [x] Check UV scales and material roughness/metallic profiles; keep day/night
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
- [x] Audit NPC skin, cloth, pants, hair and gear albedo/normal/AO maps in portraits;
refine fabric weave, skin variation and fixed detail materials. (256² maps rebuilt; fabric
now uses a 4× UV weave and skips baked per-character AO, removing the portrait blotches;
skin/hair/gear keep AO. Comparison `sheet_npc_baseline_compare-texuv.jpg`.)
- [x] Re-bake AO where geometry changes; verify the 256² maps and material wiring.
(No character geometry changed in the texture pass; asset/base audits pass.)
- [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
- [x] Run `python3 tools/scene_concepts.py realism` for the affected stages with the
judge enabled; record metric gaps and model/spend. (Run
`docs/screens/realism/2026-11-02-tex` with `z-ai/glm-5.3-flash`, one pass per
shot; mean `after` overall 1.8, lighting 2.3, materials 1.8, texture 1.2,
backdrop 2.0, characters 1.2, Pillow gap 0.306. The judge is optional and not
a CI gate; the report stays local.)
- [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.)
Completed 2026-11-02: all 0.9.5 texture and material checks are closed. The NPC
base and eight library characters use 256² maps, cloth/pants use the 4× fabric
weave without baked-AO blotches, the Walk of Fame band uses the paver/terrazzo
texture, the neighbour awning keeps its cream stripe, and the skyline fades into
per-phase haze. Godot is 263/263, Python tooling 101/101, `blender_assets.py
audit`/`fit` and `npc_pipeline.py audit` pass, the 24-NPC headless smoke is
6.94 ms (168 meshes / 131,616 triangles), the 48-frame baseline and 96-frame
final-context review pass, `check_screenshots.sh` is 21 good / 0 bad, and the
screen-design judge is 21/21 match. The optional realism judge ran with
`z-ai/glm-5.3-flash` (mean overall 1.8, Pillow gap 0.306) and its report remains
local; the committed comparison sheet is
`docs/blender_previews/sheet_texture_refinement_0.9.5.jpg`. Tagged `v0.9.5`;
Gitea run 922 passed checks and the full build and published seven assets.
## Patch 0.9.6 — Integration, performance and judge gate
Goal: integrate the UI/model/texture patches, prove the combined build on the
target paths and leave a clean release-candidate handoff.
### Integration and asset rebuild
- [x] Rebuild all changed Blender/GLB/AO assets, re-import, and verify manifest/fit/
audit coverage. (The v0.9.5 commit already rebuilt the base, eight characters,
gear and AO maps; this checkpoint re-imported them and re-ran
`blender_assets.py audit`/`fit` and `npc_pipeline.py audit`, all exit 0.)
- [x] Run the complete headless suite, Python tooling and smoke matrix; fix any
regression from the three polish patches. (Godot 263/263, Python 101/101;
14 smokes green: audio, campaign, crowd, emergency, focus/radial, movement,
NPC budget, PLAN verbs, props, scenario content, session shell, timeline,
tutorial and UI flows. Campaign/tutorial used isolated `XDG_DATA_HOME`.)
- [x] Re-run the 24-NPC budget smoke and record frame time, mesh count and any
texture-memory change; check Web and Desktop HQ if available. (24 avatars,
168 meshes, 131,616 triangles, 6.90 ms of 50 ms headless; NPC maps remain
0.48 MB. A clean detached worktree at `f5b8ef2` built Linux/Windows/macOS/
Web/HQ (`161.5/197.1/148.0/97.8/241.5 MB`); the Web zip has index.html,
wasm and pck, the HQ zip has `override.cfg`, and the HQ binary launches
headless with the Forward+ override.)
- [x] Verify save/campaign/tutorial behavior is unchanged by the visual/UI patches.
(Persistence/session tests remain in the 263-test suite; campaign and tutorial
smokes passed with isolated `XDG_DATA_HOME`.)
### Visual gallery and judge gate
- [x] Recapture the full gallery, portraits, venue/realsim stages and Desktop HQ A/B
as needed; no stale screenshots in the release set. (Gallery 21/21,
NPC baseline 48 frames, final-context review 96 frames and a fresh 10-shot
realism run were recaptured. No Forward+ code changed since 0.7.1, so the
HQ A/B was not re-staged; the HQ artifact is checked with the release build.)
- [x] Run `tools/check_screenshots.sh`, `screen_designs.py compare` and
`screen_designs.py judge` on the final captures. (21 good / 0 bad; compare
21 good / 0 bad; judge 21/21 match, 0 review, 0 diff.)
- [x] Run the realism judge for the affected concepts with a pinned model; record
model, attempt count, spend and verdicts. (`z-ai/glm-5.3-flash`, 1 pass per
shot, 10 shots, no fallback/retry: mean overall 2.1, lighting 2.4, materials
2.1, texture 1.7, backdrop 2.3, characters 1.8, Pillow gap 0.291. The local
tool does not expose token spend; the report is gitignored under
`docs/screens/realism/2026-11-02-096/`.)
- [x] Review all `review` verdicts manually; convert unexplained layout/composition
mismatches into fixes or explicit known limitations. (Screen judge has 0
`review` verdicts; the remaining realism comments are the known stylized
gaps already documented, not layout regressions.)
- [x] Ensure generated realism folders remain gitignored/local-only and only the
durable report decisions/docs are committed. (`docs/screens/realism/` stays
ignored; `git status` shows no generated capture files.)
### Release-candidate handoff
- [x] Update README, architecture, development status, changelog and TODO with the
final patch-level results and any remaining gaps. (README/PLAN/architecture
now point at 0.9.6; the changelog and development status record the
integration build, performance numbers, judge results and the clean-worktree
export rule; `docs/RELEASING.md` documents the ignored-folder trap.)
- [x] Tag/version or otherwise freeze `0.9.6` as the integration checkpoint before
the `0.10.0` release candidate. (Tag `v0.9.6` on `4bb2896`; Gitea run 1296
passed checks and the full build and published seven assets.)
- [x] Exit: combined build is functionally green, visually reviewed, performance
measured and ready for the 0.10.0 content-lock/playtest phase.
Completed 2026-11-03: the 0.9.6 integration checkpoint is closed. The current
asset set re-imported and passed `blender_assets.py audit`/`fit` and
`npc_pipeline.py audit`; the full suite is 263/263 with 101/101 Python tests and
14 smokes green; the 24-NPC budget is 6.90 ms with 168 meshes and 131,616
triangles; the gallery and screen-design gate are 21 good / 0 bad and 21/21
match; the pinned realism judge (`z-ai/glm-5.3-flash`) scored mean overall 2.1
with a 0.291 Pillow gap. A clean worktree built Linux/Windows/macOS/Web/HQ at
161.5/197.1/148.0/97.8/241.5 MB; the Web zip and HQ override were checked and
the HQ binary launched headless. Tagged `v0.9.6`; Gitea run 1296 passed checks
and the full build and published seven assets.
+113 -19
View File
@@ -4,6 +4,40 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
## Decisions
- **Gaussian splatting / photo-derived maps (unversioned local track):** expanded in
`TODO.md` "Gaussian splatting evaluation". Accepted photo channels are team captures,
permissioned user/volunteer photos and clearly licensed downloads; every set needs a
provenance/consent sidecar. Sparse web photos are reference/training supplements, not a splat
source. Five enhancement paths are ranked (reference/QA, texture bake, mesh proxy, distant
backdrop, single-facade user-photo supplement). Splats stay offline/development-only until the
ADR proves licence, privacy, Godot Compatibility/Web cost and mesh-based collision; no runtime
splat dependency in 0.10.0.
- **NPC model variety decision (2026-11-03, ADR-0015):** CC0 is acceptable for shipped
assets; CC-BY only with attribution in `CREDITS.md`; no service assets with redistribution
restrictions. The **procedural generator is the primary path**. Four crowd-variety specs were
prototyped with the existing builder (`npc_crowd_a` bun/slim/sunglasses, `npc_crowd_b`
bald/broad/beard/shoulder bag, `npc_crowd_c` coat/umbrella, `npc_crowd_d` hood/cup); all audit
clean at 4,550–5,258 tris / 4,356–5,086 verts. Prototype assets stay in
`tmp/npc-research/generator_proto/`; do not add them to the current release candidate.
**Quaternius is a reference, not a shipped base:** welding brings `Casual_Hoodie` from 12,518
to 3,199 verts and a 0.75 decimate to 2,423 verts / 4,654 tris, and its Idle/Walk clips read
well, but the T-pose 62-bone realistic rig/proportions do not match the arm-down 13-bone
contract. A contract rebuild is a no-go without a body proportion warp; a second
rig/animation path needs its own ADR. **MakeHuman is deferred** to a separate local pipeline:
the CC0 base is 19,158 verts / 36,972 tris and decimates to 3,780 / 6,468 at 0.35, but needs
retopology, a custom rig and project clothing. Research previews are committed under
`docs/blender_previews/sheet_npc_{sources_research,crowd_prototypes}.jpg`; the CC-BY entries
(CesiumMan/RiggedFigure, © Cesium) are attributed in `CREDITS.md` and are development-only.
- **0.10.0 balance protocol:** `docs/balance-acceptance.md` defines targets and
denominator/cohort rules for the 36 venue/difficulty/phase cells, seeds 1–100,
access/emergency error rates and matched calm vs emergency-first comparisons.
These are acceptance criteria, not measured results; sweeps/playtests remain open.
Inspection found `_read_npc_truth()` is type-derived, not behavior-derived;
the pending emergency fairness review must cover this oracle mismatch rather
than treating internal truth as independent evidence.
- The visual-alignment path/loader/panel/HUD work and the realism lighting pass are complete and
recorded in `DONE.md`; remaining visual items are the realism follow-ups in `TODO.md`.
- **Character mesh cap (2026-11-02):** the eight library variants now use **8,000 triangles /
@@ -63,10 +97,32 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
`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
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
portrait measured a 0.732 vision gap against the photoreal concept. No
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 272/272.
- **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,
crowd). It uses a fixed orbit camera, mid-grey studio backdrop, key/fill lights, no HUD/crowd/animation;
@@ -152,16 +208,53 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
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.
- **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
completed map/NPC work and UI polish are the patch content. Next work is
**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
major-version scope is defined yet. The v0.9.4 tag is the release trigger; no
polish milestone, **0.9.4** after the NPC pass, now **0.9.5** after the texture
and material pass, and **0.9.6** as the integration checkpoint. Next work is
the **0.10.0** minor release candidate. `TODO.md` plans only 0.x patch/minor increments; no
major-version scope is defined yet. The `v0.9.6` tag is the release trigger; no
tag was created for 0.9.3.
- **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.5 textures and 0.9.6 integration/0.10.0 release work are next. The current
source version is 0.9.4; the v0.9.4 tag triggers the CI build.
0.9.3 UI polish, 0.9.4 NPC visual optimization, 0.9.5 texture/material pass and
0.9.6 integration are complete and in `DONE.md`. 0.10.0 release-candidate work
is next. The source version is 0.10.0-rc.1.
- **0.10.0-rc.1 build (2026-11-03):** clean worktree `a9658ec` produced five
artifacts (Linux 161.5 MB, Windows 197.2 MB, macOS 148.0 MB, Web 97.8 MB, HQ
241.6 MB); Web/HQ include player docs, SHA-256 manifests are local, Linux/HQ
launch headless and Web files serve HTTP 200. Not published: external playtest,
AI/legal approval, macOS signing and the Windows/macOS matrix are still open.
- **Release/legal blockers (0.10.0 work, 2026-11-03):** macOS remains unsigned
(quarantine instructions only); AI image provider terms/commercial rights and
external storefront/rating approval are not cleared; external playtests and the
full hardware matrix are pending. `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md`
and `docs/known-limitations.md` record these explicitly rather than claiming
approval. Do not tag 0.10.0 as final until these are accepted or cleared.
- **Save migration (0.10.0 work, 2026-11-03):** fixtures in `src/test/fixtures/saves/`
cover 0.8 settings/campaign/history and a 0.9.0 checkpoint. `_load_json` uses
`JSON.new().parse()` so corrupt player files fall back silently instead of
logging a parse error; atomic writes leave the old file intact when the temp
path cannot be written. The migration suite is 9 tests; total 272.
- **0.9.6 integration (2026-11-03):** no gameplay/content changes. A clean
detached worktree at `f5b8ef2` rebuilt Linux/Windows/macOS/Web/HQ without the
dirty tree's ignored `tmp/`/`build/` payload; the in-repo build was 1.6–3.1 GB
per artifact because those folders were packed, so always build release
artifacts from a clean checkout/worktree. Verification: 263/263 tests,
101/101 Python, 14 smokes, 24-NPC 6.90 ms / 168 meshes / 131,616 triangles,
gallery 21/0, screen judge 21/21, realism judge overall 2.1 / gap 0.291.
- **Release v0.9.6 (2026-11-03):** commit `4bb2896` tagged `v0.9.6`; Gitea
Actions run 1296 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.6`. This is
the integration checkpoint before 0.10.0; do not reuse the tag.
- **Release v0.9.5 (2026-11-02):** commit `4bc6390` tagged `v0.9.5`; Gitea Actions run
922 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.5`. Do not
reuse the tag; future fixes need a new patch tag.
- **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
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
@@ -184,7 +277,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
`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 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
only in directories containing tracked files, verifies local links and requires
every tracked image in a folder to be linked from its README. Gitignored
@@ -253,7 +346,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.
`generate --role tile` already applies `make_seamless`, so a separate
`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`).
`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 /
@@ -798,7 +891,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.
- 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()`.
- 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 **272/272**; 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).
- 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
@@ -821,16 +914,17 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
## Plan vs code
`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
UI/accessibility pass, the 0.9.4 NPC visual optimization, 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
2026-11-03 the playable build covers the three venues, tutorials/campaign/checkpoints, the 0.9.3
UI/accessibility pass, the 0.9.4 NPC visual optimization, the 0.9.5 texture/material pass, the
0.9.6 integration checkpoint, and the Blender + realism asset pipeline. `TODO.md` holds the remaining
0.9.6–0.10.0 work. The project uses a custom headless runner, not GUT, and Gitea Actions, not
GitHub Actions.
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
## Uncommitted Workstreams
- **None (2026-11-01).** The NPC detail pass, the realism follow-ups and the flashlight beam
decision are committed together with the portrait capture and the refreshed screenshots.
Keep the rule from `AGENTS.md`: check `git status` before committing, and never revert
another contributor's in-flight files.
- **None (2026-11-03).** The 0.9.6 integration checkpoint is committed, tagged
`v0.9.6` and published with seven assets; the workspace is clean apart from
local/gitignored realism and capture output. Keep the rule from `AGENTS.md`: check `git status`
before committing, and never revert another contributor's in-flight files.
+2 -1
View File
@@ -176,7 +176,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.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.10.0-rc.1`). 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
@@ -189,3 +189,4 @@ The game should feel like a tense workplace simulation with occasional crisis mo
- A rare Bob Dobbs cameo (Easter-egg appearance; placement note in `MEMORY.md`).
- Specific unusual NPC behaviors, such as slapping their own face, using hands as scissors, or interacting with props unexpectedly.
- Live 3D body-camera replay; additional venues or seasonal events; real voice/dialogue audio; an image-to-mesh prop pipeline.
- More varied NPC silhouettes (age, build, hair and outfit), using either an extended procedural character generator or a reviewed CC0/CC-BY source; the evaluation and pending decision are in `docs/npc-model-sourcing.md`.
+27
View File
@@ -0,0 +1,27 @@
# Privacy Note
Gig Line runs locally. The game itself does not include telemetry, analytics,
accounts, advertising, cloud saves or network requests. It does not access the
camera, microphone, contacts or location, and the body-camera feature is a
simulated overlay, not a real recording device.
Player data is written only to Godot's local `user://` directory:
- `settings.json`
- `campaign.json`
- `shift_results.json`
- `shift_state.json`
Those files stay on the player's machine unless the player copies them. Deleting
the files (or the `Gig Line` user directory) resets the profile. See
[`docs/getting-started.md`](docs/getting-started.md#save-data-and-migration) for
locations, migration and rollback instructions.
The repository's development tools can call OpenRouter when a developer supplies
`OPENROUTER_API_KEY`; those calls generate or judge development assets and are
not part of the shipped game. The key and generated review artifacts remain
local/gitignored.
Questions about a storefront release should be checked against the final package
and the provider terms recorded in [`CREDITS.md`](CREDITS.md); external legal and
storefront approval is still pending for 0.10.0.
+29 -11
View File
@@ -38,12 +38,14 @@ 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
> `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31.
>
> **Latest published: v0.9.4** — the NPC visual optimization pass: face/head
> depth, hands and gear placement, shoes and boots, headgear clearance, and the
> 8,000/8,000 character mesh budget on top of the Hollywood/Joke Shop maps and
> the v0.9.3 UI/accessibility polish.
> **Latest published: v0.9.6** — the integration checkpoint: the 0.9.5 texture
> and material refinement (256² NPC maps, a four-times fabric weave without baked
> AO blotches, repaired Walk of Fame/awning surfaces and per-phase skyline haze)
> is packaged with the v0.9.4 NPC visual optimization, the Hollywood/Joke Shop
> maps and the v0.9.3 UI/accessibility polish, with the full gallery, smoke matrix
> and Web/Desktop HQ build re-verified.
>
> **Current source: v0.9.4** — tagged and built from the same commit.
> **Current source: v0.10.0-rc.1** — the release-candidate source; the final tag is pending external playtest and legal review.
> 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
@@ -119,24 +121,40 @@ Full setup, every run/test/smoke command, and the controls table are in
- [`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.
- [`docs/accessibility.md`](docs/accessibility.md) — Accessibility features, known gaps and statement.
- [`docs/known-limitations.md`](docs/known-limitations.md) — Release-candidate blockers and accepted limitations.
- [`docs/smoke-checklist.md`](docs/smoke-checklist.md) — Manual smoke-test checklist.
- [`CREDITS.md`](CREDITS.md) — Credits, AI/reference provenance and licensing notes.
- [`PRIVACY.md`](PRIVACY.md) — Local-only data, save files and development API use.
- [`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/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/adr/`](docs/adr/) — Architecture Decision Records.
- [`TODO.md`](TODO.md) — Active tasks plus the 0.x gap analysis and milestone roadmap.
## Development Status
v0.9.4 is the current source version: the completed NPC visual optimization
(face/head depth, hands and gear placement, shoes/boots, headgear clearance and
the 8,000/8,000 mesh budget), on top of the v0.9.3 UI/accessibility polish
pass and v0.9.2's Hollywood/Joke Shop map rebuilds. It builds on
v0.9.1's Godot 4.7.2 baseline, v0.9.0's production-quality pass, v0.8's
v0.10.0-rc.1 is the current release-candidate source: the 0.9.6 integration
checkpoint plus save-migration hardening and release tooling, on top of the 0.9.5
texture refinement (256² NPC maps, a 4× fabric weave with baked-AO removal, repaired
Walk of Fame/awning surfaces and per-phase skyline haze), on top of the v0.9.4
NPC visual optimization and the v0.9.3 UI/accessibility polish pass. It builds on
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)
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).
## Credits, Privacy and Accessibility
Gig Line is a local-only single-player game: there is no runtime telemetry or
network service, and saves stay in Godot's local `user://` folder. The
body-camera overlay is simulated. Asset provenance, AI-assisted image usage and
credits are recorded in [`CREDITS.md`](CREDITS.md); the local-data policy is in
[`PRIVACY.md`](PRIVACY.md); accessibility features and gaps are in
[`docs/accessibility.md`](docs/accessibility.md); and release-candidate blockers
are listed in [`docs/known-limitations.md`](docs/known-limitations.md).
## Development Workflow
Follow [`AGENTS.md`](AGENTS.md): inspect the repository first, record work as checkbox tasks in `TODO.md`, move completed sections to `DONE.md`, update `MEMORY.md` with durable context, and keep this README synchronized with setup and architecture changes.
+333 -112
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
Baseline: **v0.9.4** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume,
Baseline: **v0.9.6** (2026-11-03). Three playable venues, tutorial/campaign/checkpoint resume,
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, 272 headless + 101 Python
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.
@@ -37,105 +37,39 @@ 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) |
| 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 |
| 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 |
| 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 |
| Documentation | Architecture/ADR/status/roadmap ownership established | Final README/changelog/status/TODO/smoke-checklist sign-off and known-limitations page | Docs drift at launch | 0.9.6 / 0.10.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+ |
| 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 | Unscheduled |
**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
(all moved to `DONE.md` on 2026-11-02). 0.9.5 texture refinement, 0.9.6
integration and the 0.10.0 release candidate are queued. The source version is
now 0.9.4.
**0.9.3 UI polish**, **0.9.4 NPC visual optimization**, **0.9.5 texture
refinement** and **0.9.6 integration** passes are complete (all moved to
`DONE.md`). The **0.10.0 release candidate** is next: content lock, external
playtest, balance, save migration and release/legal sign-off. The source version
is 0.10.0-rc.1; the v0.9.6 release is published.
## Postponed — Patch 0.9.5 — Texture and material refinement
Goal: improve close-range surface and character texture quality while keeping GL
Compatibility/Web parity, deterministic assets and offline CI.
### Environment textures and materials
- [ ] 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
seams, tiling, scale, contrast and normal-map strength.
- [ ] Refine brick mortar/wear, paver joints/stains, asphalt aggregate/wet response
and awning weave using the existing image/asset tooling; never overwrite a
pick, use `-vN` candidates and promote validated results.
- [ ] Check UV scales and material roughness/metallic profiles; keep day/night
phase behavior and the wet-street transition intact.
- [ ] Review distant skyline haze and foliage/planter surfaces against the concept
references; fix obvious flatness without regressing readability.
### Character and prop textures
- [ ] Audit NPC skin, cloth, pants, hair and gear albedo/normal/AO maps in portraits;
refine fabric weave, skin variation and fixed detail materials.
- [ ] Re-bake AO where geometry changes; verify the 256² maps and material wiring.
- [ ] Audit library props and UI/card/icon textures at their actual screen sizes;
remove blurry/oversized or unused textures where safe.
- [ ] Run texture audits, import, and the full asset test suite; record texture memory
and triangle/draw-call impact.
### Visual verification
- [ ] Run `python3 tools/scene_concepts.py realism` for the affected stages with the
judge enabled; record metric gaps and model/spend.
- [ ] Re-run `tools/check_screenshots.sh` and the screen-design judge; clear all
`diff` verdicts.
- [ ] Capture before/after close-ups for brick, pavement, asphalt, character cloth
and skyline; document accepted trade-offs.
- [ ] Update asset docs and the changelog/status with the promoted texture versions.
## Postponed — Patch 0.9.6 — Integration, performance and judge gate
Goal: integrate the UI/model/texture patches, prove the combined build on the
target paths and leave a clean release-candidate handoff.
### Integration and asset rebuild
- [ ] Rebuild all changed Blender/GLB/AO assets, re-import, and verify manifest/fit/
audit coverage.
- [ ] Run the complete headless suite, Python tooling and smoke matrix; fix any
regression from the three polish patches.
- [ ] Re-run the 24-NPC budget smoke and record frame time, mesh count and any
texture-memory change; check Web and Desktop HQ if available.
- [ ] Verify save/campaign/tutorial behavior is unchanged by the visual/UI patches.
### Visual gallery and judge gate
- [ ] Recapture the full gallery, portraits, venue/realsim stages and Desktop HQ A/B
as needed; no stale screenshots in the release set.
- [ ] Run `tools/check_screenshots.sh`, `screen_designs.py compare` and
`screen_designs.py judge` on the final captures.
- [ ] Run the realism judge for the affected concepts with a pinned model; record
model, attempt count, spend and verdicts.
- [ ] Review all `review` verdicts manually; convert unexplained layout/composition
mismatches into fixes or explicit known limitations.
- [ ] Ensure generated realism folders remain gitignored/local-only and only the
durable report decisions/docs are committed.
### Release-candidate handoff
- [ ] Update README, architecture, development status, changelog and TODO with the
final patch-level results and any remaining gaps.
- [ ] Tag/version or otherwise freeze `0.9.6` as the integration checkpoint before
the `0.10.0` release candidate.
- [ ] Exit: combined build is functionally green, visually reviewed, performance
measured and ready for the 0.10.0 content-lock/playtest phase.
## Postponed — Milestone 0.10.0 — Release candidate
## Milestone 0.10.0 — Release candidate
Goal: freeze the feature set, prove it with external players, clear release/legal blockers and
verify every distribution path. This is the next minor release after the 0.9.x patch line.
### Content lock and external playtest
- [ ] Declare a content/feature freeze for 0.10.0; only release-blocking fixes and balance changes
may land after the freeze.
- [ ] Produce a release-candidate build with the tutorial, campaign, all three venues, Help,
accessibility settings and checkpoint/resume enabled.
- [x] Declare a content/feature freeze for 0.10.0; only release-blocking fixes and balance changes
may land after the freeze. (Frozen 2026-11-03 after the 0.9.6 integration
checkpoint; this pass is limited to release reliability, accessibility,
balance evidence and packaging.)
- [x] Produce a release-candidate build with the tutorial, campaign, all three venues, Help,
accessibility settings and checkpoint/resume enabled. (Clean worktree
`a9658ec` built `0.10.0-rc.1`: Linux 161.5 MB, Windows 197.2 MB, macOS
148.0 MB, Web 97.8 MB and HQ 241.6 MB. The Web/HQ archives include the
credits, privacy, accessibility and known-limitations pages; SHA-256
manifests were written; the Linux and HQ binaries launched headless and the
Web index/wasm/pck/docs returned HTTP 200. Not published because external
playtest and legal approval remain open.)
- [ ] Run at least five first-time-player sessions covering the tutorial and one full venue shift;
record where players get stuck, misunderstand evidence or choose disproportionate escalation.
- [ ] Triage findings by severity (P0/P1/P2) and fix every P0/P1 onboarding/accessibility issue;
@@ -145,12 +79,17 @@ verify every distribution path. This is the next minor release after the 0.9.x p
### Balance and fairness pass
- [ ] Define measurable targets for routine compliance, access-decision accuracy, peaceful
- [x] Define measurable targets for routine compliance, access-decision accuracy, peaceful
resolution, justified emergency calls and false-positive/false-negative rates.
(`docs/balance-acceptance.md` defines cohorts, denominators, 36 matrix cells,
seeds, thresholds and matched emergency-first comparisons. Documentation verified;
targets are not measured results.)
- [ ] Run scripted venue sweeps across easy/normal/hard and all four phases; compare outcomes and
tune event weights, reaction tables, compliance and report strictness where targets fail.
- [ ] Review emergency calls against actual danger: no routine behavior should make emergency the
dominant winning move, and genuine threats must remain answerable.
dominant winning move, and genuine threats must remain answerable. Include the
type-derived `_read_npc_truth()` mismatch with observed threats/forced entry;
use independently labelled behavior evidence, not the helper as its own oracle.
- [ ] Verify the reputation grades, debrief explanations and campaign progression still communicate
the de-escalation-first design after tuning.
- [ ] Record the balance evidence and any deliberate exceptions in `docs/development-status.md`
@@ -158,39 +97,80 @@ verify every distribution path. This is the next minor release after the 0.9.x p
### Save migration and session reliability
- [ ] Add fixtures for a 0.8-era settings/campaign/history save and a 0.9.0 checkpoint; verify
migration/default-filling and a clean first launch with no files.
- [ ] Test fresh install → tutorial → campaign → pause → quit → resume → review → next venue on
an isolated user-data directory.
- [ ] Test corrupt JSON, partial temp files, missing optional fields and a failed atomic rename;
confirm fallback behavior never destroys a valid save.
- [ ] Verify checkpoint resume after all three venues, after an incident, with NPCs at the door and
with flashlight/body-camera/door state set.
- [ ] Document save locations, schema versions, reset behavior and rollback/export instructions in
`docs/getting-started.md` and the release notes.
- [x] Add fixtures for a 0.8-era settings/campaign/history save and a 0.9.0 checkpoint; verify
migration/default-filling and a clean first launch with no files. (Committed
fixtures under `src/test/fixtures/saves/`; 9 migration/failure tests cover
old settings/campaign/history, the 0.9.0 checkpoint, all three venue
checkpoint round-trips, clean install, corrupt JSON and a blocked atomic
replace. Suite is now 272 tests.)
- [x] Test fresh install → tutorial → campaign → pause → quit → resume → review → next venue on
an isolated user-data directory. (Tutorial and campaign smokes shared one
isolated `XDG_DATA_HOME`; tutorial completion/replay/skip then campaign
resume/review-continue/help both passed. Pause/menu behavior is covered by
`smoke_session_shell.gd`.)
- [x] Test corrupt JSON, partial temp files, missing optional fields and a failed atomic rename;
confirm fallback behavior never destroys a valid save. (Corrupt/partial files
fall back to defaults; a blocked temp path reports an error and leaves the
prior valid settings intact. `_load_json` now uses the non-logging JSON
instance parser.)
- [x] Verify checkpoint resume after all three venues, after an incident, with NPCs at the door and
with flashlight/body-camera/door state set. (The migration suite round-trips a
checkpoint for Hollywood, the Joke Shop and the TV Studio with an open
incident, one NPC row, flashlight/body-camera/door flags; the campaign smoke
resumes a live checkpoint through the real `GameManager`.)
- [x] Document save locations, schema versions, reset behavior and rollback/export instructions in
`docs/getting-started.md` and the release notes. (`getting-started.md` now
documents the four files, version-1 checkpoint, atomic write/fallback,
clean install, reset and backup/rollback.)
### Release engineering and distribution
- [ ] Run `tools/build_releases.sh` from a clean checkout and verify Linux/Windows/macOS/Web/HQ
artifact names, sizes and checksums.
- [x] Run `tools/build_releases.sh` from a clean checkout and verify Linux/Windows/macOS/Web/HQ
artifact names, sizes and checksums. (Clean worktree `a9658ec` built the
five artifacts listed above; `PUBLISH_DRY_RUN=1` produced and printed
`build/SHA256SUMS-0.10.0-rc.1.txt` without touching the API.)
- [ ] Smoke-test fresh Linux portable/AppImage, Windows portable/installer, macOS unzip/launch,
Web static hosting and Desktop HQ Vulkan fallback on the target matrix.
- [ ] Add a disk-space preflight and retention policy to the release/publish scripts so a full
Gitea host cannot leave a partially uploaded release.
- [ ] Verify release metadata, tag/version consistency, asset counts and download links; retain
(Linux portable and Desktop HQ launched headless; Web files served over a
local static server with HTTP 200. AppImage/installer were not built locally
and Windows/macOS launch testing still needs the target matrix.)
- [x] Add a disk-space preflight and retention policy to the release/publish scripts so a full
Gitea host cannot leave a partially uploaded release. (`publish_release.sh`
checks artifact size/free space, writes SHA-256 checksums, supports
`PUBLISH_DRY_RUN`, rolls back uploaded assets/new releases on failure, and
offers opt-in `RETAIN_RELEASES` pruning; dry-run verified.)
- [x] Verify release metadata, tag/version consistency, asset counts and download links; retain
at least the latest 0.8/0.9 releases and document older-asset pruning.
- [ ] Decide and document whether macOS signing/notarization is required for 0.10.0 or explicitly
(Source/version and tag are both 0.9.6; all seven published releases carry
5–7 assets, including the retained v0.8.0/v0.8.1/v0.9.0/v0.9.1 line.
`publish_release.sh` documents opt-in `RETAIN_RELEASES` pruning and
`RELEASING.md` describes the seven-release policy.)
- [x] Decide and document whether macOS signing/notarization is required for 0.10.0 or explicitly
remains a future release blocker; record the quarantine instructions either way.
(0.10.0 ships unsigned with quarantine instructions; signing/notarization
remains an explicit public-release blocker in `RELEASING.md` and
`known-limitations.md`.)
### Legal, credits and platform policy
- [ ] Audit AI-assisted asset provenance and licenses; record source/model usage and any
- [x] Audit AI-assisted asset provenance and licenses; record source/model usage and any
redistribution constraints for textures, characters, props and UI art.
- [ ] Audit audio/SFX/music licensing (or confirm everything is procedural) and add a credits file.
- [ ] Review body-camera/privacy language, mental-health-crisis representation, age/content rating
(`CREDITS.md` records the Blender/OpenRouter toolchain, default model slugs,
missing per-file generation metadata and the commercial-rights review as an
explicit release blocker.)
- [x] Audit audio/SFX/music licensing (or confirm everything is procedural) and add a credits file.
(All runtime audio is procedural; no external music/SFX files are bundled.
`CREDITS.md` records this.)
- [x] Review body-camera/privacy language, mental-health-crisis representation, age/content rating
and storefront requirements; fix any release-blocking copy or content.
- [ ] Add the player-facing credits, known limitations, accessibility statement and privacy note to
the release package/README.
(`PRIVACY.md` clarifies the simulated body camera/local-only data; the
accessibility and known-limitations pages record the remaining
mental-health/rating/storefront review as a release blocker rather than
claiming approval.)
- [x] Add the player-facing credits, known limitations, accessibility statement and privacy note to
the release package/README. (`README.md` links `CREDITS.md`, `PRIVACY.md`,
`docs/accessibility.md` and `docs/known-limitations.md`; the Web and Desktop
HQ package builders copy those documents into the shipped archives.)
- [ ] Mark external legal/storefront approval as complete or explicitly block the 0.10.0 release on it.
### Documentation and visual sign-off
@@ -201,7 +181,11 @@ verify every distribution path. This is the next minor release after the 0.9.x p
`check_screenshots.sh` and the pinned vision judge, recording model/spend/verdicts.
- [ ] Run the manual smoke checklist on every target and attach/link the release-candidate evidence.
- [ ] Verify the tutorial/campaign/help text at maximum font scale and with the longest strings.
- [ ] Remove or archive stale plans/notes and confirm no contradictory version/status claims remain.
- [x] Remove or archive stale plans/notes and confirm no contradictory version/status claims remain.
- [x] Document the character development toolchain in `docs/architecture.md`: base vs library
sources, tool/artifact/gate table, runtime attach/animation contracts and rebuild discipline.
(Added §5 "Character development tooling" plus risk and ownership-map updates;
documentation check passed.)
### Release-candidate QA and exit
@@ -216,6 +200,238 @@ verify every distribution path. This is the next minor release after the 0.9.x p
- [ ] Exit: the release candidate is content-locked, externally played, legally reviewed,
documented, reproducibly built, and approved as the 0.10.0 release candidate.
## Character development
Goal: break the eight-character clone effect with four licence-cleared crowd-variety looks while
keeping the frozen `npc_base` bone, tint-slot and gear-attach contract. The procedural generator is
the primary path; Quaternius is a reviewed reference and MakeHuman is deferred to a separate local
development pipeline. Start only after the current release candidate is published; this work must
not delay the release.
### Source review and decision
- [x] Research free/open-source character sources and evaluate at least three downloads against
the existing Blender contract. (Measured Quaternius CC0, Kenney CC0, Khronos
CesiumMan/RiggedFigure CC-BY and the MakeHuman CC0 base mesh; none is a drop-in.
Results, local preview sheet and SHA-256 provenance are in
`docs/npc-model-sourcing.md`.)
- [x] Review the sourcing research with the project owner and choose Option A (procedural),
Option B (Quaternius CC0 intake) or Option C (MakeHuman/MPFB body pipeline); record the
decision in an ADR and in `MEMORY.md`. (2026-11-03: extend the procedural generator as the
primary path, keep Quaternius as a reviewed prototype/reference, defer MakeHuman to a
separate local pipeline. Recorded in ADR-0015 and `MEMORY.md`.)
- [x] Add a source licence allow-list and provenance rule: CC0 preferred, CC-BY only with
attribution in `CREDITS.md`, and no service assets whose terms restrict redistribution.
(ADR-0015; the CC-BY research previews are attributed in `CREDITS.md`.)
- [x] Decide whether the external preview sheet stays local under `tmp/` or is committed with
the required CC-BY attribution. (Committed as
`docs/blender_previews/sheet_npc_sources_research.jpg` with Cesium CC-BY attribution;
the project-owned crowd prototype sheet is also committed.)
### Intake and validation
- [ ] Add a Blender `inspect` path that imports a GLB, applies node transforms, reports
tris/verts, bounds, bones, materials, UVs and skinning, and lists contract failures.
- [ ] Add an `adapt` path for future external assets: scale/ground, weld seams, decimate, map the
five tint slots, and either rebuild the contract armature or record an explicit second-rig
contract. The Quaternius prototype showed that a contract rebuild needs a proportion warp,
so this is not on the character-development critical path.
- [ ] Keep `tools/blender_assets.py audit` as the single final gate; add a regression test for the
intake report/adaptation using a deterministic synthetic or licence-cleared fixture.
- [ ] Document the adapter commands and the go/no-go checks in `docs/asset-tooling.md`.
### Quaternius prototype
- [x] Prototype one Quaternius CC0 variant: weld/decimate to budget and capture Idle/Walk
renders. (12,518 imported verts -> 3,199 after weld -> 2,423 / 4,654 tris after a 0.75
decimate; Idle/Walk quality is good. Preview in
`docs/blender_previews/sheet_npc_sources_research.jpg`.)
- [x] Measure the prototype against the `npc_casual` controls: silhouette, rest pose, body
proportions and gear fit. (No-go for a contract rebuild: T-pose 62-bone realistic rig,
~0.96 m hip, ~1.68 m arm span and ~0.153 m shoulder joint do not match the arm-down
13-bone contract. Keep as reference; a second rig/animation path needs its own ADR.)
- [x] Make a go/no-go decision on Option B. (No-go for the current pass; proceed with the procedural
generator and keep Quaternius as a visual/style reference.)
### Roster and runtime
- [x] Approve four crowd-variety looks (build, head silhouette, hair/beard, outer layer and
accessory; no clothing that labels the NPC's behaviour). (`npc_crowd_a` bun/slim/sunglasses,
`npc_crowd_b` bald/broad/beard/shoulder bag, `npc_crowd_c` coat/umbrella, `npc_crowd_d`
hood/cup; prototype sheet committed.)
- [ ] Add the four approved specs to `CHARACTERS`, build and commit their GLBs/AO maps, and
update the manifest with roles and tint data. (Prototype assets are local under `tmp/`;
do not add them to the current release candidate.)
- [ ] Replace the fixed `character_model_for()` mapping with deterministic per-role pools and add
tests for repeatability and duplicate avoidance.
- [ ] Rebuild and verify gear fit for every new variant (`blender_assets.py fit` plus the runtime
gear checks).
### QA and release
- [ ] Run the 24-NPC budget smoke, portrait baseline, gear fit, headless suite and a release build
with the expanded library.
- [ ] Update `CREDITS.md`, `docs/development-status.md`, `docs/npc-visual-review.md` and
`CHANGELOG.md`; record any accepted visual limits.
- [ ] Exit: a reviewed 12-character library passes contract audit, portrait/gear acceptance,
the 24-NPC budget and licence/provenance review without changing the runtime contract.
### Deferred MakeHuman local pipeline
- [x] Evaluate the MakeHuman CC0 base mesh for a later separate local development pipeline.
(19,158 verts / 36,972 tris; scaled to 1.82 m; decimate 0.35 -> 3,780 verts / 6,468 tris;
UVs present, no normals/material groups/rig. Feasible only with retopology, a custom rig and
project-owned clothing; keep out of CI/build.)
- [ ] Decide whether body-shape diversity still matters after the 12-character library lands; if
yes, prototype the local MPFB/MakeHuman pipeline outside the repo and commit only the final
CC0-derived assets with provenance.
## Local map development
Goal: build a local, data-backed map pipeline that can replace or supplement the stylized venue
geometry with licensed OpenStreetMap footprints, 3D building massing, reference-derived photoreal
facades and AI-assisted composition. This is an unversioned development track, not a release gate.
### Data, licensing and scope
- [ ] Define the first target block and whether the result replaces, supplements or only informs
the current stylized venue specs (`docs/hollywood-boulevard-map.md`,
`docs/joke-shop-map.md`).
- [ ] Audit OpenStreetMap's ODbL attribution/share-alike obligations and decide whether derived
geometry ships, stays development-only, or needs a separate data layer.
- [ ] Choose an offline OSM extract workflow (Overpass/Geofabrik) and pin the extract date and
query so the map build is reproducible.
- [ ] Evaluate licence-compatible 3D building datasets (OSM Buildings, Overture, CityGML/LOD1–2
or local authority data); record coverage, accuracy, format and redistribution terms.
- [ ] Add a provenance sidecar for every map source and derived asset, including source URL,
licence, capture/extract date and attribution text.
### Reference imagery
- [ ] Write a reference shot list per block: facade materials, windows, signage, rooflines,
street furniture, curb cuts and lighting.
- [ ] Download and catalogue reference images from Wikimedia Commons, local/own photography or
other clearly licensed sources; record author, licence and attribution. Do not use
unlicensed Street View or scraped commercial imagery.
- [ ] Separate development-only references from shipped textures; commit only images with clear
redistribution rights and keep the rest under `tmp/` or the ignored reference folders.
- [ ] Define the photoreal target and a material-derivation workflow (albedo/normal/roughness,
AO, weathering) that matches the existing Compatibility renderer.
### Geometry and AI-assisted composing
- [ ] Prototype OSM footprint import into Blender at game scale and orientation, then compare it
against the current venue footprint/collider layout.
- [ ] Import 3D building massing for the target block, clean the topology and assign LOD bands;
do not import unbounded city-scale meshes into the runtime.
- [ ] Build a reusable photoreal facade kit (window bays, cornices, storefronts, awnings, signage)
so buildings share materials and stay inside the venue triangle/material budgets.
- [ ] Add an AI-assisted composition step for palette, facade rhythm and reference boards; record
the model, prompt and output, and keep human review as the acceptance gate. AI output must
not be treated as licensed map data or shipped without provenance.
- [ ] Integrate the result behind `WorldBuilder.VENUE_SPECS` with colliders, navigation obstacles,
occlusion and the existing day/night materials.
### Gaussian splatting evaluation: photo intake and provenance
Gaussian splatting is evaluated as a photo-driven pipeline: licensed or permissioned photos
become a reference, texture source, mesh proxy or distant backdrop. It is an unversioned local
track with no runtime dependency or release gate until the ADR closes it, and it extends the
reference-image licence and privacy gates above rather than replacing them.
- [ ] Define the accepted photo channels: project/team captures, volunteer or user-supplied
photos with written permission, and downloaded images from clearly licensed archives
(Wikimedia Commons, Openverse, Mapillary and similar). Record which channel produced
each reconstruction.
- [ ] Write a one-page capture brief for user-supplied photos: overlapping arc or orbit around
the facade, 60–80% view overlap, 12 MP+ originals, fixed exposure/white balance, HDR and
filters off, no motion blur, no people or plates in frame where avoidable, overcast or
even light, and the facade height/doors/steps included in several frames.
- [ ] Build an intake checklist and provenance sidecar per photo set: contributor, consent or
property permission, licence and attribution text, source URL, capture date, camera/lens,
location, faces/plates/interiors present, redactions applied, file hashes and whether the
set is shipped or development-only.
- [ ] Set the licence gate: own photos need an explicit contributor licence; CC0/CC BY are
preferred; CC BY-SA triggers a share-alike review for derived textures and meshes; reject
Street View, social-media scrapes, unlicensed commercial imagery and AI-upscaled copies of
unknown originals. Attribution must be feasible in `CREDITS.md` before use.
- [ ] Set the privacy gate: blur or remove faces, minors, licence plates, screens, private
interiors and anything behind a closed door; obtain property permission where required;
keep a redacted copy as the reconstruction input and never commit the originals.
- [ ] Treat downloaded photo sets as sparse and of unknown intrinsics. Use them for reference,
palette and texture study, or as extra training views; do not expect a usable splat from
10–30 casual web photos of a facade.
- [ ] Keep photos, masks, COLMAP databases, checkpoints and splats outside git; commit only
derived, licensed, provenance-stamped assets that pass the review gate.
### Gaussian splatting: reconstruction and alignment
- [ ] Pin a reproducible offline toolchain (for example COLMAP or GLOMAP for camera poses plus
gsplat, OpenSplat, Nerfstudio or Brush for training), with exact versions, GPU/VRAM/disk
needs and known failure modes recorded in the sidecar.
- [ ] Reconstruct two quality tiers: a dense 100–400-photo arc/orbit capture and a sparse
10–30-photo downloaded set. Record camera-registration rate, training time, splat count,
file size and held-out PSNR/SSIM/LPIPS for both.
- [ ] Mask sky, people, vehicles and moving foliage before training; verify the masks do not
erase facade edges, signage or window detail, and keep the redacted input variant.
- [ ] Align the splat to game coordinates from the OSM footprint plus surveyed points (door
width, column spacing, step rise, storefront height) and record alignment residuals in a
Blender scene containing both the splat and the current venue geometry.
- [ ] Judge quality from held-out views and a render from the game’s day/night camera poses,
not from training views alone; reject splats that only look correct from the capture path.
- [ ] Maintain a coverage map per block: which facades, corners and rooflines have usable views,
and which areas must fall back to the facade kit or stylized geometry.
### Gaussian splatting: map enhancement paths
- [ ] **Path A — Reference and QA (lowest risk):** render the splat from game camera poses and
compare it with the realism captures; derive dimensions, signage placement, facade rhythm
and material colours. No splat data reaches the runtime.
- [ ] **Path B — Texture derivation:** project splat renders onto the facade kit to produce
albedo, normal, roughness and AO candidates; de-light and bake them, then verify UV scale,
tiling and readability under the Compatibility renderer.
- [ ] **Path C — Mesh proxy:** extract a mesh from the splat or dense reconstruction, retopo and
dress it in Blender, assign LODs, and keep it inside the venue triangle/material budgets
with mesh-based colliders and navigation.
- [ ] **Path D — Distant backdrop:** test a baked panorama, impostor or billboard generated from
the splat for skyline and opposite-street depth where the player cannot walk; measure file
size, loading and visible pop at the venue boundary.
- [ ] **Path E — User-photo supplement:** let a small supplied photo set improve one facade or
landmark (a signage patch, colour reference or hero detail) without rebuilding the venue;
define the minimum evidence and the same licence/privacy/review gate as a full capture.
- [ ] For every path record quality gain versus effort, licence compatibility of the derived
output, and whether the result may ship or stays development-only. Prefer the cheapest
path that closes the visible gap.
### Gaussian splatting: runtime evaluation and decision
- [ ] Probe Godot 4.7.2 Compatibility and Web support for splat rendering in a throwaway scene:
addon or importer availability, shader/sort cost, memory, load time and fallback behaviour.
Do not add a splat dependency to the release project during evaluation.
- [ ] Set hard limits before any runtime experiment: maximum per-splat file size and total
memory, allowed frame-time delta on the 24-NPC budget scene, acceptable Web load time, and
no compute-shader requirement for the Compatibility/Web path.
- [ ] Keep collision and navigation mesh-based even if a splat is displayed; a splat must never
become the playable floor or the access/occlusion authority.
- [ ] Write the ADR with a per-path go/no-go, the accepted licence/provenance rules, whether each
result ships or stays development-only, rejected alternatives and the evidence bundle.
- [ ] Exit: one licensed photo capture measurably improves a venue through a documented path and
passes visual, performance and provenance review; or the approach is rejected with
reproducible findings and removed from the release path.
### QA and exit
- [ ] Compare the data-backed block against the existing screenshots and map documentation; keep
the stylized fallback available.
- [ ] Run the venue triangle/mesh budget, navigation and collider checks, plus day/night capture
comparisons.
- [ ] Update `docs/architecture.md`, the venue map documents, `docs/asset-tooling.md`,
`docs/development-status.md` and `CHANGELOG.md` when the pipeline ships or a source is
rejected.
- [ ] Exit: one local block is reconstructed from licensed data with reference-derived photoreal
facades, passes performance/navigation/visual QA and has complete provenance; or the
pipeline is rejected with the findings recorded.
## Future 0.x / non-blocking candidates
- [ ] Live 3D body-camera replay.
@@ -232,6 +448,11 @@ verify every distribution path. This is the next minor release after the 0.9.x p
- Godot version changes can alter lighting/imports; do not combine an engine baseline move with a
content/feature milestone.
- Signed macOS builds and legal/AI provenance are release blockers, not polish.
- Photo-derived map assets add a second provenance risk: downloaded or user-supplied photos can
carry unclear licences, identifiable people, plates and property restrictions. No photo or
splat-derived asset ships without a completed sidecar, consent/licence review and redaction.
- Gaussian-splat training is GPU/disk-heavy and the toolchain churns quickly; keep it offline,
version-pinned and outside CI, and never make a runtime splat renderer a release dependency.
- Keep the de-escalation-first rules and rare-emergency design unchanged while adding progression,
difficulty and feedback.
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

+6
View File
@@ -13,8 +13,14 @@ This folder holds the project documentation, visual references and generated QA
- [`asset-tooling.md`](asset-tooling.md) — image/model pipelines and screenshot gates
- [`npc-visual-review.md`](npc-visual-review.md) — NPC visual baseline, acceptance criteria and ranked gaps
- [`npc-proportion-audit.md`](npc-proportion-audit.md) — NPC proportion ratios and stylized guide results
- [`npc-model-sourcing.md`](npc-model-sourcing.md) — free/open-source character sources, contract evaluation and the character-development plan
- [`development-status.md`](development-status.md) — implemented features and current gaps
- [`accessibility.md`](accessibility.md) — accessibility statement and known gaps
- [`known-limitations.md`](known-limitations.md) — release-candidate blockers and accepted limits
- [`smoke-checklist.md`](smoke-checklist.md) — manual verification
- [`balance-acceptance.md`](balance-acceptance.md) — release-candidate balance targets and evidence protocol
- [`../CREDITS.md`](../CREDITS.md) — credits and asset provenance
- [`../PRIVACY.md`](../PRIVACY.md) — privacy note
- [`ui-copy.md`](ui-copy.md) — screen, copy and button inventory
- [`RELEASING.md`](RELEASING.md) — export, CI and publishing workflow
- [`adr/README.md`](adr/README.md) — architecture decisions
+25
View File
@@ -103,6 +103,31 @@ The optional Desktop HQ zip is assembled by `tools/build_releases.sh` from
`packaging/desktop_hq/`; keep `override.cfg` next to the extracted executable.
It is not part of the default Compatibility/Web build.
`tools/publish_release.sh` preflights every artifact for existence and non-zero
size, checks free space in `build/`, writes `build/SHA256SUMS-<version>.txt`, and
uploads only after those checks pass. If an upload or the final asset-count check
fails, it removes the assets it just uploaded (and removes a newly created
release) so the host never keeps a partial release. Use
`PUBLISH_DRY_RUN=1 ./tools/publish_release.sh` to run the preflight and checksum
steps without touching the API. Set `RETAIN_RELEASES=7` to prune older releases
after a successful publish; the default `0` disables pruning and never deletes
history implicitly. A retention of seven keeps the newest seven releases,
including the latest 0.8/0.9 line.
### 0.10.0 macOS signing decision
The 0.10.0 release candidate will continue to ship an **unsigned** macOS zip with
the documented right-click/`xattr` quarantine instructions. Signing and
notarization remain a public-release blocker until an Apple Developer account and
a reproducible signing/notarization job are available; they are not silently
claimed as complete. The same limitation is recorded in
[`known-limitations.md`](known-limitations.md).
Always run the release build from a clean checkout or detached worktree. Godot
can pack files from ignored `tmp/` and old `build/` folders into the PCK; the
0.9.6 dirty-tree build produced 1.6–3.1 GB artifacts, while the clean worktree
produced the expected ~160–240 MB packages.
Locally you can build the AppImage with `bash packaging/linux/build_appimage.sh <version>`;
the NSIS installer requires `makensis` (covered by CI).
+32
View File
@@ -0,0 +1,32 @@
# Accessibility Statement
Gig Line is a keyboard/mouse-first first-person game with gamepad support. The
current build includes the following accessibility features:
- Remappable keyboard and gamepad bindings, persisted in `settings.json`.
- Keyboard focus rings and explicit default focus in every modal, with A/B
accept/cancel on a gamepad and wrapped Tab/arrow navigation.
- Adjustable HUD font scale up to 1.75× with clipped/ellipsized long labels.
- Reduced-motion mode that disables head bob and motion-sensitive presentation.
- Captions in the HUD log for radio, interaction and event messages.
- Master, SFX and ambience volume controls.
- Shape plus number escalation meter, so escalation is readable without colour.
- Hold-to-confirm emergency action and guarded destructive actions.
- Searchable Help generated from the live input bindings.
- Mouse-transparent HUD, right-click/Esc modal handling, and a predictable
top-panel close path.
Known gaps:
- There is no screen-reader integration or text-to-speech narration.
- The game is English-only; no pseudo-locale or full localization review has
been completed.
- The visual style is stylized and some small environmental detail may be hard
to read at low resolution; a full low-vision/high-contrast audit is pending.
- Audio captions cover gameplay messages, not every ambient sound.
- A formal WCAG conformance claim has **not** been made. External accessibility
review is still a 0.10.0 release-review item.
Test evidence and the manual checklist live in
[`smoke-checklist.md`](smoke-checklist.md); settings and save behaviour are
documented in [`getting-started.md`](getting-started.md).
@@ -0,0 +1,72 @@
# ADR-0015: NPC model variety sourcing
- **Status:** Accepted (planning)
- **Date:** 2026-11-03
## Context
The library has eight skinned characters, and role/seed selection reuses them
across the three venues. Crowds therefore repeat the same faces, builds and
outfits. The existing contract is strict: `npc_base` mesh node, 13 named bones at
fixed rest positions, five tint slots, front on Blender −Y and an 8,000
triangle/vertex budget.
A review compared free/open-source sources against that contract. Quaternius
Ultimate Modular (CC0), Kenney Blocky Characters (CC0), Khronos CesiumMan and
RiggedFigure (CC BY 4.0) and the MakeHuman base mesh (CC0) were imported and
measured. None was a drop-in; the full results are in
[`docs/npc-model-sourcing.md`](../npc-model-sourcing.md).
## Decision
1. **Ship only CC0, or CC BY with recorded attribution.** No service asset whose
terms restrict redistribution may be used.
2. **Extend the project-owned procedural generator as the primary path** for
shipped NPC variety. The first target is four crowd-variety looks, bringing
the library to twelve characters.
3. **Keep Quaternius as a reviewed reference/prototype, not a shipped base.** A
contract retarget is a no-go because the source is a T-pose 62-bone realistic
rig with substantially different proportions from the arm-down 13-bone
contract. A future external-rig path would need its own ADR covering its
skeleton, animation source and per-model gear offsets.
4. **MakeHuman is deferred to a separate local development pipeline.** Its CC0
base mesh can be decimated into budget, but it needs retopology, a custom rig
and project-owned clothing before it is usable. It must not become a CI or
build dependency.
5. **Commit research previews with attribution.** CC-BY preview images are
development-only and are not bundled with the game or its release packages.
## Consequences
### Positive
- The shipped library keeps one art direction, one skeleton and one audit gate.
- Crowd variety lands without a new runtime dependency or a second animation
system.
- The licence position is simple: project-owned assets plus explicit CC-BY
attribution where a preview or future asset requires it.
- The Quaternius evaluation removes a tempting but high-risk retarget from the
critical path.
### Negative
- Procedural variation is still limited by the existing face and clothing
vocabulary; four variants do not provide full age/body diversity.
- Quaternius' animation set remains unused unless a second rig path is approved.
- MakeHuman body-shape diversity is postponed.
- External previews add a small documentation footprint that must stay out of
release packages.
## Alternatives considered
- **Retarget Quaternius onto the `npc_base` contract.** Rejected for now:
welding brings the geometry inside budget, but the rest pose and body
proportions differ enough that a contract rebuild would either distort the
model or require a full proportion warp.
- **Use Quaternius' own rig and animations directly.** Deferred: attractive
animation quality, but it adds a second skeleton/animation path and per-model
gear offsets to `NpcAvatar`.
- **Use MakeHuman as the primary body source.** Rejected for this milestone: high
polygon count, no rig in the base mesh and a separate authoring dependency.
- **Use Mixamo or Ready Player Me.** Rejected: service terms are not a simple
free/open-source redistribution basis for shipped meshes.
+1
View File
@@ -18,3 +18,4 @@ Each ADR records the context, decision, consequences and alternatives for a dura
- [0012-surface-materials-and-generated-props](0012-surface-materials-and-generated-props.md)
- [0013-blender-authored-npc-characters](0013-blender-authored-npc-characters.md)
- [0014-runtime-image-root](0014-runtime-image-root.md)
- [0015-npc-model-variety-sourcing](0015-npc-model-variety-sourcing.md)
+46 -1
View File
@@ -37,7 +37,7 @@ This document follows the structure of the arc42 architecture documentation temp
- **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible.
- **PATCH** — bug fixes and small polish that do not change behavior.
The single source of truth for the version is `config/version` in `project.godot` (currently `0.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.10.0-rc.1`). 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
@@ -127,6 +127,48 @@ Contracts that keep the pipeline replaceable:
- UI/scene art is loaded through `UiArt` with a null/neutral fallback; composed images live in `assets/images/`, not in the docs reference tree (ADR-0014).
- CI stays offline: only committed `.glb`/`.png`/`.tres` and Godot are needed; optional AI/Blender steps are reproducible development tools.
### Character development tooling
Characters have two authored sources and one procedural fallback. The eight-variant
Blender library (`assets/models/blender/npc/`) is the normal presentation path; the
older skinned `npc_base` (`assets/models/generated/npc_base.glb`) remains a frozen
contract and fallback; `NpcAvatar`'s procedural rig is the offline last resort.
Blender is a developer tool only — runtime, tests and CI consume committed GLBs and
maps.
| Tool | Role | Committed artifact | Verification gate |
| --- | --- | --- | --- |
| `tools/npc_pipeline.py` | Builds the skinned base avatar, five-slot material maps, palette and AI-fabric preparation (`prepare`/`palette`) | `npc_base.glb`, `assets/textures/npc/*_{albedo,normal,orm}.png`, `palette.json` | `audit_glb` against the frozen 6,000-triangle / 4,000-vertex base contract |
| `tools/blender_assets.py` | Builds/renders the library: 8 characters, 18 gear pieces, 20 props, plus `manifest.json`; stdlib `audit`/`fit`/`sheet`/`list` | `assets/models/blender/{npc,npc_gear,props}/*.glb` | Library cap 8,000 tris / 8,000 verts per character, 1,500 per gear, 4,000 per prop; grounding, UV, degenerate-triangle, attach-fit and manifest coverage checks |
| `tools/bake_ao.py` | Bakes per-asset AO maps | `<asset>_ao.png` (256²) | Wired through `Materials.apply_ao()`/`SurfaceMaterials`; cloth/pants opt out (see below) |
| `tools/npc_proportions.py` | Measures exported GLB rest pose (height, head, shoulders, hips, torso, limbs, feet) against the stylized guide | report / JSON in `docs/npc-proportion-audit.md` | All eight variants must stay inside the guide after a body change |
| `tools/npc_baseline.py` | Assembles neutral-baseline sheets and before/after comparisons | `docs/blender_previews/sheet_npc_baseline*.jpg` | Human review against `docs/npc-visual-review.md` |
| `src/test/capture_npc_baseline.gd` | Fixed-camera studio baseline, 8 variants × 6 views | local `tmp/` frames | Neutral silhouette/face/gear review |
| `src/test/capture_npc_review.gd` | Final-context gate: 8 variants × 2 venues × day/night × 3 distances = 96 frames, crowd hidden, review-only `character_override` for the guard | local `tmp/` frames | Pre-build acceptance gate for gear fit and venue lighting |
| `src/test/capture_realism.gd` | Venue realism stages plus a frozen portrait and surface close-ups | `docs/screens/realism/<run>/` | Optional vision judge; portrait is the only avatar-detail capture |
Runtime contracts that must survive any character change:
- `NpcAvatar.character_model_for(role, seed)` selects the library variant; `character_override`
exists only to stage roles without a spawned model (for example the guard).
- The armature node names, `Rig/Head`, the Hip/Knee/Shoulder/Elbow/Hand proxies and the
**+Z visual front** are frozen; animation and `yaw_towards()` depend on them.
- Only the five contract slots are tinted. Fixed detail materials (eyes, reflective bands,
rubber) stay untouched, gear is placed by `GEAR_ATTACH`, and `GEAR_OFFSETS` plus
`HAND_ITEM_FORWARD` are runtime attach-space corrections, not authoring changes.
- Cloth/pants use `Materials.NPC_FABRIC_UV_SCALE` (4×) and skip the baked per-character AO;
skin/hair/gear keep AO. Do not reintroduce the blotchy cloth AO.
- `apply_lod()` swaps to an impostor at 26 m; the 24-NPC budget smoke (6.94 ms, 168 meshes,
131,616 triangles) is the performance gate. Changing either triangle cap requires
re-measuring that smoke and the Web/Compatibility path.
- Rebuild only the assets whose geometry changed (Blender GLB export is byte-nondeterministic),
then run `godot --headless --path . --import`; never commit a no-op full rebuild.
Evidence and detailed contracts live in [`blender-assets.md`](blender-assets.md),
[`npc-visual-review.md`](npc-visual-review.md) and
[`npc-proportion-audit.md`](npc-proportion-audit.md); command-level usage is in
[`asset-tooling.md`](asset-tooling.md).
### Content authoring and runtime data flow
`ScenarioFactory` is the source of truth for the three authored venues. `tools/export_venues.gd`
@@ -272,6 +314,7 @@ Architecture decisions are recorded as ADRs:
- Accessibility and localization coverage is partial; a public release needs subtitles/captions, colour-independent cues, remapping, text expansion tests and at least a documented localization workflow.
- Unsigned macOS builds, AI asset provenance and any storefront/legal requirements are release blockers for a public release.
- Documentation drift has already occurred (release version, implemented status and obsolete plan docs); the ownership map and roadmap must be kept current in the same change as the implementation.
- Character quality is only partly automatable: proportions, budgets and gear fit are measured, but face realism and venue-lighting readability still need the baseline/review sheets and human sign-off after any mesh, map or material change.
## 12. Documentation Consolidation and Ownership
@@ -287,6 +330,8 @@ architecture document.
| [`docs/adr/`](adr/) | Immutable decision records with context and consequences | Decision history linked from §9 |
| [`docs/asset-tooling.md`](asset-tooling.md) | Build/audit/capture commands and pipeline usage | Operational appendix to the pipeline section |
| [`docs/blender-assets.md`](blender-assets.md) | Blender character/gear/prop contracts and attach points | Detailed asset contract appendix |
| [`docs/npc-visual-review.md`](npc-visual-review.md) | NPC baseline views, acceptance criteria and ranked visual gaps | Visual-QA appendix to the character tooling section |
| [`docs/npc-proportion-audit.md`](npc-proportion-audit.md) | Measured proportions against the stylized guide | Measurement appendix to the character tooling section |
| [`docs/getting-started.md`](getting-started.md) | Setup, commands, controls | Developer onboarding |
| [`docs/RELEASING.md`](RELEASING.md) | Release checklist and CI/publishing steps | Deployment appendix to §7 |
| [`docs/smoke-checklist.md`](smoke-checklist.md) | Manual verification script | QA appendix to §10 |
+55 -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/`,
which is gitignored) onto the `cloth`/`pants` albedos; `build` bakes them into the
`.glb` and regenerates the procedural normal + packed ORM maps. `palette` writes
which is gitignored) onto the 256² `cloth`/`pants` albedos; `build` bakes them into
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`.
## Blender asset library (loaded by the game)
@@ -126,6 +129,11 @@ with colliders/avoidance and drives their emissive parts from the day/night
cycle. The single `npc_base.glb` and primitive props remain offline fallbacks.
Contract and integration notes: [`blender-assets.md`](blender-assets.md).
Sourcing and expansion research (no external model is shipped yet):
[`npc-model-sourcing.md`](npc-model-sourcing.md) compares Quaternius, Kenney, Khronos and
MakeHuman sources against the character contract and records the 2026-11-03 direction
(procedural generator first, Quaternius reference, MakeHuman deferred).
| Characters | Props |
| --- | --- |
| ![characters](../docs/blender_previews/sheet_npc.jpg) | ![props](../docs/blender_previews/sheet_props.jpg) |
@@ -232,17 +240,52 @@ godot --path . --script res://src/test/capture_npc_baseline.gd -- \
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);
`--stages=day,late` limits it while tuning; `--flashlight` turns on the guard lamp for
night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one NPC on open sidewalk
and frames the face, which is the only capture that can judge avatar detail
(`--portrait-type=N` picks a role). The generated realism runs under
`capture_realism.gd` runs nine stages in this order: day, Hollywood surface close-ups,
portrait, evening, late, Joke Shop day, Joke Shop surface close-ups, Joke Shop night,
studio; `--stages=day,late` limits it while tuning; `--flashlight` turns on the guard
lamp for night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one
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
resulting decisions/docs, not the run folders. The texture-adoption run compared
the pre-adoption captures with the current build: on the shots both sets captured,
the judge's like-for-like means rose on every sub-score (overall 0.8 → 1.0,
backdrop 0.6 → 1.6) and the Pillow metric gap fell 0.444 → 0.348. The character
pass added the portrait shot and recorded the new faces, hands and sleeve cuffs.
resulting decisions/docs, not the run folders. The 0.9.5 texture/UI audit checked actual screen sizes: all screen art is
1280×720, access cards are 256×128 and HUD icons are 64×64; no runtime texture
was oversized or unused. The 24-NPC smoke now reports 168 meshes / 131,616
triangles / 6.94 ms. Compressed payloads are 24.55 MB of surface picks, 22.67 MB
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.
For the 0.9.6 integration checkpoint the same judge was pinned to
`z-ai/glm-5.3-flash` and run once per shot on the 10-shot set: mean overall 2.1,
lighting 2.4, materials 2.1, texture 1.7, backdrop 2.3, characters 1.8, Pillow
gap 0.291. No `review` verdicts were produced by the screen judge, so there was
no manual layout verdict to convert into a fix; the remaining realism comments
are the accepted stylized gaps.
Build release artifacts from a clean checkout or detached worktree. A normal
working tree may contain ignored `tmp/` captures and old `build/` artifacts, and
Godot can pack enough of those into exports to inflate a 0.9.6 package from
~160 MB to 1.6–3.1 GB. The 0.9.6 clean worktree produced 161.5 MB Linux,
197.1 MB Windows, 148.0 MB macOS, 97.8 MB Web and 241.5 MB 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
head/skirt bounds with `python3 tools/blender_assets.py fit`; it also fixed the
+115
View File
@@ -0,0 +1,115 @@
# Release-candidate balance acceptance
These are the acceptance targets for the 0.10.0 balance pass, not measured results.
No reaction tables, event weights or scoring rules were changed to set them. The
[roadmap](../TODO.md) tracks the sweeps and external playtests still needed.
[Gameplay](gameplay.md) owns the mechanics; this page owns their balance-test protocol.
## Test matrix and evidence
Run Hollywood, Joke Shop and TV Studio on easy, normal and hard, in Early,
Midday, Evening and Late: **36 cells**. Use seeds 1–100 for each cell and retain
those seeds for before/after comparisons. Exercise each eligible authored event
at least once as a separate coverage pass; a random schedule can miss rare events.
Record scheduled event counts separately from forced coverage cases so the latter
do not distort event-frequency estimates.
For reaction measurements, use a fresh controller at the entrance for each sample.
Routine cases are tourists, confused visitors and delivery workers obstructing the
entrance without threatening behavior. Do not include authorized entry requests in
this redirect cohort. For peaceful-resolution measurements, send one polite
direction (L1), then one warning (L2) only if still unresolved. Do not reroll until
success. Observe return behavior for 60 simulated seconds before closing the case.
Do not issue emergency calls as part of this policy.
For access cases, follow the venue's required inspection and screening steps before
allow/deny. Include valid, expired, counterfeit, mismatched, missing and ambiguous
artifacts, known recognition, age boundaries, screening refusal, benign wand alerts
and prohibited items. Recognition must be independently justified by the fixture;
calling a stranger recognized is not evidence of correct admission. An unresolved
hold counts as unresolved, not as a correct refusal.
Evidence must identify the commit, engine, venue, difficulty, phase, seed, event ID,
cohort, visible evidence, policy action, reaction/state, return, access truth and
decision, emergency truth and call, report fields, reputation modifiers, final grade
and unresolved cases. Save raw rows plus numerator/denominator summaries for every
cell. Use at least **100 eligible cases per metric per cell**; if the schedule
provides fewer, add a separately labelled forced-case sample. Report zero eligible
cases as **N/A**, never as 100% success. Keep scheduled and forced results separate.
## Targets
Percentages below apply to each matrix cell, not only a pooled average. Scripted
access/report cases use complete evidence and are held to exact correctness;
first-time-player results measure whether that evidence is understandable.
| Metric | Numerator / denominator | Acceptance target |
| --- | --- | --- |
| First-direction routine compliance | First L1 reaction is `COMPLY` or `LEAVE` / eligible routine redirect cases | Easy ≥80%; normal ≥75%; hard ≥65% |
| Routine peaceful resolution | Routine cases leaving after at most L1 then L2, with no return during 60 s and no L3–L5 / eligible routine redirect cases | Easy ≥90%; normal ≥85%; hard ≥80% |
| Access accuracy | Correct final allow/deny decisions / all eligible access cases (including unresolved cases) | Scripted 100%; first-time players ≥90% pooled, with each error reviewed |
| Access false positive | Unauthorized people allowed / unauthorized access cases | Scripted 0%; first-time players ≤5% pooled |
| Access false negative | Authorized people denied / authorized access cases | Scripted 0%; first-time players ≤10% pooled |
| Emergency precision | Calls supported by immediate-danger evidence / all emergency calls | Scripted 100%; first-time players ≥95% pooled; zero calls is N/A |
| Emergency false positive | Routine/no-immediate-danger cases receiving an emergency call / all routine/no-immediate-danger cases | Scripted 0%; first-time players ≤5% pooled |
| Emergency false negative | Immediate-danger cases without an accepted emergency call within 15 simulated seconds of visible danger / all immediate-danger cases with radio initially available | Scripted 0%; first-time players ≤10% pooled |
| Report accuracy | Reports with correct weapon and danger fields / all submitted reports | Scripted 100%; first-time players ≥90% pooled |
For access, authorized means entitled under the venue policy **after required
screening**, without a prohibited item or an independent immediate-danger reason
to refuse. Compare to a hand-labelled fixture expectation, not only to
`AccessVerifier.verify()`: using the verifier as both decision and oracle would
hide policy bugs. Report unresolved access cases separately; they cannot improve
either error rate by being omitted from accuracy.
Immediate danger means an observed weapon, assault, active forced entry or a
credible immediate threat to occupants. Argument, filming, intoxication, a crisis,
a refused screen or a benign wand alert alone is not immediate danger. A mental
health label or clothing must never be used as the emergency oracle. The 15 s
window is a test-response target, not the responder-arrival time (currently 15 s
after an accepted emergency call). Separately test depleted battery and active
cooldown cases for safe containment/assistance; do not silently drop them from the
evidence or claim the initially-available-radio target covers them.
## Scoring and emergency dominance
Compare the calm policy with an emergency-first policy on the same routine events
and seeds. Each comparison starts with fresh radio, crowd and reputation state.
An accurate report about harmless behavior must not make its emergency call
justified. Require an unjustified-call investigation and a strictly lower rating
for emergency-first in every routine comparison. Inspect modifiers as well as the
final rating: the 0–100 clamp can hide rewards/penalties. Emergency-first must not
improve the campaign outcome or final grade. Grade boundaries remain A ≥90,
B ≥75, C ≥60, D ≥40, otherwise F; these are existing rules, not new tuning.
In genuine-danger cases, verify that the call is accepted, responders arrive,
the incident remains answerable and the debrief explains the outcome without an
unjustified-call investigation. Also compare assistance/containment routes where
appropriate; emergency need not be mandatory for every difficult interaction.
## Implementation cautions and sign-off
- `NPCController.roll_reaction()` samples per-level weights. Difficulty adds
+12/0/−8 to the `COMPLY` weight before normalization; `base_compliance` alone
is not the observed compliance percentage. Measure `COMPLY` plus `LEAVE`.
- `GameManager._read_npc_truth()` currently derives weapon/danger truth from the
NPC type (violent threat: weapon/3; everyone else: no weapon/1), rather than
observed behavior. This is a balance-review risk: test nonviolent profiles that
threaten or force entry, and violent profiles before visible danger. Do not
treat agreement with this helper as proof of fair emergency judgment.
- `RadioSystem.credibility_hits()` checks weapon and danger fields only;
location, description and behavior still need human review. Difficulty scales
report penalties, not the oracle.
- A terminal peaceful reaction is not sufficient evidence of an incident staying
resolved: returning NPCs and unresolved shift-review entries must be retained.
Do not tune to a pooled average that hides a failing venue, phase or difficulty.
Record each miss, its severity, the raw evidence and any approved exception before
changing weights. Re-run the affected cells and tutorial/campaign smokes after a
fix. Five first-time-player sessions must cover the tutorial and a complete venue
shift; report pooled counts and per-session errors, not statistical confidence
from five players. Scripted sweeps do not substitute for those sessions.
**Status:** targets defined; sweeps, emergency fairness review, grade/campaign
verification and external-player acceptance remain pending. No balance sign-off
or release approval is implied by this document.
+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
animation proxies in `_build_skinned_body()` stay valid.
- material slots `skin`, `cloth`, `pants`, `accessory`, `hair` carry the same
neutral `assets/textures/npc/<slot>_{albedo,normal}.png` maps, so
`Materials.npc_slot()` tints them the same way it tints `npc_base`.
neutral `assets/textures/npc/<slot>_{albedo,normal}.png` maps at 256², so
`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
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
+5
View File
@@ -43,6 +43,8 @@ Committed contact sheets and local Blender renders for the character, gear and p
| <img src="security_podium.png" width="280" alt="security_podium.png"> | [`security_podium.png`](security_podium.png) |
| <img src="sedan.png" width="280" alt="sedan.png"> | [`sedan.png`](sedan.png) |
| <img src="sheet_npc.jpg" width="280" alt="sheet_npc.jpg"> | [`sheet_npc.jpg`](sheet_npc.jpg) |
| <img src="sheet_npc_sources_research.jpg" width="280" alt="sheet_npc_sources_research.jpg"> | [`sheet_npc_sources_research.jpg`](sheet_npc_sources_research.jpg) |
| <img src="sheet_npc_crowd_prototypes.jpg" width="280" alt="sheet_npc_crowd_prototypes.jpg"> | [`sheet_npc_crowd_prototypes.jpg`](sheet_npc_crowd_prototypes.jpg) |
| <img src="sheet_npc_baseline.jpg" width="280" alt="sheet_npc_baseline.jpg"> | [`sheet_npc_baseline.jpg`](sheet_npc_baseline.jpg) |
| <img src="sheet_npc_baseline_compare-p1.jpg" width="280" alt="sheet_npc_baseline_compare-p1.jpg"> | [`sheet_npc_baseline_compare-p1.jpg`](sheet_npc_baseline_compare-p1.jpg) |
| <img src="sheet_npc_baseline_compare-body.jpg" width="280" alt="sheet_npc_baseline_compare-body.jpg"> | [`sheet_npc_baseline_compare-body.jpg`](sheet_npc_baseline_compare-body.jpg) |
@@ -52,6 +54,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_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-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_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) |
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: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

+12 -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)
- 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
- 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: **272 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`)
@@ -42,6 +42,16 @@ 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 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`.
- Release v0.9.6 (2026-11-03): the tagged commit `4bb2896` passed Gitea Actions run 1296 (checks + 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.6>.
- Release candidate (0.10.0-rc.1, 2026-11-03): clean worktree `a9658ec` built Linux 161.5 MB, Windows 197.2 MB, macOS 148.0 MB, Web 97.8 MB and Desktop HQ 241.6 MB. The Web/HQ archives contain `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md` and `docs/known-limitations.md`; SHA-256 checksums were generated, the Linux and HQ binaries launched headless, and the Web index/wasm/pck/docs served with HTTP 200. The candidate is not published: external playtest, AI-provider/legal approval, macOS signing and the Windows/macOS target matrix remain open blockers.
- Save migration and reliability (0.10.0 work): committed 0.8-era settings/campaign/history fixtures and a 0.9.0 checkpoint. The loader default-fills old settings, keeps campaign progress and validates checkpoints for all three venues with an open incident, NPC rows, door, flashlight and body-camera state. Corrupt and partial files fall back to defaults; a blocked atomic replace leaves the prior valid file intact. `_load_json` uses the non-logging JSON parser; save locations and rollback are documented in `getting-started.md`. Suite is 272/272.
- Integration checkpoint (0.9.6 work): the clean worktree at `f5b8ef2` rebuilt all five release packages and the Desktop HQ zip without ignored `tmp/`/`build/` content (Linux 161.5 MB, Windows 197.1 MB, macOS 148.0 MB, Web 97.8 MB, HQ 241.5 MB). The Web zip contains `index.html`, `index.wasm` and `index.pck`; the HQ zip contains `override.cfg` and launches headless with the Forward+ override. Current verification: Godot 263/263, Python 101/101, 14 smokes green, 24-NPC budget 6.90 ms / 168 meshes / 131,616 triangles, gallery 21 good / 0 bad, screen judge 21/21 match with 0 review/diff, and a pinned realism judge (`z-ai/glm-5.3-flash`, one pass per shot) at mean overall 2.1 and Pillow gap 0.291. No new gameplay or content changes.
- Release v0.9.5 (2026-11-02): the tagged commit `4bc6390` passed Gitea Actions run 922 (checks + 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.5>.
- 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 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.
@@ -71,6 +81,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`.
- 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`.
- 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).
+26
View File
@@ -74,6 +74,32 @@ XDG_DATA_HOME=/tmp/gigline-campaign godot --path . --fixed-fps 60 --script res:/
The unit test runner is the project's own lightweight harness (no editor plugin required) so CI can run it without the editor.
## Save Data and Migration
The game keeps four small JSON files in Godot's `user://` directory, which is
`~/.local/share/godot/app_userdata/Gig Line/` on Linux,
`%APPDATA%\Godot\app_userdata\Gig Line\` on Windows and
`~/Library/Application Support/Godot/app_userdata/Gig Line/` on macOS:
| File | Contents |
| --- | --- |
| `settings.json` | Camera, HUD, binding, difficulty, audio, accessibility and tutorial flags. Missing keys are filled from the current defaults; there is no schema version because the field set grew compatibly. |
| `campaign.json` | Campaign index, completed venues, difficulty, shift number and last grade. Older files with missing fields open at the safe defaults. |
| `shift_results.json` | Completed-shift review history, appended as an array. |
| `shift_state.json` | Resumable mid-shift checkpoint, version `1`. It stores the venue, guard/door/flashlight/body-camera state, coordinator snapshot and NPC rows. |
Writes go to a sibling `.tmp` file and are renamed only after the complete JSON
is written. A corrupt or partial file is ignored on load, so the player gets
defaults rather than a crash and a valid old file is not overwritten by a failed
replace. The migration fixtures used by the tests live in
`src/test/fixtures/saves/`.
For a clean first launch, delete the four files (or the whole `Gig Line` user
directory). Settings' **Reset campaign** clears campaign progress separately.
To back up or move a profile, copy those files while the game is closed; to roll
back, restore the previous copies. Shift history is diagnostic and is not
required to resume a checkpoint.
## Controls
Keyboard and mouse are the default; a gamepad works without configuration (left stick moves,
+48
View File
@@ -0,0 +1,48 @@
# Known Limitations
This page records the limitations that remain at the 0.10.0 release-candidate
stage. Items marked **blocker** must be cleared or explicitly accepted before a
public release.
## Release blockers
- **External playtest:** at least five first-time-player sessions and the
resulting onboarding/accessibility triage have not been completed. The
tutorial/campaign evidence is currently automated smoke coverage, not player
observation.
- **AI asset provenance:** the image-generation provider terms and commercial
redistribution rights for the committed AI-assisted textures/UI art still need
legal review. See [`../CREDITS.md`](../CREDITS.md).
- **Storefront/legal approval:** age/content rating, storefront policy, trademark
language and the mental-health-crisis/body-camera representation have not been
externally approved.
- **macOS signing/notarization:** the 0.10.0 release candidate ships unsigned
with right-click/`xattr` quarantine instructions. Signing and notarization are
required before a broad public/storefront release and remain blocked until an
Apple Developer account and signing job exist.
- **Hardware matrix:** Web, Windows and Desktop HQ have been built and checked
structurally; a complete target-hardware performance/launch matrix is still
pending.
## Content and presentation
- The venues are stylized reconstructions, not survey-accurate models. The Joke
Shop is a fictional stand-in for the real Sunset Strip block.
- Character faces, hands and fabric use a deliberately low-poly style; the
>26 m tier remains a two-quad impostor and there is no separate finger-curl
rig. The accepted limits are recorded in
[`npc-visual-review.md`](npc-visual-review.md).
- There is no live 3D body-camera replay; the timeline is text-based.
- There is no recorded voice/music track. Ambience and cues are procedural.
- The game is English-only and does not yet have a translator workflow or
pseudo-locale layout sign-off.
## Persistence and platform
- Save migration is covered by synthetic 0.8/0.9 fixtures and failure-mode tests,
not by a real player profile collected from every earlier release.
- Desktop HQ requires a Vulkan-capable GPU/driver; the default Compatibility and
Web builds remain the broad target.
- The manual smoke checklist is completed by developers; external device
coverage (Windows installer, macOS launch, gamepad and Web hosts) is not yet a
complete evidence set.
+309
View File
@@ -0,0 +1,309 @@
# NPC model sourcing and expansion plan
Status: **direction chosen 2026-11-03.** The procedural generator is the primary
path, the Quaternius CC0 pack is a reviewed reference/prototype, and MakeHuman is
deferred to a separate local development pipeline. No downloaded source model is
committed to the game or included in a release; the committed research preview
sheets carry the required attribution. Local evaluation files live under
`tmp/npc-research/` (gitignored). The decision is recorded in
[ADR-0015](adr/0015-npc-model-variety-sourcing.md).
This page records the free/open-source character sources that were reviewed, how
they behave when imported into the existing Blender tooling, and the options for
creating a larger NPC model library. The runtime contract and current library are
documented in [`blender-assets.md`](blender-assets.md) and
[`npc-visual-review.md`](npc-visual-review.md).
## Why this review exists
The library has eight skinned characters, and `NpcAvatar.character_model_for()`
reuses them across role families. That is enough for a release candidate, but
crowds repeat the same faces, builds and outfits. Three venues, a 24-NPC budget
and close-up portraits make that repetition visible.
Any expansion must preserve the existing integration contract:
- mesh node `npc_base` under a `Skeleton3D`;
- bones `Head`, `HipL/R`, `KneeL/R`, `ShoulderL/R`, `ElbowL/R`, `HandL/R` plus
`Hips`, `Spine`, `Neck` at the contract rest positions;
- five tint slots (`skin`, `cloth`, `pants`, `accessory`, `hair`);
- front on Blender −Y / Godot +Z, feet near the floor, 1.6–2.1 m height;
- at most 8,000 triangles and 8,000 glTF vertices per character;
- gear attach points at head, back, chest and hand.
The existing `audit` command is the gate. A source that looks good in a preview
but fails `audit` is not usable without an adaptation step.
## What was evaluated
Blender 5.2.0 LTS (Flatpak) imported each file in background mode. Measurements
below come from the imported scene, because external GLBs often carry node
transforms that the project's audit (which reads accessor-local bounds) does not
apply. "Vertices" are the imported, seam-split counts that matter for the glTF
budget, not the source file's vertex count.
| Source | Licence | Files checked | Geometry / rig | Height | Verdict |
| --- | --- | --- | --- | --- | --- |
| **Quaternius Ultimate Modular Men / Women** (mirrored GLB pack) | CC0 1.0 | `Casual_Hoodie.glb`, `Worker.glb`, `Casual.glb` | 5,240–6,424 tris; 10,534–12,968 verts; 62 bones; 24 animations; 4 skinned meshes on one armature | 1.80–1.87 m | **Best silhouette/outfit match.** Fails the contract: wrong bone names/rest positions, no tint slots, no `npc_base` node, vertex budget exceeded. |
| **Kenney Blocky Characters 2.0** | CC0 1.0 | full pack (18 characters), `character-a.glb`, `character-f.glb` | 72 tris; 143 verts; static, no armature; one texture atlas per character | 2.7 m imported (needs 0.67× scale) | **Not a close-up character.** Useful as a crowd/impostor LOD or placeholder, but cannot be driven by the skeleton and has no separate face/clothing slots. |
| **Khronos CesiumMan** | CC BY 4.0 (© Cesium) | `CesiumMan.glb` | 4,672 tris; 3,273 verts; 19-bone walk rig; 1 material/texture; 1 animation | 1.45 m | **Technical retarget test only.** One outfit and one look, wrong rig, short; works as a skinning/pose reference. Attribution is required. |
| **Khronos RiggedFigure** | CC BY 4.0 (© Cesium) | `RiggedFigure.glb` | 256 tris; 370 verts; 19-bone walk rig; no UVs; 1 animation | 1.48 m | **Rig smoke test only.** A mannequin with no usable surface detail. |
| **MakeHuman core base mesh / MPFB** | Core assets CC0 1.0; application AGPL/GPL | `makehuman/data/3dobjs/base.obj` | 19,158 verts; 18,486 polygons; 36,972 tris; no rig in the base mesh | ~16.9 model units before scaling | **Best body-shape diversity, highest pipeline cost.** Needs scaling, decimation/retopology and a generated rig. The MakeHuman/MPFB tool itself is not bundled. |
Local review sheet (front views, not committed):
`tmp/npc-research/previews/contact_sheet.jpg`.
Rejected without downloading: Mixamo and Ready Player Me. Both are useful
services, but their terms are not a simple free/open-source redistribution basis
for shipped character meshes. Sketchfab, Poly Pizza and OpenGameArt can contain
excellent CC0 work, but each asset needs a case-by-case licence and provenance
check; they are not a consistent library source.
### Measured contract failures
Every example fails the unmodified `audit` contract:
| Check | Quaternius | Kenney | CesiumMan | RiggedFigure | MakeHuman base |
| --- | --- | --- | --- | --- | --- |
| `npc_base` mesh node | no | no | no | no | no |
| Required 13 bones | no (62 different names) | no rig | no (19 different names) | no (19 different names) | no rig |
| Five tint slots | no | no | no | no | no |
| ≤ 8,000 tris | yes | yes | yes | yes | no |
| ≤ 8,000 verts | no (10.5–13k) | yes | yes | yes | no (19k) |
| Feet on floor | yes | yes (2.7 m scale) | yes (−11 mm) | yes | no (needs scaling) |
| Height 1.6–2.1 m | yes | no (2.7 m) | no (1.45 m) | no (1.48 m) | needs scale |
| Skinned / UV mapped | yes / yes | no / yes | yes / yes | yes / no | no / no |
The practical conclusion is that **there is no drop-in download**. External
sources need a deliberate intake step, and the expensive part is not decimation:
it is rebuilding or retargeting the skeleton and material slots without breaking
the animation proxies and gear placement.
The table uses Blender-imported world bounds. The stdlib `audit` reads accessor-local
bounds and ignores node transforms, so an unapplied external GLB can fail floor/height
checks even when the imported scene looks correct. The intake step must apply
transforms before it measures and exports.
## Options
### Option A — extend the procedural generator
Add more variation to `tools/blender_assets.py` rather than importing models:
body builds, height ranges, head/face shapes, hair and facial hair, outfit
layers, and more `CHARACTERS` specs. This keeps a single owned style, needs no
third-party licence review, and stays inside the existing builder/audit flow.
- **Strengths:** lowest integration risk, deterministic, no new dependencies.
- **Limits:** the current face and clothing vocabulary is intentionally simple;
more variants may still read as the same model with a different coat.
- **Best for:** a quick 4–8 variant expansion and filling obvious role gaps.
### Option B — Quaternius CC0 intake with a contract adapter
Use Quaternius meshes as silhouette/outfit donors and adapt them to the project
contract. The visible result is a different body/clothing language from the
current library, so the adapter must also normalise head/hand style or rebuild
those parts.
- **Strengths:** best free outfit/hair variety, animations included, CC0,
already close to game proportions, front axis and floor contact.
- **Work:** weld/decimate to the vertex budget, remap materials to the five
slots, rebuild the armature at the contract rest positions, transfer weights,
repose/normalise proportions, rename the mesh node, export, audit and compare
in the acceptance harness.
- **Main risk:** retargeting a 62-bone T-pose rig to the 13-bone arm-down
contract. Hip/knee rest heights differ substantially (Quaternius upper leg
starts near 0.96 m; the contract hip starts near 0.62 m), so a naive
nearest-bone weight transfer can produce a good still image and bad walk
poses. Prototype one character before committing to a roster.
- **Best for:** a larger, visibly different library if the prototype passes the
portrait and walk gates.
### Option C — MakeHuman/MPFB body-shape pipeline
Generate a range of ages, builds and body shapes from the CC0 MakeHuman base
mesh, then replace the head/hands and dress them with project-owned clothing.
This is the strongest answer to "everyone has the same body", but requires an
offline authoring step, decimation/retopology and a custom rig/weight step. It
should be treated as a later pipeline project, not a quick content drop.
## Decision and prototype outcome
The project owner reviewed this page on 2026-11-03. The decision is:
1. **CC0 is acceptable** for shipped character assets. CC-BY is acceptable only
with attribution in `CREDITS.md`; service assets whose terms restrict
redistribution remain excluded.
2. **Extend the procedural generator** as the primary path, starting with
crowd-variety looks.
3. **Prototype Quaternius** as a secondary source. It is not approved for
shipping yet; the contract-retarget result below is a no-go for the current
skeleton.
4. **Use crowd variety** as the first visual goal, not role-specific costumes.
5. **Evaluate MakeHuman for a later, separate local development pipeline.** It
is not part of the normal build, CI or release flow.
6. **Commit the research previews with attribution.** The CC-BY entries are
listed in `CREDITS.md` and remain development-only.
### Quaternius prototype
The prototype used `Casual_Hoodie.glb`:
- **Geometry budget is not the blocker.** Imported: 12,518 vertices / 6,206
triangles. After welding seam splits: 3,199 vertices / 6,206 triangles. After
a 0.75 decimate: 2,423 vertices / 4,654 triangles, comfortably inside the
library budget.
- **Animation quality is attractive.** The imported Idle and Walk clips read
well at game distance, and the model brings 24 animations.
- **The contract rest pose is the blocker.** The source is a T-pose 62-bone rig
with realistic proportions: upper leg starts near 0.96 m, arm span is about
1.68 m, and the shoulder joint sits near x = 0.153 m. The project contract is
an arm-down 13-bone rig with a hip near 0.62 m, a shoulder near x = 0.27 m and
a hand near z = 0.78 m. Rebuilding the contract armature around this mesh would
either distort the body substantially or require a full proportion warp.
- **Decision:** do not retarget Quaternius to the contract now. Keep it as a
silhouette/style reference and, if a second rig path is ever wanted, prototype
it as its own animation path (its own skeleton, AnimationPlayer clips and
per-model gear offsets) rather than forcing it into `npc_base`. That is a
separate architecture decision, not an asset conversion.
Preview: [`blender_previews/sheet_npc_sources_research.jpg`](blender_previews/sheet_npc_sources_research.jpg).
### Generator crowd prototype
Four proposed crowd specs were built with the existing generator in a temporary
output directory. All four audit clean and stay inside the character budget:
| Prototype | Look | Tris / verts | Gear |
| --- | --- | --- | --- |
| `npc_crowd_a` | slim, bun, sunglasses | 4,644 / 4,356 | sunglasses |
| `npc_crowd_b` | broad, bald, beard | 4,550 / 4,453 | shoulder bag |
| `npc_crowd_c` | average, short hair, long coat | 5,118 / 5,086 | umbrella |
| `npc_crowd_d` | slim, short hair, hood up | 5,258 / 4,907 | cup |
They vary build, head silhouette, hair/beard and outer layer while keeping the
existing face language. They are **not in the shipped catalogue**; production
integration belongs to the roster/runtime work under the unversioned `Character
development` section, after the current release candidate.
Preview: [`blender_previews/sheet_npc_crowd_prototypes.jpg`](blender_previews/sheet_npc_crowd_prototypes.jpg).
### MakeHuman local-pipeline evaluation
The CC0 base mesh is 19,158 vertices / 18,486 polygons (36,972 triangles after
triangulation), with 21,334 UV coordinates but no normals, material groups or
rig. Scaling it to 1.82 m and decimating to 0.35 produced 3,780 vertices / 6,468
triangles, which fits the geometry budget but is a collapse-decimation result,
not production topology.
MakeHuman is therefore **feasible only as a separate local body-shape pipeline**.
The boundary for that future work is:
- MPFB/MakeHuman is installed and run by a contributor, never by CI or the game
build.
- Generated bodies and intermediate exports stay in `tmp/` (gitignored); the
CC0 base mesh is not committed.
- The contributor scales to 1.8–1.9 m, decimates or retopologises to roughly
6–7k triangles, and checks the silhouette against `npc-visual-review.md`.
- The body gets the contract armature, weights, project-owned head/hands,
clothing and gear; it must pass `blender_assets.py audit` and `fit`.
- Only the final CC0-derived GLB and its provenance note are committed. The
application code and any non-CC0 assets stay outside the repository.
This is deferred; it is not part of the current crowd-variety work.
## Provisional NPC roster
The design rule stays: clothes, props and behaviour carry the read; a model must
not label a person as "the threat". The roster therefore adds ordinary adults
across age, build and style, not costumes that telegraph the outcome.
| Look family | Current | Proposed additions | Variation axes |
| --- | --- | --- | --- |
| Everyday | `npc_casual`, `npc_jacket`, `npc_coat`, `npc_hoodie`, `npc_dress` | `npc_crowd_a` (bun/slim), `npc_crowd_b` (bald/broad), `npc_crowd_c` (coat/umbrella), `npc_crowd_d` (hood/cup) | build, height, hair, facial hair, outer layer, accessory |
| Workwear | `npc_vest`, `npc_guard` | later: second hi-vis build, venue-security build | build, height, headwear, badge/radio gear |
| Responder | `npc_firefighter` | later: paramedic/medical responder only if the design supports it | turnout vs medical kit, build, helmet |
| Crowd/LOD | none | optional Kenney blocky or a decimated library variant for distant crowds | silhouette and palette only |
A first target is **12 library characters** (the current eight plus the four
crowd prototypes), not twenty. That is enough to break the clone effect without
doubling the portrait QA matrix. `character_model_for()` becomes a pool per role
family with a deterministic seed choice; the current two-way alternation for
guests, trespassers and threats is the template.
## Implementation plan
The actionable tasks live in `TODO.md` under the `Character development` section. The workstreams
are:
1. **Decision and licence gate.** Complete: ADR-0015, the CC0/CC-BY rule and the
committed preview sheets.
2. **Generator crowd variants.** Complete as a prototype (four specs, clean
audit, review sheet). Production integration is pending the current release candidate.
3. **Quaternius intake.** The contract-retarget prototype is a no-go. Keep the
source as a reference; a second-rig/animation path would need its own ADR.
4. **Roster build and runtime.** Add the four approved specs to `CHARACTERS`,
build and commit their GLBs, update the manifest, and extend the role pools.
5. **QA and integration.** Run the 24-NPC budget, portrait baseline, gear fit,
headless suite and release build. Record provenance and any accepted limits.
Only then update `CHANGELOG.md` and `docs/development-status.md`.
### Adapter notes
- Apply node transforms before measuring or exporting. CesiumMan imports with a
Z-up root and an `Icosphere` helper object; exclude helpers, not just the mesh
names, when measuring.
- Weld duplicate seam vertices before decimating. Quaternius' 12k imported
vertices are mostly UV/normal splits; the underlying surface is much smaller,
but the glTF budget counts the splits after export.
- Decimate to a target below 8,000 tris/verts, then re-unwrap only if the
existing UVs cannot survive the material remap. The current library uses
smart projection at 80° to keep islands large.
- Map materials by semantic role, not by source name. A source "Purple" or
"Worker_Vest" needs an explicit mapping to `cloth`, `pants`, `skin`, `hair` or
`accessory`; do not rely on colour heuristics.
- Keep the face direction unambiguous. The front is Blender −Y; verify it with
a capture, not by guessing from the source file.
- For any future external-rig path, do not silently add a second skeleton format
to `NpcAvatar`. Record the per-model skeleton, animation source and gear
offsets as an explicit contract.
## Review outcome
The project owner answered the review questions on 2026-11-03:
| Question | Answer |
| --- | --- |
| Are third-party CC0 characters acceptable? | Yes. |
| Which direction? | Prototype Quaternius and extend the procedural generator. |
| Which looks first? | Crowd variety. |
| Should MakeHuman be pursued? | Evaluate it for a later, separate local development pipeline. |
| Commit the previews? | Yes, with CC-BY attribution. |
## Downloaded example provenance
Source models were downloaded on 2026-11-03 and kept out of version control. The two
research preview sheets are committed under `docs/blender_previews/`; the CC-BY
attribution is recorded in `CREDITS.md`.
| File | Source | SHA-256 |
| --- | --- | --- |
| `quaternius/Casual_Hoodie.glb` | `raw.githubusercontent.com/LuanDucate/Ducz.CharacterCreator/HEAD/public/models/Male/Casual_Hoodie.glb` (Quaternius CC0) | `f5b722199f7628b24550989af4230f65d5230727df9c326fd2b3e360effe5249` |
| `quaternius/Worker.glb` | same mirror, `Male/Worker.glb` | `8d0f454336c2effc5c08f63a177af3a82d1a7af025ca9588b40e4c90c4703534` |
| `quaternius/Casual.glb` | same mirror, `Female/Casual.glb` | `bc2d84fba249ebaa0629cb9d133ad37234196e4b8004aa205a6397abed04e05c` |
| `kenney_blocky_characters.zip` | `kenney.nl/assets/blocky-characters` | `5e123859aa0c1598342b600c6db197024a1d63eb9ec531398b310725f589887e` |
| `khronos/CesiumMan.glb` | `github.com/KhronosGroup/glTF-Sample-Assets` | `b7001eaeea8254bd44773bcd247e78696d94169388fbb2a1800fc69434e777d9` |
| `khronos/RiggedFigure.glb` | same repository | `d6be85417d3e256861ee733eea6916093a7af7c79c16366181fd8abcaeb38cf5` |
| `makehuman/base.obj` | `github.com/makehumancommunity/makehuman/blob/master/makehuman/data/3dobjs/base.obj` | `8e761e6624b8f54536409135d1636da63b32486a90d4897f84e121d144f6fb4c` |
Source terms:
- Quaternius packs are CC0 1.0 according to the pack page and the mirror's
credits. The mirror is a convenience download; the original pack page is
<https://quaternius.com/packs/ultimatemodularcharacters.html>.
- Kenney Blocky Characters 2.0 is CC0 1.0; the pack's `License.txt` is included
in the download.
- CesiumMan and RiggedFigure are CC BY 4.0, © 2017 Cesium. If any derivative is
committed or shipped, the attribution must be added to `CREDITS.md`.
- MakeHuman core assets are CC0 per `LICENSE.ASSETS.md`; the application source
is AGPL and is not bundled.
+9 -4
View File
@@ -93,7 +93,7 @@ pass every applicable item before a library rebuild is accepted.
authored grip points.
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
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.
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.
- 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`.
- 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;
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 forward-grip pass is
[`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
rather than replacing it.
The 0.9.5 texture pass adds
[`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
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
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: 542 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 KiB

After

Width:  |  Height:  |  Size: 385 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: 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/description="Workplace security simulation about observation, access verification, and proportionate escalation."
config/version="0.9.4"
config/version="0.10.0-rc.1"
run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.7", "GL Compatibility")
+7 -2
View File
@@ -14,8 +14,13 @@ func _load_json(file_name: String) -> Variant:
var text := FileAccess.get_file_as_string(path)
if text.is_empty():
return null
var parsed = JSON.parse_string(text)
return parsed
# JSON.parse_string logs an engine error on malformed player files. Use the
# instance parser so a corrupt save silently falls back to defaults instead
# of looking like a crash in the console.
var json := JSON.new()
if json.parse(text) != OK:
return null
return json.data
func _save_json(file_name: String, data: Variant) -> Error:
var path := "user://" + file_name
+6
View File
@@ -13,6 +13,10 @@ extends RefCounted
## asphalt replaced the flat first-generation tiles. Normal maps carry the mortar,
## paver joints and aggregate that Compatibility cannot get from screen space.
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_CONCRETE := preload(PICK_DIR + "sidewalk_pavers.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:
material = StandardMaterial3D.new()
material.albedo_color = tint
if material.resource_name in ["cloth", "pants"]:
material.uv1_scale = NPC_FABRIC_UV_SCALE
return material
## 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):
continue
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_texture = ao
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.
const SKYLINE_HEIGHT := 90.0
const SKYLINE_BY_PHASE := {
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)},
Rules.Phase.EVENING: {"texture": T_SKYLINE_DUSK, "tint": Color(0.9, 0.92, 1.0)},
Rules.Phase.LATE: {"texture": T_SKYLINE_NIGHT, "tint": Color(1.5, 1.55, 1.65)},
Rules.Phase.EARLY: {"texture": T_SKYLINE_DAY, "tint": Color(0.98, 1.0, 1.04),
"haze": Color(0.62, 0.71, 0.82), "haze_strength": 0.36},
Rules.Phase.MIDDAY: {"texture": T_SKYLINE_DAY, "tint": Color(1.02, 1.04, 1.08),
"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.
## 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.
_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")
# 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),
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.
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),
@@ -1379,8 +1385,10 @@ func _build_walk_of_fame() -> void:
var walk := Node3D.new()
walk.name = "WalkOfFame"
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),
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 brass := Materials.flat(Color(0.70, 0.55, 0.24), "metal")
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.set_shader_parameter("sky_texture", SKYLINE_BY_PHASE[_phase]["texture"])
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)
_skyline_mat = mat
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 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;
void fragment() {
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_SCISSOR_THRESHOLD = 0.5;
}
@@ -2241,6 +2258,8 @@ func set_phase(phase: int) -> void:
if _skyline_mat != null:
_skyline_mat.set_shader_parameter("sky_texture", entry["texture"])
_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
set_night_mode(night)
if _street_mat != null:
+58 -13
View File
@@ -62,6 +62,18 @@ const SHOTS_JOKE_DAY := [
const SHOTS_STUDIO := [
{"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()`.
const SHOTS_PORTRAIT := [
{"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.
const STAGES := [
{"name": "day", "elapsed": 0.0, "venue": "", "shots": SHOTS_DAY, "night": false},
# Portrait next, while the day crowd is still on the street.
{"name": "portrait", "elapsed": 0.0, "venue": "", "shots": SHOTS_PORTRAIT,
{"name": "day", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_DAY, "night": false},
{"name": "surfaces", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_SURFACES, "night": false},
# 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},
{"name": "evening", "elapsed": 620.0, "venue": "", "shots": SHOTS_EVENING, "night": true},
{"name": "late", "elapsed": 760.0, "venue": "", "shots": SHOTS_LATE, "night": true},
{"name": "evening", "elapsed": 620.0, "venue": "hollywood_building", "shots": SHOTS_EVENING, "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": "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": "studio", "elapsed": 700.0, "venue": "tv_studio", "shots": SHOTS_STUDIO, "night": true},
]
@@ -166,6 +181,7 @@ func _process(delta: float) -> bool:
return false
func _begin_stage() -> void:
_clear_capture_pause()
_stage += 1
while _stage < STAGES.size() and not _only_stages.is_empty() \
and not _only_stages.has(str(STAGES[_stage]["name"])):
@@ -182,6 +198,7 @@ func _begin_stage() -> void:
gm.hud.clear_log()
gm.hud.set_target_label("")
gm.hud.set_action_hint("")
gm.hud.visible = not str(stage["name"]).begins_with("surfaces")
if _flashlight:
var want := bool(stage["night"])
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()])
func _run_queue(delta: float) -> void:
_clear_capture_pause()
if _queue.is_empty():
_phase = "stage"
return
@@ -226,9 +244,18 @@ func _run_queue(delta: float) -> void:
_queue.pop_front()
_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
## is lit. The subject's own yaw is re-written every frame by its FSM, so the
## portrait turns the visual `Rig` child instead and stands on the sunlit side.
## is lit. The subject's FSM and idle head-turn are frozen so the still keeps the
## 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:
var guard_pos: Vector3 = gm.guard.global_position
var light := _sun_direction()
@@ -239,7 +266,17 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
var npc := node as Node3D
if npc == null or _faces_covered(npc):
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
var distance: float = npc.global_position.distance_to(guard_pos)
if distance > 25.0:
@@ -265,9 +302,10 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
continue
other.set_physics_process(false)
other.global_position += Vector3(0.0, 0.0, 140.0)
# Freeze the subject: its FSM re-writes the yaw every physics tick, which
# would turn it away from the camera again.
# Freeze the subject: its FSM re-writes the yaw every physics tick, and the
# idle head-turn would still look away after the body is aimed at the camera.
best.set_physics_process(false)
best.set_process(false)
# 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.
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")
best.global_position = spot + _portrait_spot
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.
var to_light := -light
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.
# 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
# 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:
rig.rotation.y = atan2(-look.x, -look.z)
else:
best.rotation.y = atan2(-look.x, -look.z)
rig.rotation.y = 0.0
return true
+12
View File
@@ -0,0 +1,12 @@
# Test Fixtures
Committed JSON fixtures used by persistence and migration tests.
- [`saves/settings_0_8.json`](saves/settings_0_8.json) — a 0.8-era settings file missing the later volume, motion and caption keys; loading it must keep the old values and fill the new defaults.
- [`saves/campaign_0_8.json`](saves/campaign_0_8.json) — a 0.8-era campaign after the Hollywood shift; loading it must resolve the Joke Shop as the next venue.
- [`saves/shift_results_0_8.json`](saves/shift_results_0_8.json) — a 0.8-era shift history with two completed venues.
- [`saves/shift_state_0_9_0.json`](saves/shift_state_0_9_0.json) — a version-1 0.9.0 checkpoint at the Joke Shop with door, flashlight and body-camera state set.
These are deliberately schema-less where the old game was schema-less; the
loader fills missing fields from the current defaults rather than rewriting the
player's file on load.
+11
View File
@@ -0,0 +1,11 @@
{
"index": 1,
"next_venue": "joke_shop",
"shift_number": 2,
"difficulty": "hard",
"complete": false,
"completed": [
"hollywood_building"
],
"last_grade": "B"
}
+15
View File
@@ -0,0 +1,15 @@
{
"look_sensitivity": 0.0031,
"hud_font_scale": 1.15,
"invert_y": true,
"head_bob": false,
"input_bindings": {
"inspect": {
"type": "key",
"code": 90
}
},
"ai_difficulty": "hard",
"tutorial_completed": true,
"tutorial_skipped": false
}
+20
View File
@@ -0,0 +1,20 @@
[
{
"scenario_id": "hollywood_building",
"reputation": {
"rating": 81.0,
"grade": "B"
},
"spawned_count": 6,
"completed_at_ms": 1000
},
{
"scenario_id": "joke_shop",
"reputation": {
"rating": 74.5,
"grade": "C"
},
"spawned_count": 5,
"completed_at_ms": 2000
}
]
+26
View File
@@ -0,0 +1,26 @@
{
"version": 1,
"scenario_id": "joke_shop",
"guard_position": [0.0, 0.1, 10.5],
"guard_yaw": 1.25,
"head_pitch": 0.1,
"door_locked": true,
"flashlight_on": true,
"body_camera_on": true,
"npc_counter": 7,
"coordinator": {
"duration_s": 900.0,
"elapsed_s": 540.0,
"current_phase": 3,
"phase_offset_s": 700.0,
"spawned_count": 7,
"responder_outcomes": [],
"investigations": [],
"recorder": {},
"reputation": {},
"crowd": {},
"radio": {},
"scheduler": {}
},
"npcs": []
}
+12 -3
View File
@@ -42,10 +42,19 @@ func _process(delta: float) -> bool:
if frame >= WARMUP_FRAMES + SAMPLE_FRAMES:
var average := sample_ms / SAMPLE_FRAMES
var meshes := 0
var triangles := 0
for avatar in avatars:
meshes += avatar.find_children("*", "MeshInstance3D", true, false).size()
print("NPC budget: %d avatars, %d meshes, avg frame %.2f ms (budget %.0f ms)" % [
COUNT, meshes, average, BUDGET_MS])
for node in avatar.find_children("*", "MeshInstance3D", true, false):
var mi := node as MeshInstance3D
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:
printerr("SMOKE FAIL: frame time %.1f ms exceeds the %.0f ms budget" % [
average, BUDGET_MS])
+28
View File
@@ -3,6 +3,7 @@ extends RefCounted
## styles, queue slots, LOD tiers/labels, and the articulated rig structure.
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 Rules := preload("res://src/core/rules.gd")
@@ -333,6 +334,33 @@ func test_skinned_outfit_tints_the_material_slots() -> bool:
avatar.free()
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:
var profile := NpcProfile.new()
profile.id = "fallback_test"
+248
View File
@@ -0,0 +1,248 @@
extends RefCounted
## Persistence migration and failure-mode tests. The fixtures represent a
## 0.8-era player profile and a 0.9.0 checkpoint; loading them must fill current
## defaults without rewriting or losing the old fields.
const Campaign := preload("res://src/core/campaign.gd")
const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
const ShiftCoordinator := preload("res://src/core/shift_coordinator.gd")
const FixtureDir := "res://src/test/fixtures/saves/"
const UserFiles := ["settings.json", "campaign.json", "shift_results.json", "shift_state.json"]
func _save_instance() -> Object:
var save_script: GDScript = load("res://src/autoload/save.gd")
return save_script.new()
func _fixture(name: String) -> String:
return FileAccess.get_file_as_string(FixtureDir + name)
func _user_path(name: String) -> String:
return ProjectSettings.globalize_path("user://" + name)
func _write_user(name: String, text: String) -> void:
var file := FileAccess.open("user://" + name, FileAccess.WRITE)
if file == null:
return
file.store_string(text)
file.close()
func _read_user(name: String) -> String:
var path := "user://" + name
if not FileAccess.file_exists(path):
return ""
return FileAccess.get_file_as_string(path)
func _remove_user(name: String) -> void:
var path := _user_path(name)
if FileAccess.file_exists("user://" + name) or DirAccess.dir_exists_absolute(path):
DirAccess.remove_absolute(path)
func _snapshot_files() -> Dictionary:
var snapshot := {}
for name in UserFiles:
snapshot[name] = _read_user(name)
return snapshot
func _restore_files(snapshot: Dictionary) -> void:
for name in UserFiles:
_remove_user(name)
_remove_user(name + ".tmp")
var text := str(snapshot.get(name, ""))
if not text.is_empty():
_write_user(name, text)
func test_settings_0_8_fixture_fills_new_defaults() -> bool:
var backup := _snapshot_files()
_write_user("settings.json", _fixture("settings_0_8.json"))
var save := _save_instance()
var loaded: Dictionary = save.load_settings()
var a := TestUtils.check(is_equal_approx(float(loaded.get("look_sensitivity", 0.0)), 0.0031),
"old sensitivity survives")
var b := TestUtils.check(is_equal_approx(float(loaded.get("hud_font_scale", 0.0)), 1.15),
"old font scale survives")
var c := TestUtils.check_eq(bool(loaded.get("invert_y", false)), true, "old invert setting survives")
var d := TestUtils.check_eq(bool(loaded.get("head_bob", true)), false, "old head-bob setting survives")
var e := TestUtils.check_eq(str(loaded.get("ai_difficulty", "")), "hard", "old difficulty survives")
var f := TestUtils.check_eq(bool(loaded.get("tutorial_completed", false)), true,
"old tutorial completion survives")
var g := TestUtils.check(is_equal_approx(float(loaded.get("master_volume", 0.0)), 1.0),
"new master-volume default is filled")
var h := TestUtils.check_eq(bool(loaded.get("captions_enabled", false)), true,
"new caption default is filled")
var i := TestUtils.check_eq(bool(loaded.get("reduce_motion", true)), false,
"new reduced-motion default is filled")
save.free()
_restore_files(backup)
return a and b and c and d and e and f and g and h and i
func test_campaign_0_8_fixture_keeps_progress() -> bool:
var backup := _snapshot_files()
_write_user("campaign.json", _fixture("campaign_0_8.json"))
var save := _save_instance()
var campaign := Campaign.new()
campaign.setup(save.load_campaign())
var a := TestUtils.check_eq(campaign.index, 1, "campaign index survives")
var b := TestUtils.check_eq(campaign.next_venue(), "joke_shop", "next venue survives")
var c := TestUtils.check_eq(campaign.shift_number, 2, "shift number survives")
var d := TestUtils.check_eq(campaign.difficulty, "hard", "difficulty survives")
var e := TestUtils.check_eq(campaign.completed, ["hollywood_building"], "history survives")
var f := TestUtils.check_eq(campaign.last_grade, "B", "last grade survives")
save.free()
_restore_files(backup)
return a and b and c and d and e and f
func test_history_0_8_fixture_loads() -> bool:
var backup := _snapshot_files()
_write_user("shift_results.json", _fixture("shift_results_0_8.json"))
var save := _save_instance()
var results: Array = save.load_shift_results()
var a := TestUtils.check_eq(results.size(), 2, "both old results load")
var b := TestUtils.check_eq(str(results[0].get("scenario_id", "")), "hollywood_building",
"first result keeps its venue")
var c := TestUtils.check_eq(str(results[1].get("scenario_id", "")), "joke_shop",
"second result keeps its venue")
save.free()
_restore_files(backup)
return a and b and c
func test_checkpoint_0_9_0_fixture_restores() -> bool:
var backup := _snapshot_files()
_write_user("shift_state.json", _fixture("shift_state_0_9_0.json"))
var save := _save_instance()
var state: Dictionary = save.load_shift_state()
var a := TestUtils.check_eq(int(state.get("version", 0)), 1, "checkpoint version survives")
var b := TestUtils.check_eq(str(state.get("scenario_id", "")), "joke_shop",
"checkpoint venue survives")
var c := TestUtils.check_eq(bool(state.get("door_locked", false)), true, "door state survives")
var d := TestUtils.check_eq(bool(state.get("flashlight_on", false)), true,
"flashlight state survives")
var e := TestUtils.check_eq(bool(state.get("body_camera_on", false)), true,
"body-camera state survives")
var coordinator := ShiftCoordinator.new()
coordinator.setup(ScenarioFactory.joke_shop(), 900.0, 7)
coordinator.restore(state.get("coordinator", {}))
var f := TestUtils.check(is_equal_approx(coordinator.elapsed_s, 540.0), "elapsed time restores")
var g := TestUtils.check_eq(coordinator.current_phase, 3, "late phase restores")
var h := TestUtils.check(is_equal_approx(coordinator.phase_offset_s, 700.0),
"phase offset restores")
save.free()
_restore_files(backup)
return a and b and c and d and e and f and g and h
func test_checkpoint_round_trip_all_venues() -> bool:
var backup := _snapshot_files()
var venues := [
["hollywood_building", ScenarioFactory.hollywood_building()],
["joke_shop", ScenarioFactory.joke_shop()],
["tv_studio", ScenarioFactory.tv_studio()],
]
var save := _save_instance()
var ok := true
for index in venues.size():
var venue_id: String = venues[index][0]
var scenario: VenueScenario = venues[index][1]
var coordinator := ShiftCoordinator.new()
coordinator.setup(scenario, 900.0, index + 3)
coordinator.elapsed_s = 300.0 + index * 60.0
coordinator.recorder.start_incident("guest", "focus")
var state := {
"version": 1,
"scenario_id": venue_id,
"guard_position": [1.0, 0.1, 8.0 + index],
"guard_yaw": 0.5,
"head_pitch": 0.1,
"door_locked": index % 2 == 0,
"flashlight_on": true,
"body_camera_on": true,
"npc_counter": index + 1,
"coordinator": coordinator.snapshot(),
"npcs": [{"npc_id": "guest_%d" % index, "npc_type": 0}],
}
ok = TestUtils.check_eq(int(save.save_shift_state(state)), OK,
"%s checkpoint writes" % venue_id) and ok
var loaded: Dictionary = save.load_shift_state()
ok = TestUtils.check_eq(str(loaded.get("scenario_id", "")), venue_id,
"%s checkpoint keeps its venue" % venue_id) and ok
ok = TestUtils.check_eq(bool(loaded.get("flashlight_on", false)), true,
"%s checkpoint keeps flashlight state" % venue_id) and ok
ok = TestUtils.check_eq(bool(loaded.get("body_camera_on", false)), true,
"%s checkpoint keeps body-camera state" % venue_id) and ok
ok = TestUtils.check_eq(bool(loaded.get("door_locked", false)), index % 2 == 0,
"%s checkpoint keeps door state" % venue_id) and ok
ok = TestUtils.check_eq((loaded.get("npcs", []) as Array).size(), 1,
"%s checkpoint keeps NPC rows" % venue_id) and ok
var restored := ShiftCoordinator.new()
restored.setup(scenario, 900.0, index + 3)
restored.restore(loaded.get("coordinator", {}))
ok = TestUtils.check(is_equal_approx(restored.elapsed_s, 300.0 + index * 60.0),
"%s checkpoint restores elapsed time" % venue_id) and ok
ok = TestUtils.check(restored.recorder.has_open_incident("guest"),
"%s checkpoint restores an open incident" % venue_id) and ok
save.free()
_restore_files(backup)
return ok
func test_clean_install_has_safe_defaults() -> bool:
var backup := _snapshot_files()
for name in UserFiles:
_remove_user(name)
var save := _save_instance()
var settings: Dictionary = save.load_settings()
var a := TestUtils.check(is_equal_approx(float(settings.get("look_sensitivity", 0.0)), 0.0022),
"clean install gets default sensitivity")
var b := TestUtils.check_eq(str(settings.get("ai_difficulty", "")), "normal",
"clean install gets normal difficulty")
var c := TestUtils.check(save.load_campaign().is_empty(), "clean install has no campaign")
var d := TestUtils.check_eq(save.load_shift_results().size(), 0, "clean install has no history")
var e := TestUtils.check(save.load_shift_state().is_empty(), "clean install has no checkpoint")
save.free()
_restore_files(backup)
return a and b and c and d and e
func test_corrupt_json_falls_back_without_crashing() -> bool:
var backup := _snapshot_files()
_write_user("settings.json", "{not valid json")
_write_user("campaign.json", "{not valid json")
_write_user("shift_results.json", "{not valid json")
_write_user("shift_state.json", "{not valid json")
var save := _save_instance()
var a := TestUtils.check(is_equal_approx(float(save.load_settings().get("look_sensitivity", 0.0)),
0.0022), "corrupt settings fall back to defaults")
var b := TestUtils.check(save.load_campaign().is_empty(), "corrupt campaign returns an empty dictionary")
var c := TestUtils.check_eq(save.load_shift_results().size(), 0, "corrupt history returns an empty array")
var d := TestUtils.check(save.load_shift_state().is_empty(), "corrupt checkpoint returns an empty dictionary")
save.free()
_restore_files(backup)
return a and b and c and d
func test_partial_temp_file_is_ignored() -> bool:
var backup := _snapshot_files()
_remove_user("settings.json")
_write_user("settings.json.tmp", "{\"look_sensitivity\": 0.009, ")
var save := _save_instance()
var loaded: Dictionary = save.load_settings()
var ok := TestUtils.check(is_equal_approx(float(loaded.get("look_sensitivity", 0.0)), 0.0022),
"a leftover partial temp file is not treated as settings")
save.free()
_restore_files(backup)
return ok
func test_failed_atomic_write_keeps_existing_save() -> bool:
var backup := _snapshot_files()
_remove_user("settings.json")
_remove_user("settings.json.tmp")
var save := _save_instance()
var first := TestUtils.check_eq(int(save.save_settings({"look_sensitivity": 0.0022})), OK,
"baseline settings write succeeds")
var temp_path := _user_path("settings.json.tmp")
var made := DirAccess.make_dir_absolute(temp_path)
var err: int = save.save_settings({"look_sensitivity": 0.009})
var a := TestUtils.check(made == OK, "test can block the temp path")
var b := TestUtils.check(err != OK, "blocked temp path reports a write error")
var loaded: Dictionary = save.load_settings()
var c := TestUtils.check(is_equal_approx(float(loaded.get("look_sensitivity", 0.0)), 0.0022),
"a failed replace leaves the valid settings file intact")
DirAccess.remove_absolute(temp_path)
save.free()
_restore_files(backup)
return first and a and b and c
+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 d := TestUtils.check(banners != null and banners.get_child_count() >= 9,
"three red event banners with their trim are present")
var e := TestUtils.check(root.find_child("WalkOfFame", true, false) != null,
"the Hollywood Walk of Fame strip is built")
var walk := root.find_child("WalkOfFame", true, false)
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
and root.get_node_or_null("NeighbourBlocks/EastRetailBlock") != null,
"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 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 c := TestUtils.check(code.contains("ALPHA") and code.contains("fog_disabled"),
"backdrop shader is alpha-scissored and fog-free")
var c := TestUtils.check(code.contains("ALPHA") and code.contains("fog_disabled")
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")
builder.set_phase(Rules.Phase.LATE)
var night: Texture2D = mat.get_shader_parameter("sky_texture")
+12
View File
@@ -17,6 +17,12 @@ set -euo pipefail
GODOT="${GODOT:-godot}"
VERSION="$(grep -oP 'config/version="\K[^"]+' project.godot)"
PLAYER_DOCS=(
"CREDITS.md"
"PRIVACY.md"
"docs/accessibility.md"
"docs/known-limitations.md"
)
mkdir -p build
# Presets now embed the PCK into a single binary; drop stale .pck leftovers.
rm -f build/*.pck
@@ -34,6 +40,9 @@ echo "==> Exporting Web (no threads, static-hosting friendly)"
rm -rf build/web
mkdir -p build/web
"$GODOT" --headless --export-release "Web" "build/web/index.html"
for doc in "${PLAYER_DOCS[@]}"; do
cp "$doc" "build/web/$(basename "$doc")"
done
# Ship the web build as a single zip so it can be downloaded and served as-is.
rm -f "build/gig-line-web-${VERSION}.zip"
if command -v zip >/dev/null 2>&1; then
@@ -58,6 +67,9 @@ cp "build/gig-line-linux-${VERSION}.x86_64" "$HQ_DIR/"
cp "build/gig-line-windows-${VERSION}.exe" "$HQ_DIR/"
cp packaging/desktop_hq/override.cfg "$HQ_DIR/"
cp packaging/desktop_hq/README.txt "$HQ_DIR/"
for doc in "${PLAYER_DOCS[@]}"; do
cp "$doc" "$HQ_DIR/$(basename "$doc")"
done
rm -f "build/gig-line-desktop-hq-${VERSION}.zip"
if command -v zip >/dev/null 2>&1; then
(cd "$HQ_DIR" && zip -q -r "../gig-line-desktop-hq-${VERSION}.zip" .)
+1 -1
View File
@@ -46,7 +46,7 @@ BONES = [
]
MAX_TRIANGLES = 6000
MAX_VERTICES = 4000
TEX_SIZE = 128
TEX_SIZE = 256
AI_DIR = TEX_DIR / "ai"
# Base material slots that get an AI fabric albedo (`prepare` layers them over
# the procedural weave). Outer layers (jacket / vest / coat / dress) have no
+84 -4
View File
@@ -78,12 +78,64 @@ for f in "${extras[@]}"; do
fi
done
# Preflight before creating anything on the host: no empty files, and enough
# local disk to assemble/checksum the release. Upload failures are rolled back
# below so a full or interrupted host never keeps a partial release.
total_bytes=0
for f in "${all_artifacts[@]}"; do
if [[ ! -s "$f" ]]; then
echo "error: missing or empty artifact $f — run ./tools/build_releases.sh first" >&2
exit 1
fi
total_bytes=$((total_bytes + $(stat -c '%s' "$f")))
done
free_kb="$(df -Pk build | awk 'NR==2 {print $4}')"
need_kb=$(((total_bytes + 1023) / 1024))
reserve_kb=262144
if [[ -z "${free_kb:-}" || "$free_kb" -lt $((need_kb + reserve_kb)) ]]; then
echo "error: not enough free disk in build/ for ${need_kb} KiB of artifacts plus a 256 MiB reserve (free: ${free_kb:-unknown} KiB)" >&2
exit 1
fi
# Local checksums are useful evidence even though the release host does not need
# a duplicate checksum asset; CI keeps the manifest with the build workspace.
checksum_file="build/SHA256SUMS-$VERSION.txt"
: > "$checksum_file"
for f in "${all_artifacts[@]}"; do
sha256sum "$f" >> "$checksum_file"
done
echo "==> Checksums written to $checksum_file"
cat "$checksum_file"
if [[ "${PUBLISH_DRY_RUN:-0}" == "1" ]]; then
echo "DRY RUN: preflight and checksums passed; no release or upload was created."
exit 0
fi
API="$HOST/api/v1/repos/$REPO/releases"
created_release=0
uploaded_ids=()
rollback_release() {
echo "error: rolling back the partial release upload" >&2
for id in ${uploaded_ids[@]+"${uploaded_ids[@]}"}; do
curl -sS -X DELETE "${AUTH[@]}" "$API/$RELEASE_ID/assets/$id" >/dev/null 2>&1 || true
done
if [[ "$created_release" == "1" ]]; then
curl -sS -X DELETE "${AUTH[@]}" "$API/$RELEASE_ID" >/dev/null 2>&1 || true
echo "removed the new release $TAG" >&2
else
echo "removed only the ${#uploaded_ids[@]} newly uploaded asset(s) from existing release $TAG" >&2
fi
}
echo "==> Creating release $TAG on $HOST/$REPO"
RELEASE_ID="$(curl -s -X POST "$API" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"Gig Line $VERSION — playable prototype. See README for controls.\",\"draft\":false,\"prerelease\":false}" \
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)"
if [[ -n "$RELEASE_ID" ]]; then
created_release=1
fi
# If the release already exists (tag already pushed), reuse it.
if [[ -z "$RELEASE_ID" ]]; then
@@ -96,10 +148,6 @@ fi
echo "Release id: $RELEASE_ID"
for f in "${all_artifacts[@]}"; do
if [[ ! -f "$f" ]]; then
echo "error: missing artifact $f — run ./tools/build_releases.sh first" >&2
exit 1
fi
echo "==> Uploading $(basename "$f")"
# Fail loudly: a silent 4xx here used to leave a release with no artifacts while
# the job still reported success.
@@ -109,8 +157,16 @@ for f in "${all_artifacts[@]}"; do
echo "error: upload of $(basename "$f") failed with HTTP $code" >&2
cat /tmp/release-upload.json >&2 || true
echo "" >&2
rollback_release
exit 1
fi
asset_id="$(python3 -c "import sys,json;print(json.load(open('/tmp/release-upload.json')).get('id',''))" 2>/dev/null || true)"
if [[ -z "$asset_id" ]]; then
echo "error: upload of $(basename "$f") returned no asset id" >&2
rollback_release
exit 1
fi
uploaded_ids+=("$asset_id")
done
# Verify the release actually carries the artifacts we just sent.
@@ -123,9 +179,33 @@ except Exception:
print(len(d.get('assets', [])))" 2>/dev/null || echo 0)"
if [[ "${have:-0}" -lt "${#all_artifacts[@]}" ]]; then
echo "error: release has $have assets, expected ${#all_artifacts[@]}" >&2
rollback_release
exit 1
fi
echo "Verified $have artifacts on the release"
# Optional retention: set RETAIN_RELEASES=7 to keep the newest seven releases.
# The default 0 disables pruning; never delete old releases implicitly.
RETAIN_RELEASES="${RETAIN_RELEASES:-0}"
if [[ "$RETAIN_RELEASES" =~ ^[0-9]+$ ]] && (( RETAIN_RELEASES > 0 )); then
echo "==> Pruning releases older than the newest $RETAIN_RELEASES"
releases_json="$(curl -sS "${AUTH[@]}" "$API?limit=50")"
while IFS= read -r old_id; do
[[ -z "$old_id" ]] && continue
echo " removing release id $old_id"
curl -sS -X DELETE "${AUTH[@]}" "$API/$old_id" >/dev/null 2>&1 || true
done < <(RELEASES_JSON="$releases_json" RETAIN="$RETAIN_RELEASES" python3 - <<'PYPRUNE'
import json, os
try:
data = json.loads(os.environ["RELEASES_JSON"])
except Exception:
raise SystemExit
keep = int(os.environ["RETAIN"])
for release in sorted(data, key=lambda r: r.get("created_at", ""), reverse=True)[keep:]:
print(release.get("id", ""))
PYPRUNE
)
fi
echo ""
echo "Release live: $HOST/$REPO/releases/tag/$TAG"