Compare commits

..
18 Commits
Author SHA1 Message Date
zwitschi 5dc3ef0f4c Add a Blender GLB intake inspection command
Full build / Import + headless tests (push) Successful in 44s
Full build / Full desktop build + optional release (push) Skipped
2026-11-03 10:08:45 +01:00
zwitschi 6f3facff43 Install the Blender skills pack locally and keep it out of git
Full build / Import + headless tests (push) Successful in 38s
Full build / Full desktop build + optional release (push) Skipped
2026-11-03 10:02:34 +01:00
zwitschi 51f537720f Reorder TODO and docs around character and map priorities
Full build / Full desktop build + optional release (push) Skipped
Full build / Import + headless tests (push) Successful in 37s
2026-11-03 09:17:49 +01:00
zwitschi 08e9044593 Add commit, build and version discipline to agent workflow 2026-11-03 09:17:49 +01:00
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
38 changed files with 2523 additions and 222 deletions

No files matched your search

+4
View File
@@ -58,6 +58,10 @@ assets/models/generated/npc_base_*_normal.png
# Local AppImage extraction
squashfs-root/
# Local agent skills (installed per machine from the Blender Skills Pack; not part of
# the game, tests or release artifacts, and never shipped)
.agents/
# Blender asset library: per-asset Cycles renders are local review output (the
# committed review artefacts are docs/blender_previews/sheet_*.jpg), and Godot
# extracts the GLB-embedded textures next to the models on import.
+48 -4
View File
@@ -19,10 +19,23 @@ The project should be developed in small, reviewable steps. Agents must keep pla
- `docs/architecture.md` — The arc42-style architecture documentation (goals, constraints, building blocks, runtime views, decisions, risks).
- `docs/gameplay.md` — Gameplay systems, venues, and the escalation model.
- `docs/asset-tooling.md` — Asset pipelines, model tools, and screenshot gates.
- `docs/npc-model-sourcing.md` — Character-source evaluation, licence rules and the character-development plan.
- `docs/getting-started.md` — Setup, run/test commands, and the controls table.
- `docs/smoke-checklist.md` — The manual smoke-test checklist.
- `docs/adr/` — Architecture Decision Records.
## Priority and Versioning
`TODO.md` is the priority source. The active order is:
1. unversioned development tracks (currently character development and local map development);
2. versioned release milestones, in version order;
3. future/unversioned candidates and deferred work.
Do not assign a version number to an unversioned track until its scope is accepted into a
release. When a version is assigned, it must be the next semantic version that matches the
scope (major/minor/patch), not a placeholder.
## Required Workflow
### 1. Start by Inspecting the Repository
@@ -47,6 +60,9 @@ All planned implementation work must be represented in `TODO.md`.
- Use level-2 headings (`##`) for milestones or stable themes. A milestone should state its goal
and an explicit exit criterion; a theme groups small related work that does not need a
milestone.
- Order level-2 sections by current priority: unversioned development tracks first, then
versioned release milestones in version order, then future/deferred work. Do not assign a
version number to an unversioned track until it is scheduled for a release.
- Use level-3 headings (`###`) for workstreams inside a milestone (for example, `0.8.1
First-shift tutorial`). Keep the hierarchy to three levels: milestone → workstream → checkbox.
- Use checkbox items for each concrete task, normally under a level-3 workstream. Direct
@@ -87,6 +103,10 @@ Recommended format:
- [ ] Document the test procedure in `README.md`.
```
Order the actual file as described in "Priority and Versioning": unversioned development tracks
first (for example character and local-map development), then versioned milestones in version
order, then future/deferred work.
### 3. Move Completed Sections to `DONE.md`
`TODO.md` uses nested sections. Apply these completion rules:
@@ -175,6 +195,8 @@ The root-level `CHANGELOG.md` records the user-facing changes for every release.
- Add an entry for every patch, minor, and major version change.
- Follow Semantic Versioning (`major.minor.patch`): bump major for breaking or vision-level changes, minor for new features, and patch for fixes and small polish.
- A version increment is not complete until its `CHANGELOG.md` entry, `config/version` update and
matching local release build have all been produced and verified.
- Group each entry under `Added`, `Changed`, `Fixed`, and `Removed` headings, and date every entry.
- Describe changes in user-facing terms, not as raw commit messages.
- Add the entry when its work ships. If the version number is not yet known, record it under `Unreleased` and move it into a numbered version when the version is tagged.
@@ -198,13 +220,33 @@ For each task:
5. Update documentation if the change creates lasting context: add features to `docs/development-status.md`, adjust the README links or other `docs/` files as needed, and record decisions/gotchas in `MEMORY.md`.
6. Mark the task complete.
7. Keep completed level-3 workstreams in place until the parent milestone is complete; then move the entire level-2 milestone to `DONE.md` after recording its `CHANGELOG.md` entry.
8. Commit the changes to the version control system with a descriptive message.
9. If a level-2 milestone from `TODO.md` has been fully completed, ensure its `CHANGELOG.md` entry is in place, then create a new version or tag in the version control system to mark the milestone.
10. Push the committed changes to the shared repository if applicable.
11. Note the new version or tag created in the version control system in any relevant documentation or handoff summary.
8. Commit each coherent, verified change as it is completed. Do not wait until a whole
milestone is finished; keep commits small and descriptive and push them when the branch is
shareable.
9. On every major, minor or patch version increment: update `config/version`, add the
`CHANGELOG.md` entry, run the local release build (`tools/build_releases.sh`), verify the
artifacts and checksums, then commit, tag and push the version.
10. If a level-2 milestone from `TODO.md` has been fully completed, ensure its `CHANGELOG.md`
entry and matching build are in place, then create the next semantic version or tag.
11. Push committed changes to the shared repository if applicable.
12. Note the new version, build artifacts or tag in the relevant documentation and handoff
summary.
Avoid unrelated refactors unless they are required for the task or explicitly approved.
### 9. Commit, Build and Version Discipline
- **Commit often.** Make a commit after each coherent change or verified task, not only when a
milestone or release is finished. Keep the working tree clean between tasks where possible.
- **Push regularly.** Push shareable commits to the shared repository; do not accumulate a long
local-only history.
- **Build every version.** Major, minor and patch increments each require a local release build
and artifact/checksum verification before tagging. A changelog entry alone is not a release.
- **CI is a second gate, not a substitute.** Tag builds remain automated, but the local build is
the required pre-tag check for every version.
- **Unversioned tracks stay unversioned.** Character, map and other development tracks do not get
a placeholder version; the version is assigned when the work is scoped into a release.
## Collaboration Rules
- Prefer clear, reversible changes over broad rewrites.
@@ -213,6 +255,8 @@ Avoid unrelated refactors unless they are required for the task or explicitly ap
- Do not claim a task is complete without verification.
- Keep documentation synchronized with the implementation.
- Mention files changed and checks run in handoff summaries.
- Commit coherent increments as you go; do not wait until the end of a milestone.
- Run a release build whenever a major, minor or patch version is incremented.
- Leave follow-up work as checkbox items in `TODO.md` rather than hiding it in prose.
- If blocked, document the blocker in `MEMORY.md` and add a corresponding task to `TODO.md`.
- Avoid overwriting another contributor’s work; inspect recent changes before editing shared files.
+16 -6
View File
@@ -4,6 +4,22 @@ 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
@@ -262,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.
+76
View File
@@ -1957,3 +1957,79 @@ screen-design judge is 21/21 match. The optional realism judge ran with
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.
+98 -13
View File
@@ -4,6 +4,69 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
## Decisions
- **GLB intake inspection (`blender_assets.py inspect`, 2026-11-03):** runs inside Blender, clears
parents and applies object transforms, then reports tris/budget, raw and seam-split verts,
Godot-space bounds (height/feet for characters), bones, materials, UVs, skinning and
animations as `issues` (contract failures, exit 1) or `warnings`. `--json`/`--category` exist
for intake scripts. Gotchas: the glTF importer creates an `Icosphere` mesh used as
`pose_bone.custom_shape`, which must be excluded or it dominates bounds; a parented import
needs `parent_clear(CLEAR_KEEP_TRANSFORM)` before `transform_apply`; Blender is Z-up so bounds
and bone rests are converted to `(x, z, -y)`. On project assets its counts match `audit`
exactly, but `audit` stays the stdlib/CI release gate and `inspect` never adapts a file.
- **Blender Skills Pack installed locally (2026-11-03):** the MIT `blender-skills` pack
(v1.3.0, 94 skills + a shared `references/` folder) is installed at
`.agents/skills/blender-skills/` for Blender asset work. `.agents/` is gitignored and
`.agents/.gdignore` keeps Godot's scanner out, so it never ships, exports or affects tests.
Skills link shared docs as `../references/<file>.md`, so keep the pack flat when updating:
`cp -a <pack>/.claude/skills/. .agents/skills/blender-skills/`. Pi gates project
`.agents/skills` behind project trust, so the trust prompt must be accepted before the
skills load.
- **Priority order (2026-11-03):** `TODO.md` now leads with the unversioned **Character
development** and **Local map development** tracks, then the versioned 0.10.0 release
candidate and later releases. Do not attach a placeholder version to an unversioned track;
assign one only when its scope enters a release. Character development is generator-first
(ADR-0015); local map development evaluates OSM/3D data, licensed reference imagery,
AI-assisted composition and development-only Gaussian splatting.
- **Commit/build discipline (2026-11-03):** commit each coherent verified change as it lands, not
only at milestone end, and push shareable commits. Every major, minor and patch version requires
a `CHANGELOG.md` entry, a local `tools/build_releases.sh` build and artifact/checksum
verification before tagging. CI tag builds are the second gate, not a substitute.
- **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 /
@@ -88,7 +151,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
`Materials.NPC_FABRIC_UV_SCALE = Vector3(4,4,1)`, and `NpcAvatar` skips the baked per-character
AO on those two slots. The 256² AO was magnifying into large blotches; the fabric now reads as a
fine weave. Skin/hair/gear keep baked AO. Test `test_fabric_uv_scale_avoids_the_baked_ao_pattern`
guards this. Current suite is 263/263.
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;
@@ -175,14 +238,30 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
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, **0.9.4** after the NPC pass, now **0.9.5** after the texture
and material pass. Next work is **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.5` tag is the release trigger; no
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, 0.9.4 NPC visual optimization and 0.9.5 texture/material pass
are complete and in `DONE.md`. 0.9.6 integration and 0.10.0 release work are
next. The current source version is 0.9.6.
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
@@ -190,6 +269,11 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
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:
@@ -836,7 +920,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 **263/263**; Python tooling is **101/101**; screenshot compare is **21 good / 0 bad** and the automatic judge is **21/21 match, 0 diff**. Remaining visual follow-ups are in `TODO.md`.
- 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
@@ -859,8 +943,9 @@ 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, the 0.9.5 texture/material pass, and the Blender + realism asset pipeline. `TODO.md` holds the remaining
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.
@@ -868,7 +953,7 @@ GitHub Actions.
## Uncommitted Workstreams
- **None (2026-11-02).** The 0.9.5 texture pass is committed, tagged `v0.9.5` 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`
- **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.
+22 -4
View File
@@ -14,6 +14,23 @@ The game is about **judgment, communication, access verification, and proportion
- **Emergency help is a last resort.** Responders handle crises the guard cannot safely manage; calling them has costs.
- **Readability beats raw fidelity.** The GL Compatibility renderer, clear silhouettes, the focus ring, and the HUD stay legible.
## Development Priorities
The active planning order is set in [`TODO.md`](TODO.md) and in `AGENTS.md`:
1. **Character development (unversioned).** Expand the owned procedural character library with
crowd, age, build, hair and outfit variety. Keep the frozen bone/tint/gear contract; see
[ADR-0015](docs/adr/0015-npc-model-variety-sourcing.md) and
[`docs/npc-model-sourcing.md`](docs/npc-model-sourcing.md). Quaternius is a reference;
MakeHuman is deferred to a separate local pipeline.
2. **Local map development (unversioned).** Move beyond stylized venue reconstruction with
licensed OpenStreetMap and 3D building data, downloaded reference imagery for photoreal
facades, AI-assisted composition, and a Gaussian-splatting evaluation that stays in the
development pipeline unless it proves runtime-safe.
3. **Versioned release hardening.** The 0.10.0 release candidate and later semantic versions come
after the unversioned development tracks in planning order. Versions are assigned only when a
track's scope is accepted into a release.
## Core Gameplay Loop
1. Watch the entrance, steps, queue, and sidewalk.
@@ -152,14 +169,15 @@ The action wheel prioritizes de-escalation: Stop, Look, Point, No, Ask, Inspect,
The visual direction is readable stylized realism:
- **Blender library:** 8 skinned NPC variants, 18 gear pieces, and 20 street/venue props, with baked albedo/normal/AO/ORM maps, a manifest, and procedural fallbacks. `Rig/Head` stays the focus anchor.
- **Character direction:** 8 skinned NPC variants, 18 gear pieces and 20 street/venue props, with four crowd-variety variants prototyped. The generator-first expansion is the active character track ([ADR-0015](docs/adr/0015-npc-model-variety-sourcing.md)); Quaternius is a reference and MakeHuman is deferred to a local pipeline.
- **Realism pass:** AI-assisted per-phase skyline panoramas, v2 surface tiles with normal maps, wet asphalt at night, a warm day haze, phase-specific sun/exposure/fog, a visible flashlight beam, and denser foliage.
- **Local map direction:** the venues are currently stylized reconstructions. The local-map track evaluates licensed OpenStreetMap/3D building data, licensed reference photography, AI-assisted composition and Gaussian splatting as a development tool; online data and splat rendering are not runtime dependencies.
- **References and QA:** runtime UI art lives in `assets/images/ui/`; reference/art-direction sets live in `docs/screens/designs/` and `docs/screens/concepts/`; captures live in `docs/screenshots/`; realism runs live in `docs/screens/realism/`. `screen_designs.py compare/judge` and `tools/check_screenshots.sh` guard drift.
- **Storage convention:** see [ADR-0014](docs/adr/0014-runtime-image-root.md). Never overwrite an existing image; new outputs get a new path or `-vN` suffix.
The path from the current vertical slice to the 0.10.0 release candidate — content depth, onboarding, accessibility, platform QA and release readiness — is tracked in [`TODO.md`](TODO.md); this plan stays at the product/design level.
The active planning order — unversioned character and local-map development first, then the versioned 0.10.0 release-candidate hardening and later releases — is tracked in [`TODO.md`](TODO.md); this plan stays at the product/design level.
Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [ADR-0008](docs/adr/0008-cone-focus-and-head-ring.md), [ADR-0009](docs/adr/0009-radial-action-menu.md), [ADR-0010](docs/adr/0010-procedural-npc-presentation.md), [ADR-0011](docs/adr/0011-aim-free-selection-and-precision-walk.md), [ADR-0012](docs/adr/0012-surface-materials-and-generated-props.md), [ADR-0013](docs/adr/0013-blender-authored-npc-characters.md).
Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [ADR-0008](docs/adr/0008-cone-focus-and-head-ring.md), [ADR-0009](docs/adr/0009-radial-action-menu.md), [ADR-0010](docs/adr/0010-procedural-npc-presentation.md), [ADR-0011](docs/adr/0011-aim-free-selection-and-precision-walk.md), [ADR-0012](docs/adr/0012-surface-materials-and-generated-props.md), [ADR-0013](docs/adr/0013-blender-authored-npc-characters.md), [ADR-0015](docs/adr/0015-npc-model-variety-sourcing.md).
## Tech Stack
@@ -176,7 +194,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.6`). 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/): every major, minor and patch increment needs a changelog entry, a local `tools/build_releases.sh` build, and artifact/checksum verification before tagging. Unversioned development tracks do not receive a version until their scope enters a release. See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands.
## Design Direction
+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.
+45 -15
View File
@@ -6,6 +6,16 @@ The central design goal is simple:
> The player wins by choosing the lowest level of escalation that keeps everyone safe.
## Development Priorities
The active planning order lives in [`TODO.md`](TODO.md) and [`AGENTS.md`](AGENTS.md):
1. **Character development** — crowd, age, build, hair and outfit variety on the owned procedural library, keeping the frozen NPC contract. Source evaluation and the licence decision are in [`docs/npc-model-sourcing.md`](docs/npc-model-sourcing.md).
2. **Local map development** — licensed OpenStreetMap/3D data, reference-derived photoreal facades, AI-assisted composition and a Gaussian-splatting development evaluation.
3. **Versioned release hardening** — the 0.10.0 release candidate and later semantic versions follow the unversioned development tracks.
Versions are assigned only when a track's scope is accepted into a release. Every major, minor and patch version requires a changelog entry, a local release build and artifact verification before tagging.
## Screenshots
| | |
@@ -38,13 +48,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.5** — the texture and material refinement pass:
> 256² NPC maps, a four-times fabric weave without baked AO blotches, repair of
> the Walk of Fame terrazzo band and awning stripe, and per-phase skyline haze,
> on top of the v0.9.4 NPC visual optimization, the Hollywood/Joke Shop maps and
> the v0.9.3 UI/accessibility polish.
> **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.6** — 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
@@ -106,12 +117,14 @@ Full setup, every run/test/smoke command, and the controls table are in
## Project Documentation
- [`PLAN.md`](PLAN.md) — Game design plan and scenario details.
- [`AGENTS.md`](AGENTS.md) — Development and collaboration workflow.
- [`PLAN.md`](PLAN.md) — Game design plan, development priorities and scenario details.
- [`TODO.md`](TODO.md) — Priority-ordered tasks: character and local-map development, release milestones, and future work.
- [`docs/README.md`](docs/README.md) — Documentation index, current priorities and visual previews.
- [`docs/npc-model-sourcing.md`](docs/npc-model-sourcing.md) — Character-source evaluation, licence rules and the character-development plan.
- [`AGENTS.md`](AGENTS.md) — Development, commit, build and collaboration workflow.
- [`DONE.md`](DONE.md) — Completed task sections.
- [`MEMORY.md`](MEMORY.md) — Persistent session notes.
- [`CHANGELOG.md`](CHANGELOG.md) — User-facing changes by version.
- [`docs/README.md`](docs/README.md) — Documentation index and visual previews.
- [`docs/architecture.md`](docs/architecture.md) — arc42-style architecture documentation.
- [`docs/gameplay.md`](docs/gameplay.md) — Gameplay systems, venues, and the escalation model.
- [`docs/hollywood-boulevard-map.md`](docs/hollywood-boulevard-map.md) — Real-site references, game mapping and limitations for the Hollywood venue.
@@ -120,24 +133,41 @@ 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; 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.6 is the current source version: the integrated texture/material and UI
checkpoint on top of the 0.9.5 texture refinement (256² NPC maps, a 4× fabric weave with baked-AO removal, repaired
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).
tutorial/campaign/resume work and the v0.7.x venue/gear/judge work. The active
planning priorities are the unversioned character and local-map development tracks;
the 0.10.0 release candidate is the next versioned release. See
[`docs/development-status.md`](docs/development-status.md) for the implemented list
and [`TODO.md`](TODO.md) for the priority-ordered 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
+409 -164
View File
@@ -3,13 +3,404 @@
Work is grouped by theme. Each item should be small enough to complete and verify on its own;
see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these plans.
**Priority order:** Character development → Local map development → versioned release milestones →
future/deferred work. See `AGENTS.md` §Priority and Versioning.
## 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. This is the current development priority; keep it out of the frozen
0.10.0 release candidate and assign a release version only when its scope is accepted.
### 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
- [x] 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.
(`blender_assets.py inspect --file ...` applies parent/object transforms, converts to
Godot Y-up, ignores the importer's bone-shape helper and prints budgets, bones, slots,
UVs, skinning and animations with issues/warnings; `--json` and `--category` supported.
Seven synthetic contract tests added (Python 108/108); all 46 library GLBs inspect clean,
and a wrapped 2x fixture correctly reported a 0.975 m local/world delta. Blender-side only;
`audit` remains the final gate.)
- [ ] 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.
## 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 remains the next versioned release after the 0.9.x patch
line, but follows the unversioned character and local-map tracks in planning order.
### Content lock and external playtest
- [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;
keep the rest as explicit known limitations or post-0.10 items.
- [ ] Re-run the affected tutorial/campaign smokes after each fix and capture a fresh first-hour
walkthrough for the release notes.
### Balance and fairness pass
- [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. 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`
or `MEMORY.md`.
### Save migration and session reliability
- [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
- [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.
(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.
(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
- [x] Audit AI-assisted asset provenance and licenses; record source/model usage and any
redistribution constraints for textures, characters, props and UI art.
(`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.
(`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
- [ ] Update `README.md`, `PLAN.md`, `docs/architecture.md`, ADRs, `docs/development-status.md`,
`docs/getting-started.md`, `docs/smoke-checklist.md`, `CHANGELOG.md` and this roadmap.
- [ ] Regenerate the full screenshot gallery and realism baselines after content lock; run
`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.
- [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
- [ ] Run the complete headless suite, Python tooling, tutorial/campaign/session/scenario smokes,
24-NPC budget smoke and release build from a clean checkout.
- [ ] Run the manual QA matrix: fresh install, upgrade, offline launch, gamepad, accessibility
settings, pause/resume, all venues and Desktop HQ fallback.
- [ ] Confirm no P0/P1 defects remain; P2 items must be documented as known limitations or
post-0.10 work.
- [ ] Create the `v0.10.0` version bump, changelog entry and tag; build/publish the release and
verify all assets and download links.
- [ ] Exit: the release candidate is content-locked, externally played, legally reviewed,
documented, reproducibly built, and approved as the 0.10.0 release candidate.
## Gap Analysis and 0.x Roadmap
Baseline: **v0.9.5** (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, 263 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.
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 the
baseline; 0.10.0-rc.1 is the current candidate source, with release approvals still open.
### Documentation audit (completed 2026-11-01)
@@ -32,6 +423,8 @@ release-candidate foundation, not yet a 0.10.0 release candidate.
| Area | Current baseline | Remaining gap | Risk | Milestone |
| --- | --- | --- | --- | --- |
| Character development | 8-variant library; source/licence review and Quaternius/MakeHuman evaluation complete | Four crowd variants, role pools, gear fit and runtime integration | Repetition remains or external assets break the contract | Unversioned |
| Local map development | Stylized venue geometry and reference maps | OSM/3D data, photoreal references, AI-assisted composition and splat evaluation | Licensing/provenance or performance failure | Unversioned |
| Content and balance | 3 venues with validated 12+ event matrices, recovery events and contextual dialogue | External playtest evidence, balance/fairness pass, event-weight tuning and final content lock | Repetition or unfair outcomes survive into the release candidate | 0.10.0 |
| Dialog/UI | Audited terminology, copy, button states, focus/navigation, modal close, long strings and all 21 screen captures | Keep the capture/compare/judge gate current as content changes | Low; the remaining risk is drift after new UI is added | Complete (0.9.3) |
| 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) |
@@ -42,167 +435,14 @@ release-candidate foundation, not yet a 0.10.0 release candidate.
| 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**, **0.9.4 NPC visual optimization** and **0.9.5 texture
refinement** passes are complete (all moved to `DONE.md` on 2026-11-02).
0.9.6 integration and the 0.10.0 release candidate are next. The source version
is now 0.9.5; the v0.9.5 release is published.
## 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.)
- [-] Tag/version or otherwise freeze `0.9.6` as the integration checkpoint before
the `0.10.0` release candidate. (claimed; committing and tagging)
- [ ] 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
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.
- [ ] 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;
keep the rest as explicit known limitations or post-0.10 items.
- [ ] Re-run the affected tutorial/campaign smokes after each fix and capture a fresh first-hour
walkthrough for the release notes.
### Balance and fairness pass
- [ ] Define measurable targets for routine compliance, access-decision accuracy, peaceful
resolution, justified emergency calls and false-positive/false-negative rates.
- [ ] 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.
- [ ] 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`
or `MEMORY.md`.
### 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.
### 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.
- [ ] 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
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
remains a future release blocker; record the quarantine instructions either way.
### Legal, credits and platform policy
- [ ] 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
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.
- [ ] Mark external legal/storefront approval as complete or explicitly block the 0.10.0 release on it.
### Documentation and visual sign-off
- [ ] Update `README.md`, `PLAN.md`, `docs/architecture.md`, ADRs, `docs/development-status.md`,
`docs/getting-started.md`, `docs/smoke-checklist.md`, `CHANGELOG.md` and this roadmap.
- [ ] Regenerate the full screenshot gallery and realism baselines after content lock; run
`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.
### Release-candidate QA and exit
- [ ] Run the complete headless suite, Python tooling, tutorial/campaign/session/scenario smokes,
24-NPC budget smoke and release build from a clean checkout.
- [ ] Run the manual QA matrix: fresh install, upgrade, offline launch, gamepad, accessibility
settings, pause/resume, all venues and Desktop HQ fallback.
- [ ] Confirm no P0/P1 defects remain; P2 items must be documented as known limitations or
post-0.10 work.
- [ ] Create the `v0.10.0` version bump, changelog entry and tag; build/publish the release and
verify all assets and download links.
- [ ] Exit: the release candidate is content-locked, externally played, legally reviewed,
documented, reproducibly built, and approved as the 0.10.0 release candidate.
**Current priority:** the unversioned **Character development** and **Local map development**
tracks are the active planning order. The **0.10.0 release candidate** follows them in this list
as the next versioned release; content lock, external playtest, balance, save migration and
release/legal sign-off remain tracked there. The source version is 0.10.0-rc.1; the v0.9.6
release is published. Earlier map/NPC/UI/texture/integration passes are complete and in
`DONE.md`.
## Future 0.x / non-blocking candidates
@@ -220,6 +460,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.
+18 -5
View File
@@ -2,19 +2,32 @@
This folder holds the project documentation, visual references and generated QA evidence.
## Current priorities
1. [Character development](../TODO.md#character-development) — crowd, age, build, hair and outfit variety; generator-first, with the frozen NPC contract.
2. [Local map development](../TODO.md#local-map-development) — OpenStreetMap/3D data, licensed reference imagery, photoreal facades, AI-assisted composition and Gaussian-splatting evaluation.
3. [0.10.0 release candidate](../TODO.md#milestone-0100--release-candidate) — versioned release hardening after the unversioned development tracks.
## Start here
- [`../PLAN.md`](../PLAN.md) — product and game-design plan
- [`gameplay.md`](gameplay.md) — controls, venues and escalation
- [`joke-shop-map.md`](joke-shop-map.md) — Sunset Strip real-site mapping
- [`tutorial.md`](tutorial.md) — first-shift tutorial beats
- [`../PLAN.md`](../PLAN.md) — product, design intent and development priorities
- [`../TODO.md`](../TODO.md) — priority-ordered active tasks and roadmap
- [`npc-model-sourcing.md`](npc-model-sourcing.md) — character-source evaluation, licence rules and the character-development plan
- [`architecture.md`](architecture.md) — arc42 system architecture
- [`getting-started.md`](getting-started.md) — setup, run/test commands and controls
- [`asset-tooling.md`](asset-tooling.md) — image/model pipelines and screenshot gates
- [`gameplay.md`](gameplay.md) — controls, venues and escalation
- [`joke-shop-map.md`](joke-shop-map.md) — Sunset Strip real-site mapping
- [`tutorial.md`](tutorial.md) — first-shift tutorial beats
- [`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
- [`development-status.md`](development-status.md) — implemented features and current gaps
- [`development-status.md`](development-status.md) — implemented features, current gaps and active planning
- [`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
+26
View File
@@ -27,6 +27,12 @@ godot --headless --export-release "Linux" build/gig-line-smoke.x86_64
`project.godot` sets `config/version`. Keep it in sync with the release tag
(e.g. `v0.3.0` → `config/version="0.3.0"`, plus a git tag `v0.3.0`).
Every major, minor and patch increment is a version in its own right: add the
`CHANGELOG.md` entry, run the local build (`tools/build_releases.sh`), verify the
artifact names/sizes/checksums, and only then commit and tag. A changelog entry
without a matching build is not a completed version. Unversioned development tracks
do not get a placeholder version; assign one when the scope enters a release.
## 3. CI (Gitea Actions)
The repository ships a Gitea Actions workflow (`.gitea/workflows/build.yml`):
@@ -103,6 +109,26 @@ 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
+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)
+83 -3
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.6`). 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
@@ -117,6 +117,7 @@ The asset/content pipeline is part of the architecture, not an external build de
| Gear/props | Blender flatpak + tool + bake | `npc_gear/*.glb`, `props/*.glb`, `*_ao.png` | `NpcAvatar`, `WorldBuilder` | primitive assets / generated props |
| Surface/UI art | Python pipelines / AI-assisted references | `assets/textures/ai/game_ready/`, `assets/images/` | `SurfaceMaterials`, `UiArt` | neutral material / procedural UI |
| Screenshots/QA | capture scripts + `scene_concepts`/`screen_designs` | `docs/screenshots/`, `docs/screens/realism/` | reviewers and CI gates | regenerate; never runtime input |
| Local map data (planned) | OSM/3D building extract + licensed reference photos + AI/splat evaluation | derived map assets with provenance sidecars | `WorldBuilder` venue specs | current stylized venue geometry |
| Release | Gitea workflow + build/publish scripts | platform binaries, Web zip, Desktop HQ zip | players | source build |
Contracts that keep the pipeline replaceable:
@@ -127,6 +128,80 @@ 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.
Four crowd-variety prototypes have been built and audited locally but are not yet in
the shipped catalogue; generator-first expansion is the active character track
(ADR-0015). 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).
### Local map development tooling (planned)
The current venues are hand-authored procedural reconstructions in
`src/game/world_builder.gd`. The **Local map development** track in
[`TODO.md`](../TODO.md#local-map-development) is unversioned and planned: it will
test whether licensed data and reference photography can replace or supplement the
stylized geometry without moving map data, AI composition or splat rendering into
the runtime.
Planned stages and gates:
| Stage | Source | Output | Gate |
| --- | --- | --- | --- |
| Footprints and massing | Offline OpenStreetMap extract + licence-compatible 3D building data (OSM Buildings, Overture, CityGML/LOD1–2) | Local footprint/massing scenes, extract date and query pinned | ODbL attribution/share-alike review; scale/orientation and venue-footprint comparison |
| Reference imagery | Wikimedia Commons, own/team photos or other clearly licensed images | Reference boards and material candidates | Written author/licence/attribution, privacy redaction, no unlicensed Street View or scraped commercial imagery |
| AI-assisted composition | Project image tooling | Palette/facade-rhythm boards and material candidates | Model/prompt/output recorded; human review; never treated as licensed map data |
| Photoreal facade kit | Project-authored geometry and derived materials | Committed facade modules and PBR maps | WorldBuilder budget, collider/nav coverage, Compatibility-readability and screenshot review |
| Gaussian splatting | Licensed/permissioned photo sets + pinned offline toolchain | Development-only splat, texture or proxy experiments | Per-path ADR; runtime only if performance, collision/navigation and provenance are proven |
Contracts for any future map source:
- Runtime consumes committed, derived assets only; no online OSM, photo or splat
service is a build or play dependency.
- OSM ODbL, photo licences, capture permissions and privacy redaction are recorded
in a provenance sidecar before an asset enters the repository.
- Collision and navigation stay mesh-based; a splat may never become the playable
floor or the access/occlusion authority.
- The stylized venue geometry remains the fallback until a data-backed block passes
the venue budget, navigation, visual and provenance gates.
### Content authoring and runtime data flow
`ScenarioFactory` is the source of truth for the three authored venues. `tools/export_venues.gd`
@@ -272,6 +347,8 @@ 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.
- Local-map data and photo-derived assets add a second provenance and privacy risk: OSM share-alike, photo rights, faces/plates, property permission and rapidly churning splat tooling must be resolved before any derived asset ships.
## 12. Documentation Consolidation and Ownership
@@ -287,11 +364,14 @@ 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/npc-model-sourcing.md`](npc-model-sourcing.md) | Character-source evaluation, licence rules and the character-development plan | Supports the character tooling section and ADR-0015 |
| [`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 |
| [`docs/development-status.md`](development-status.md) | Implemented features and current gaps | Status companion to the roadmap |
| [`TODO.md`](../TODO.md) | Active tasks and 0.x roadmap | Planning source; no architecture duplication |
| [`TODO.md`](../TODO.md) | Priority-ordered active tasks, unversioned development tracks and versioned roadmap | Planning source; no architecture duplication |
| [`CHANGELOG.md`](../CHANGELOG.md) | User-facing release history | Release/version companion |
Consolidation decisions:
@@ -299,7 +379,7 @@ Consolidation decisions:
- **Architecture owns the "how"** (components, contracts, data flow, constraints, quality gates); **PLAN/gameplay own the "what"**; **ADRs own the "why"**. Links replace duplicated prose.
- **Asset and release documents remain separate** because they are command-heavy operational procedures; their architecture-relevant contracts and failure modes are summarized in §5 and §7.
- **Completed plans are removed, not kept as competing roadmaps.** Historical graphics work is summarized by ADR-0013, the asset pipeline section and the v0.6.0 changelog; new work belongs in TODO.
- **Status and roadmap are separate from architecture.** `docs/development-status.md` records what exists; `TODO.md` records what remains, including the 0.x gap analysis and milestone exit criteria.
- **Status and roadmap are separate from architecture.** `docs/development-status.md` records what exists; `TODO.md` records what remains, ordered as unversioned development tracks, versioned milestones and deferred work.
- **Documentation drift is a tracked risk:** release/version, test counts, renderer status, and stale plans must be corrected in the same commit as the change that invalidates them.
## 13. Glossary
+68
View File
@@ -129,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) |
@@ -138,9 +143,72 @@ flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- render
python3 tools/blender_assets.py sheet # docs/blender_previews/sheet_*.jpg
python3 tools/blender_assets.py audit # no Blender needed
python3 tools/blender_assets.py fit # gear-vs-body fit report
godot --headless --path . --import
```
### Intake inspection (`inspect`)
`audit` reads accessor-local bounds and ignores node transforms, so it is the right
release gate but a poor first look at a downloaded or newly authored GLB. `inspect`
imports the file into Blender, clears parents and applies object transforms, then
reports what the scene actually contains:
```bash
# one asset, a batch, or an external download; repeat --file as needed
flatpak run org.blender.Blender --background \
--python "$PWD/tools/blender_assets.py" -- inspect \
--file "$PWD/assets/models/blender/npc/npc_casual.glb" \
--file "$PWD/tmp/downloads/SomeCharacter.glb"
# machine-readable, for intake scripts
... -- inspect --file "$PWD/tmp/x.glb" --json
# force a contract family instead of inferring it from the path
... -- inspect --file "$PWD/tmp/x.glb" --category npc
```
It prints triangles (with the category budget), raw and seam-split vertex counts,
Godot-space bounds with height/feet for characters, whether transforms had to be
applied, mesh nodes and armatures, bone names, materials, UV layers, skinning and
animation count. Hard contract failures (`issues`) fail the command; advisory
findings (`warnings`) do not. The category is inferred from the path:
`blender/npc` → `npc`, `blender/npc_gear` → `npc_gear`, `blender/props` and
`generated` → `props`, anything else → `external` (measured but with no project
budget or rig contract).
Caveats:
- The glTF importer adds a bone-display mesh (`Icosphere`, used as
`pose_bone.custom_shape`). `inspect` excludes custom-shape helpers so they never
pollute bounds or the unskinned list.
- Bounds and bone rests are converted from Blender Z-up to Godot Y-up (`x, z, -y`),
matching the contract bands and the audit's coordinate space.
- Numbers are Blender-side measurements. They agree with `audit` on project assets
(same triangle and glTF vertex counts), but `audit` stays the single final gate:
it is stdlib-only, runs in CI, and matches what the engine imports.
- `inspect` never writes or adapts the file; scale/grounding/weld/retarget work is the
separate (not yet built) `adapt` step.
## Local map development (planned)
The **Local map development** track in [`TODO.md`](../TODO.md#local-map-development)
is not implemented yet; there are no map-generation commands to run. The planned
pipeline will evaluate:
- offline OpenStreetMap extracts for footprints and licence-compatible 3D building
massing (OSM Buildings, Overture, CityGML/LOD1–2);
- downloaded reference imagery from Wikimedia Commons, own/team photography or
other clearly licensed sources, with author/licence/attribution and privacy
redaction recorded per set;
- AI-assisted composition boards for palette, facade rhythm and material study,
with model/prompt/output provenance and human acceptance;
- Gaussian splatting as a development-only capture/reference/texture/proxy tool,
version-pinned offline and outside CI, until an ADR proves runtime safety.
No online map/photo/splat service is a build or play dependency. Committed map
assets will need provenance sidecars, ODbL/photo-licence review and the same
`WorldBuilder` venue-budget/navigation/collider gates as the current stylized
geometry. The detailed task plan and exit criteria live in `TODO.md`.
## Confirmed game assets
Everything below is audited and shipped in the game; development-only outputs
+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.
+3
View File
@@ -48,6 +48,9 @@ python3 tools/blender_assets.py sheet # -> docs/blender_previews/sheet_<catego
python3 tools/blender_assets.py audit # stdlib only; exit 1 on any issue
python3 tools/blender_assets.py fit # head/clothing fit + bag-body clearance, per character
python3 tools/blender_assets.py list
# import a GLB, apply node transforms, report geometry/bones/UVs/skinning + contract failures
flatpak run org.blender.Blender --background \
--python "$PWD/tools/blender_assets.py" -- inspect --file "$PWD/tmp/downloads/x.glb"
godot --headless --path . --import # after any rebuild
```
+2
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) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+8 -1
View File
@@ -1,11 +1,15 @@
# Development Status
Current planning priorities are the unversioned **Character development** and **Local map
development** tracks in [`TODO.md`](../TODO.md); neither is part of the implemented list below
yet. The 0.10.0 release candidate remains the next versioned release.
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: **263 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
- 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 +46,9 @@ 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>.
+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.
+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.6"
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
+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": []
}
+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
+343
View File
@@ -12,6 +12,9 @@ integration: nothing in the game loads them yet (see
# Cycles preview renders -> docs/blender_previews/<asset>.png (local only)
flatpak run org.blender.Blender --background \
--python "$PWD/tools/blender_assets.py" -- render
# inspect an exported or external GLB exactly as Blender imports it
flatpak run org.blender.Blender --background \
--python "$PWD/tools/blender_assets.py" -- inspect --file some.glb
# outside Blender (stdlib + Pillow)
python3 tools/blender_assets.py audit # GLB contract checks
python3 tools/blender_assets.py fit # gear-vs-body fit measurements
@@ -1788,6 +1791,324 @@ def audit_asset(category, name):
return res
# ---------------------------------------------------------------------------
# Intake inspection (import a GLB the way Blender sees it)
# ---------------------------------------------------------------------------
# `audit` reads accessor-local bounds and ignores node transforms, so an
# external GLB can pass locally and sit metres away in the scene. `inspect`
# imports the file, applies node transforms and measures world space.
INSPECT_TRANSFORM_TOLERANCE = 0.005 # metres of local/world drift before reporting
INSPECT_CATEGORY_DIRS = {"npc": "npc", "npc_gear": "npc_gear", "props": "props",
"generated": "props", "blender": "external"}
def category_for_path(path) -> str:
"""Guess the contract family from a GLB path (npc, npc_gear, props, external)."""
p = Path(path)
stem = p.stem
parent = p.parent.name
if parent in INSPECT_CATEGORY_DIRS:
return INSPECT_CATEGORY_DIRS[parent]
if stem in CHARACTERS:
return "npc"
if stem in GEAR:
return "npc_gear"
if stem in PROPS:
return "props"
return "external"
def analyse_inspection(stats: dict) -> dict:
"""Pure contract analysis of `inspect_glb()` measurements (no Blender).
Hard contract failures land in `issues`; advisory findings in `warnings`.
The category gates the budget/rig checks; `external` assets only get the
generic checks so an unadapted download can be measured before intake.
"""
category = str(stats.get("category", "external"))
issues: list[str] = []
warnings: list[str] = []
tris = int(stats.get("triangles", 0))
world_verts = int(stats.get("split_vertices", 0))
meshes = list(stats.get("mesh_nodes", []))
bones = list(stats.get("bones", []))
materials = list(stats.get("materials", []))
bones_ok = {b for b in BONES if b in bones}
needed = set(BONES) | {"Hips", "Spine", "Neck"}
if not meshes:
issues.append("no mesh objects")
if tris <= 0:
issues.append("no triangles")
bounds = stats.get("world_bounds") or {}
lo, hi = bounds.get("min"), bounds.get("max")
if not lo or not hi:
issues.append("empty world bounds")
for value in (tris, world_verts, *(lo or []), *(hi or [])):
if isinstance(value, float) and not math.isfinite(value):
issues.append("non-finite measurement")
break
if category == "npc":
if "npc_base" not in meshes:
issues.append("no mesh node named npc_base")
for bone in sorted(needed - set(bones)):
issues.append(f"missing bone {bone}")
for slot in SLOTS:
if slot not in materials:
issues.append(f"missing material slot {slot}")
unskinned = list(stats.get("unskinned", []))
if not stats.get("skinned", False) or unskinned:
issues.append("mesh not skinned" + (f" ({', '.join(unskinned)})" if unskinned else ""))
if tris > CHAR_MAX_TRIS:
issues.append(f"triangles {tris} > {CHAR_MAX_TRIS}")
if world_verts > CHAR_MAX_VERTS:
issues.append(f"glTF vertices {world_verts} > {CHAR_MAX_VERTS}")
if lo and abs(float(lo[1])) > 0.03:
issues.append(f"feet not on the ground (min y {float(lo[1]):.3f})")
if hi and not (1.6 < float(hi[1]) < 2.1):
issues.append(f"unexpected height {float(hi[1]):.2f} m")
elif category == "npc_gear":
if tris > GEAR_MAX_TRIS:
issues.append(f"triangles {tris} > {GEAR_MAX_TRIS}")
elif category == "props":
if tris > PROP_MAX_TRIS:
issues.append(f"triangles {tris} > {PROP_MAX_TRIS}")
else:
warnings.append("external asset: no project budget or rig contract applied")
if int(stats.get("meshes_without_uv", 0)) > 0:
issues.append(f"{int(stats['meshes_without_uv'])} mesh(es) without UVs")
if int(stats.get("meshes_without_material", 0)) > 0:
warnings.append(f"{int(stats['meshes_without_material'])} mesh(es) without a material")
if int(stats.get("objects", 0)) > max(4, tris // 2000):
warnings.append(f"{stats['objects']} objects: merge before runtime use (draw calls)")
if float(stats.get("transform_delta", 0.0)) > INSPECT_TRANSFORM_TOLERANCE:
warnings.append(
"node transforms changed the pose by "
f"{float(stats['transform_delta']):.3f} m; apply transforms before export "
"(`audit` reads local bounds)")
if int(stats.get("animations", 0)) > 0:
warnings.append(f"{int(stats['animations'])} animation(s) imported; the runtime has "
"no clip for them")
if int(stats.get("images", 0)) == 0 and materials and category in ("npc", "external"):
# Library gear/props are intentionally solid-colour palette materials.
warnings.append("no images: materials are untextured")
return {
"ok": not issues,
"category": category,
"issues": issues,
"warnings": warnings,
"budget": {
"max_triangles": {"npc": CHAR_MAX_TRIS, "npc_gear": GEAR_MAX_TRIS,
"props": PROP_MAX_TRIS}.get(category),
"max_vertices": CHAR_MAX_VERTS if category == "npc" else None,
},
"bones_found": sorted(bones_ok),
"bones_missing": sorted(needed - set(bones)) if category == "npc" else [],
}
def inspect_glb(path, category=None) -> dict:
"""Import `path` into the current Blender session and measure it.
Node transforms are applied first, so bounds and bone rests match what the
scene would export rather than the accessor-local values `audit` reads.
"""
if bpy is None:
return {"ok": False, "error": "inspect must run inside Blender", "issues": [],
"warnings": [], "category": category or "external"}
file_path = Path(path).expanduser()
if not file_path.is_file():
return {"ok": False, "error": f"missing file {file_path}", "issues": [],
"warnings": [], "category": category or "external"}
reset_scene()
try:
bpy.ops.import_scene.gltf(filepath=str(file_path))
except RuntimeError as exc:
return {"ok": False, "error": f"import failed: {exc}", "issues": [],
"warnings": [], "category": category or "external"}
armatures = [o for o in bpy.context.scene.objects if o.type == "ARMATURE"]
# The glTF importer adds a bone-display helper (an Icosphere used as
# pose_bone.custom_shape). It is not part of the asset, so never measure it.
helpers = {pb.custom_shape.name for arm in armatures for pb in arm.pose.bones
if pb.custom_shape is not None}
meshes = [o for o in bpy.context.scene.objects
if o.type == "MESH" and o.name not in helpers]
local = _bounds_of(meshes, world=False)
world = _bounds_of(meshes, world=True)
delta = _bounds_delta(local, world)
applied = _apply_imported_transforms(meshes, armatures)
if applied:
world = _bounds_of(meshes, world=True)
bones = [b.name for arm in armatures for b in arm.data.bones]
bone_heads = {b.name: _to_godot(arm.matrix_world @ b.head_local, digits=3)
for arm in armatures for b in arm.data.bones}
uv_layers = sorted({uv.name for o in meshes for uv in o.data.uv_layers})
objects = [{
"name": o.name,
"triangles": _triangle_count(o),
"vertices": len(o.data.vertices),
"split_vertices": _split_vertex_count(o),
"uv_layers": [uv.name for uv in o.data.uv_layers],
"materials": [s.material.name for s in o.material_slots if s.material],
"vertex_groups": len(o.vertex_groups),
"skinned": any(m.type == "ARMATURE" for m in o.modifiers),
} for o in meshes]
stats = {
"file": str(file_path),
"category": category or category_for_path(file_path),
"objects": len(meshes),
"mesh_nodes": [o.name for o in meshes],
"armatures": [o.name for o in armatures],
"helpers": sorted(helpers),
"bones": bones,
"bone_heads": bone_heads,
"materials": sorted({m for row in objects for m in row["materials"]}),
"images": len(bpy.data.images),
"uv_layers": uv_layers,
"meshes_without_uv": sum(1 for row in objects if not row["uv_layers"]),
"meshes_without_material": sum(1 for row in objects if not row["materials"]),
"unskinned": [row["name"] for row in objects if not row["skinned"]],
"skinned": any(row["skinned"] for row in objects),
"animations": len(bpy.data.actions),
"triangles": sum(row["triangles"] for row in objects),
"vertices": sum(row["vertices"] for row in objects),
"split_vertices": sum(row["split_vertices"] for row in objects),
"local_bounds": local,
"world_bounds": world,
"transform_delta": delta,
"transforms_applied": applied,
"detail": objects,
}
result = analyse_inspection(stats)
result.update({"stats": stats})
return result
def _to_godot(co, digits: int = 4) -> list:
"""Blender Z-up (x, y, z) -> Godot/glTF Y-up (x, z, -y) so reports match
the contract bounds, HEAD_BAND and the audit's coordinate space."""
return [round(float(co[0]), digits), round(float(co[2]), digits),
round(-float(co[1]), digits)]
def _bounds_of(objects, world: bool) -> dict:
"""Bounds in Godot coordinates (Y up, +Z front) for the given meshes."""
lo = [1e18] * 3
hi = [-1e18] * 3
found = False
for obj in objects:
matrix = obj.matrix_world if world else None
for vert in obj.data.vertices:
co = (matrix @ vert.co) if matrix is not None else vert.co
found = True
for k in range(3):
lo[k] = min(lo[k], float(co[k]))
hi[k] = max(hi[k], float(co[k]))
if not found:
return {"min": None, "max": None}
# Blender is Z-up; Godot is Y-up with +Z forward: (x, y, z) -> (x, z, -y).
return {
"min": [round(lo[0], 4), round(lo[2], 4), round(-hi[1], 4)],
"max": [round(hi[0], 4), round(hi[2], 4), round(-lo[1], 4)],
}
def _bounds_delta(a: dict, b: dict) -> float:
if not a.get("min") or not b.get("min"):
return 0.0
return round(max(abs(x - y) for pair in ((a["min"], b["min"]), (a["max"], b["max"]))
for x, y in zip(*pair)), 4)
def _triangle_count(obj) -> int:
mesh = obj.data
if hasattr(mesh, "calc_loop_triangles"):
mesh.calc_loop_triangles()
return len(mesh.loop_triangles)
return sum(len(poly.vertices) - 2 for poly in mesh.polygons)
def _split_vertex_count(obj) -> int:
"""Approximate the glTF vertex count: unique (vertex, uv, normal) per loop."""
mesh = obj.data
uv_layer = mesh.uv_layers.active
seen = set()
for poly in mesh.polygons:
for loop_index in poly.loop_indices:
loop = mesh.loops[loop_index]
uv = tuple(round(float(c), 4) for c in (uv_layer.data[loop_index].uv if uv_layer
else (0.0, 0.0)))
normal = tuple(round(float(c), 3) for c in loop.normal)
seen.add((loop.vertex_index, uv, normal))
return len(seen)
def _apply_imported_transforms(meshes, armatures) -> bool:
"""Clear parents and apply object transforms so measurements match export."""
if not meshes and not armatures:
return False
try:
bpy.ops.object.select_all(action="DESELECT")
for obj in meshes + armatures:
obj.select_set(True)
bpy.context.view_layer.objects.active = meshes[0] if meshes else armatures[0]
bpy.ops.object.parent_clear(type="CLEAR_KEEP_TRANSFORM")
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
return True
except (RuntimeError, AttributeError): # pragma: no cover - Blender only
return False
def print_inspection(result: dict) -> None:
stats = result.get("stats", {})
if result.get("error"):
print(f"INSPECT FAILED {stats.get('file', '')}\n - {result['error']}", file=sys.stderr)
return
world = stats.get("world_bounds") or {}
lo, hi = world.get("min"), world.get("max")
print(f"INSPECT {Path(stats['file']).name}")
print(f" category {result['category']}")
budget = result.get("budget", {})
print(f" triangles {stats.get('triangles', 0)}" +
(f" / {budget['max_triangles']}" if budget.get("max_triangles") else ""))
print(f" vertices {stats.get('vertices', 0)} raw, "
f"{stats.get('split_vertices', 0)} seam-split" +
(f" / {budget['max_vertices']}" if budget.get("max_vertices") else ""))
if lo and hi:
print(f" bounds (m) x=[{lo[0]:+.3f},{hi[0]:+.3f}] "
f"y=[{lo[1]:+.3f},{hi[1]:+.3f}] z=[{lo[2]:+.3f},{hi[2]:+.3f}]")
if result["category"] == "npc":
print(f" height / feet {hi[1]:.3f} m / {lo[1]:+.3f} m")
print(f" transforms {'applied' if stats.get('transforms_applied') else 'NOT applied'}"
f" (local/world delta {stats.get('transform_delta', 0.0):.3f} m)")
print(f" objects {stats.get('objects', 0)} mesh(es) "
f"[{', '.join(stats.get('mesh_nodes', [])) or 'none'}], "
f"{len(stats.get('armatures', []))} armature(s)"
+ (f", helpers ignored [{', '.join(stats['helpers'])}]" if stats.get("helpers") else ""))
print(f" bones {len(stats.get('bones', []))}"
+ (f": {', '.join(stats.get('bones', []))}" if stats.get("bones") else ""))
print(f" materials {len(stats.get('materials', []))}"
+ (f": {', '.join(stats.get('materials', []))}" if stats.get("materials") else ""))
print(f" uv layers {len(stats.get('uv_layers', []))}"
+ (f": {', '.join(stats.get('uv_layers', []))}" if stats.get("uv_layers") else ""))
print(f" skinning {'yes' if stats.get('skinned') else 'no'}"
f" (unskinned: {', '.join(stats.get('unskinned', [])) or 'none'})")
print(f" animations {stats.get('animations', 0)}")
print(f" issues {'none' if result['ok'] else str(len(result['issues']))}")
for issue in result["issues"]:
print(" - " + issue)
if result["warnings"]:
print(" warnings")
for warning in result["warnings"]:
print(" ! " + warning)
def character_fit_reference(name):
"""Measure a character's head width and hip/skirt radius from its GLB.
@@ -1990,10 +2311,32 @@ def main(argv=None):
a.add_argument("--json", action="store_true")
fit = sub.add_parser("fit")
fit.add_argument("--json", action="store_true")
insp = sub.add_parser("inspect", help="import a GLB and report its contract state")
insp.add_argument("--file", action="append", required=True,
help="GLB to inspect (repeatable; absolute path, it runs in Blender)")
insp.add_argument("--category", default="",
choices=["", "npc", "npc_gear", "props", "external"],
help="override the path-derived contract category")
insp.add_argument("--json", action="store_true")
sub.add_parser("sheet")
sub.add_parser("list")
args = ap.parse_args(raw)
if args.cmd == "inspect":
if bpy is None:
print("inspect must run inside Blender:\n flatpak run org.blender.Blender "
f"--background --python {Path(__file__).resolve()} -- inspect "
"--file <path.glb>", file=sys.stderr)
return 2
results = [inspect_glb(f, args.category or None) for f in args.file]
if args.json:
print(json.dumps(results, indent=2))
else:
for result in results:
print_inspection(result)
ok = all(r.get("ok", False) for r in results)
return 0 if ok else 1
if args.cmd in ("build", "render"):
if bpy is None:
print(f"{args.cmd} must run inside Blender:\n flatpak run org.blender.Blender "
+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" .)
+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"
+105
View File
@@ -95,5 +95,110 @@ class BuiltAssetTests(unittest.TestCase):
self.assertTrue((ba.SRC_DIR / ".gdignore").is_file())
class InspectionTests(unittest.TestCase):
"""`inspect` reports on imported GLBs; the contract logic is pure and tested
here with synthetic measurements so Blender is not a test dependency."""
def stats(self, category="npc", **overrides):
base = {
"file": "/tmp/synthetic.glb",
"category": category,
"objects": 1,
"mesh_nodes": ["npc_base"] if category == "npc" else ["prop"],
"armatures": ["Armature"] if category == "npc" else [],
"helpers": [],
"bones": list(ba.BONES) + ["Hips", "Spine", "Neck"],
"bone_heads": {},
"materials": list(ba.SLOTS),
"images": 5,
"uv_layers": ["UVMap"],
"meshes_without_uv": 0,
"meshes_without_material": 0,
"unskinned": [],
"skinned": category == "npc",
"animations": 0,
"triangles": 4500,
"vertices": 4200,
"split_vertices": 4400,
"local_bounds": {"min": [-0.3, 0.0, -0.2], "max": [0.3, 1.82, 0.2]},
"world_bounds": {"min": [-0.3, 0.0, -0.2], "max": [0.3, 1.82, 0.2]},
"transform_delta": 0.0,
"transforms_applied": True,
}
base.update(overrides)
return base
def test_clean_character_passes(self):
result = ba.analyse_inspection(self.stats())
self.assertTrue(result["ok"], result["issues"])
self.assertEqual(result["bones_missing"], [])
self.assertEqual(result["budget"]["max_triangles"], ba.CHAR_MAX_TRIS)
def test_character_contract_failures_are_listed(self):
stats = self.stats(mesh_nodes=["Armature"], skinned=False, unskinned=["Armature"],
materials=["cloth"], bones=["Head"], meshes_without_uv=1,
triangles=9000, split_vertices=9000,
world_bounds={"min": [-0.3, -0.4, -0.2], "max": [0.3, 1.2, 0.2]})
issues = "\n".join(ba.analyse_inspection(stats)["issues"])
self.assertIn("no mesh node named npc_base", issues)
self.assertIn("missing bone HipL", issues)
self.assertIn("missing material slot skin", issues)
self.assertIn("mesh not skinned", issues)
self.assertIn("mesh(es) without UVs", issues)
self.assertIn(f"triangles 9000 > {ba.CHAR_MAX_TRIS}", issues)
self.assertIn(f"glTF vertices 9000 > {ba.CHAR_MAX_VERTS}", issues)
self.assertIn("feet not on the ground", issues)
self.assertIn("unexpected height", issues)
def test_budget_gates_follow_the_category(self):
gear = ba.analyse_inspection(self.stats("npc_gear", mesh_nodes=["cap"],
triangles=ba.GEAR_MAX_TRIS + 1))
self.assertIn(f"triangles {ba.GEAR_MAX_TRIS + 1} > {ba.GEAR_MAX_TRIS}",
"\n".join(gear["issues"]))
prop = ba.analyse_inspection(self.stats("props", mesh_nodes=["bollard"],
triangles=ba.PROP_MAX_TRIS + 1))
self.assertIn(f"triangles {ba.PROP_MAX_TRIS + 1} > {ba.PROP_MAX_TRIS}",
"\n".join(prop["issues"]))
external = ba.analyse_inspection(self.stats("external", mesh_nodes=["body"],
materials=[], skinned=False,
triangles=999_999,
world_bounds={"min": [-3.0, -1.0, -3.0],
"max": [3.0, 9.0, 3.0]}))
self.assertTrue(external["ok"], external["issues"])
self.assertIsNone(external["budget"]["max_triangles"])
self.assertTrue(any("no project budget" in w for w in external["warnings"]))
def test_unapplied_transforms_are_reported_not_failed(self):
result = ba.analyse_inspection(self.stats(transform_delta=1.9))
self.assertTrue(result["ok"])
self.assertTrue(any("node transforms changed the pose" in w
for w in result["warnings"]))
def test_missing_mesh_or_bounds_is_a_failure(self):
result = ba.analyse_inspection(self.stats(mesh_nodes=[], objects=0, triangles=0,
world_bounds={"min": None, "max": None}))
self.assertFalse(result["ok"])
self.assertIn("no mesh objects", result["issues"])
self.assertIn("no triangles", result["issues"])
self.assertIn("empty world bounds", result["issues"])
def test_category_inference_from_path(self):
self.assertEqual(ba.category_for_path("assets/models/blender/npc/npc_casual.glb"), "npc")
self.assertEqual(ba.category_for_path("assets/models/blender/npc_gear/cap.glb"),
"npc_gear")
self.assertEqual(ba.category_for_path("assets/models/blender/props/bollard.glb"),
"props")
self.assertEqual(ba.category_for_path("assets/models/generated/bollard.glb"), "props")
self.assertEqual(ba.category_for_path("tmp/quaternius/Worker.glb"), "external")
def test_inspect_requires_blender(self):
# The stdlib test run has no bpy; inspect must fail cleanly, not crash.
if ba.bpy is not None: # pragma: no cover - only inside Blender
self.skipTest("running inside Blender")
result = ba.inspect_glb("does-not-exist.glb")
self.assertFalse(result["ok"])
self.assertIn("inside Blender", result["error"])
if __name__ == "__main__":
unittest.main()