Compare commits
22
Commits
v0.9.6
...
2d67e14e52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d67e14e52 | ||
|
|
2443343f2b | ||
|
|
3e41cda591 | ||
|
|
84bc4137cb | ||
|
|
5dc3ef0f4c | ||
|
|
6f3facff43 | ||
|
|
51f537720f | ||
|
|
08e9044593 | ||
|
|
ad14410337 | ||
|
|
876fb59882 | ||
|
|
979c6ad789 | ||
|
|
1921bfae91 | ||
|
|
78485259b7 | ||
|
|
fef3af44c0 | ||
|
|
bfe3c5a701 | ||
|
|
46f1e41874 | ||
|
|
f48d87c84f | ||
|
|
a9658ecd81 | ||
|
|
b8507df346 | ||
|
|
60bf9a8ef6 | ||
|
|
b51bdc8efe | ||
|
|
a7b2852253 |
No files matched your search
@@ -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.
|
||||
@@ -74,3 +78,7 @@ assets/models/blender_src/*.blend
|
||||
assets/textures/ai/game_ready/v2/*
|
||||
!assets/textures/ai/game_ready/v2/picks/
|
||||
docs/screens/realism/
|
||||
|
||||
## Local pi agent session data
|
||||
pi-session-*.html
|
||||
pi-session-*.jsonl
|
||||
@@ -19,10 +19,84 @@ 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 3D model re-evaluation, 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.
|
||||
|
||||
## Local Blender skills (development aid)
|
||||
|
||||
The MIT `blender-skills` pack is installed locally at `.agents/skills/blender-skills/`
|
||||
(94 skills plus a shared `references/` folder). It is **gitignored on purpose**: it is not
|
||||
game content, not part of the build, tests, CI or release packages, and every contributor
|
||||
may or may not have it. The agent runtime (Pi) gates project `.agents/skills` behind project
|
||||
trust, so the trust prompt must be accepted before the skills load; `/skill:blender-director`
|
||||
forces one.
|
||||
|
||||
Use them when a task touches `tools/blender_assets.py`, `tools/npc_pipeline.py`,
|
||||
`tools/bake_ao.py`, `assets/models/blender/**`, or Blender-authored venue/prop geometry.
|
||||
|
||||
### Routing
|
||||
|
||||
Start with `blender-director` for anything multi-discipline, then load only the
|
||||
specialists the task needs:
|
||||
|
||||
| Work | Skills |
|
||||
| --- | --- |
|
||||
| Plan/route a Blender task, reference photo match | `blender-director` |
|
||||
| Character mesh, face, hands, proportions | `character-artist`, `character-archetypes`, `retopology` |
|
||||
| Skeleton, weights, pose/contract rigging | `rigging` |
|
||||
| UV layout, texel density, UDIM | `uv-workflow` |
|
||||
| Bakery, atlas, normal/AO/ORM maps | `texture-workflow` |
|
||||
| PBR materials and shading | `materials` |
|
||||
| Screenshot-driven material/light iteration | `lookdev` |
|
||||
| Triangle/vertex budgets, naming, scene hygiene | `asset-optimization` |
|
||||
| LOD tiers and impostors | `lod-pipeline` |
|
||||
| Collision shapes and proxies | `collision-proxy` |
|
||||
| GLB export for Godot | `godot-export`, `export-pipeline` |
|
||||
| Gear, props, street furniture | `prop-artist`, `hard-surface` |
|
||||
| Trees, hedges, foliage cards | `vegetation-artist` |
|
||||
| Facades, venues, modular kits, dressing | `environment-artist`, `archviz`, `set-dressing`, `geometry-nodes` |
|
||||
| Lighting, framing, final render/composite | `lighting`, `camera-cinematography`, `rendering`, `compositing` |
|
||||
| Style direction (stylized / low-poly / realistic) | `stylized-style`, `lowpoly-style`, `realistic-style` |
|
||||
| Ship/no-ship review | `qa-review` |
|
||||
|
||||
The same skills cover the local map track (procedural geometry, facade kits, OSM massing,
|
||||
splat reference renders) and the NPC roster work.
|
||||
|
||||
### Project rules that override a skill
|
||||
|
||||
1. **The project contract wins.** `npc_base` mesh node, the 13 contract bones at their rest
|
||||
positions, five tint slots, front on Blender −Y / Godot +Z, grounded feet, 8,000
|
||||
triangles / 8,000 glTF vertices per character, 1,500 per gear, 4,000 per prop. A skill
|
||||
may recommend another topology, naming or budget scheme; do not adopt it unless the
|
||||
change is explicitly planned and the contract, tests and docs are updated together.
|
||||
2. **There is no Blender MCP here.** The skills assume MCP tools; this project runs Blender
|
||||
as headless flatpak scripts (`tools/blender_assets.py`, `tools/npc_pipeline.py`). Translate
|
||||
MCP steps into script changes or CLI runs. Never add an MCP dependency to the build, CI or
|
||||
runtime.
|
||||
3. **CI stays offline and skill-free.** Skills are local guidance only; every gate must still
|
||||
work from a fresh checkout with committed `.glb`/`.png`/`.tres` files and Godot.
|
||||
4. **No new asset source without the licence gate.** Skills may suggest reference packs or
|
||||
generated meshes; ADR-0015 (CC0, or CC-BY with attribution in `CREDITS.md`) and the
|
||||
provenance rules in `docs/npc-model-sourcing.md` still apply.
|
||||
5. **Evidence, not vibes.** After a skill-informed change, run `blender_assets.py inspect`,
|
||||
`audit` and `fit` plus the relevant capture/smoke gates, and say in the handoff which
|
||||
skills informed the change and what the measurements were.
|
||||
|
||||
## Required Workflow
|
||||
|
||||
### 1. Start by Inspecting the Repository
|
||||
@@ -47,6 +121,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 +164,13 @@ Recommended format:
|
||||
- [ ] Document the test procedure in `README.md`.
|
||||
```
|
||||
|
||||
Recommended `DONE.md` format: a `# DONE` heading, followed by each moved level-2 section
|
||||
unchanged, with a completion date line under each section heading.
|
||||
|
||||
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:
|
||||
@@ -174,10 +258,12 @@ Update `README.md` whenever setup instructions, commands, project structure, or
|
||||
The root-level `CHANGELOG.md` records the user-facing changes for every release. It exists so players and contributors can see what changed between versions without reading the commit log.
|
||||
|
||||
- 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.
|
||||
- Follow Semantic Versioning (`major.minor.patch`): bump major only for changes that remove or break an existing player-facing feature, save format, or config contract, or that replace a system listed as a core pillar in PLAN.md; bump minor for new user-facing features; bump 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.
|
||||
- Add the entry when its work ships. If the version number is not yet known, record it under `Unreleased` as a staging entry; moving it into a numbered version is a version increment and is complete only when the `config/version` update and matching local release build have also been produced and verified (see "Commit, Build and Version Discipline").
|
||||
|
||||
When a full executable level-2 milestone/theme in `TODO.md` is implemented, completed, and moved to `DONE.md`:
|
||||
|
||||
@@ -198,13 +284,41 @@ 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. If the change is a release (a major, minor, or patch version increment), follow the Release
|
||||
Procedure in "Commit, Build and Version Discipline".
|
||||
10. Push committed changes to the shared repository if applicable.
|
||||
11. 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.** Every major, minor and patch increment follows the Release Procedure
|
||||
below; 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.
|
||||
|
||||
**Release Procedure** — follow these steps in order for every major, minor, or patch version
|
||||
increment:
|
||||
|
||||
1. Update `config/version`.
|
||||
2. Write the `CHANGELOG.md` entry for the version.
|
||||
3. Run the local release build (`tools/build_releases.sh`).
|
||||
4. Verify the build artifacts and checksums.
|
||||
5. Commit the changes.
|
||||
6. Tag the version.
|
||||
7. Push the commit and tag.
|
||||
|
||||
## Collaboration Rules
|
||||
|
||||
- Prefer clear, reversible changes over broad rewrites.
|
||||
@@ -213,6 +327,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.
|
||||
- Follow the Release Procedure in "Commit, Build and Version Discipline" 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.
|
||||
@@ -224,6 +340,10 @@ Before handing off work, an agent must run every check below that applies to the
|
||||
- Run automated tests.
|
||||
- Run formatting, linting, or type checks.
|
||||
- Start the game or application and perform a basic smoke test.
|
||||
- For 3D/Blender changes: `blender_assets.py inspect`, `audit` and `fit`, the relevant capture
|
||||
gate, and a note of which local skills informed the change.
|
||||
- If the local Blender skills are not installed, state "no local skills used" in the handoff
|
||||
and list the skill-free checks that were run instead.
|
||||
- Check that documentation and task status reflect the actual state.
|
||||
- Review the final diff for accidental changes or unfinished markers.
|
||||
|
||||
|
||||
+16
-6
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -4,6 +4,86 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
|
||||
## Decisions
|
||||
|
||||
- **3D review baseline (2026-11-03):** `docs/3d-model-review.md` records the first skill-guided
|
||||
measurements: 46/46 assets clean, library 59,012 tris, characters 4,482–5,474 tris (68–69% of
|
||||
the 8,000 caps), `npc_base` only 1,204/2,488, 24-NPC 6.94 ms, and authored texel density
|
||||
64–122 px/m (fabric effective 287–440 after the 4× repeat). That is below the skill's 128–256
|
||||
background tier but the slot maps are neutral and tinted at runtime, so it is a recorded decision
|
||||
point, not a defect. Added `blender_assets.py texels` (stdlib, per-slot px/m from GLB UV/world
|
||||
area, fabric reported twice) and `inspect --json --out` (the glTF importer's INFO log is captured
|
||||
in `stats.import_log`; Blender's startup banner still pollutes stdout). Baseline/review captures
|
||||
run fine under `xvfb-run` into `tmp/`.
|
||||
- **3D model re-evaluation first (2026-11-03):** `TODO.md` is re-planned around a new unversioned
|
||||
`3D model re-evaluation` track, ahead of character and local-map work, because the crowd-variant
|
||||
roster would otherwise multiply whatever the existing models get wrong. It scores all 8
|
||||
characters, 18 gear, 20+6 props and the three venues in `docs/3d-model-review.md`, fixes ranked
|
||||
P0/P1 only, and must not change the `npc_base` contract, renderer or 0.10.0 candidate. The MIT
|
||||
Blender skill pack is now routed through `AGENTS.md` §Local Blender skills; skill advice loses
|
||||
to the project contract, and there is no Blender MCP here (headless flatpak scripts instead).
|
||||
MEMORY is ~960 lines with dated historical entries; trimming it is a tracked maintenance task.
|
||||
- **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 +168,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 +255,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 +286,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:
|
||||
@@ -272,11 +373,11 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
**Mockup copies:** 21 design references live in `assets/images/ui/`; `docs/screens/designs/`
|
||||
remains the reference-only copy. `tools/screen_designs.py` reads/writes the runtime root.
|
||||
|
||||
## Handoff (read this first)
|
||||
## Handoff — historical snapshot (2026-11-01; superseded by Current State / Plan vs code)
|
||||
|
||||
- **Status (2026-11-01, after the venue-geometry pass):** v0.7.0 is released. Post-v0.6.0 the
|
||||
- **Status (2026-11-01, after the venue-geometry pass):** v0.7.0 was released at that point. Post-v0.6.0 the
|
||||
realism pass, the visual-alignment pass and the NPC face/hand pass all landed. Headless
|
||||
**229/229**, Python tooling **87/87**, `tools/check_screenshots.sh` **11 good / 0 bad**,
|
||||
**272/272**, Python tooling **108/108**, `tools/check_screenshots.sh` **21 good / 0 bad**,
|
||||
`screen_designs.py compare` 11 good / 0 bad, `scene_concepts.py audit` all GOOD,
|
||||
`blender_assets.py audit` exit 0, `smoke_npc_budget` OK (16 avatars, 6.94 ms of 50 ms).
|
||||
`PLAN.md` is compact; README/architecture track the current controls, asset pipeline, CI and
|
||||
@@ -830,13 +931,14 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
- Shift length: `GameManager` uses the scenario's `shift_duration_s` (900s ≈ 10-15 min);
|
||||
tests and the screenshot tool override elapsed time directly.
|
||||
- Headless (dummy renderer) logs "Parameter m is null" from `mesh_get_surface_count`; it does not occur with a real GL driver and does not affect logic.
|
||||
- Steps are visual-only (no collider) so NPCs can path to the entrance; the guard spawns on the landing so it never needs to climb.
|
||||
- Steps have per-tread colliders (`_build_steps()` adds a `StaticBody3D` with one `BoxShape3D`
|
||||
per tread), so the guard and NPCs walk up the entrance; the guard spawns on the landing.
|
||||
- Persistence tests write into the real `user://shift_results.json` (harmless local pollution of the demo history).
|
||||
- macOS export required `rendering/textures/vram_compression/import_etc2_astc=true` in `project.godot` (universal build demands ETC2 ASTC enabled).
|
||||
- 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 **108/108**; 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 +961,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 +971,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.
|
||||
@@ -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.
|
||||
- **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.
|
||||
- **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 tight flashlight spot (no beam geometry), 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
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -3,13 +3,400 @@
|
||||
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:** 3D model re-evaluation → character development → local map development →
|
||||
versioned release milestones (0.10.0) → future/deferred work. See `AGENTS.md` §Priority and
|
||||
Versioning and §Local Blender skills.
|
||||
|
||||
The MIT Blender skill pack lives locally at `.agents/skills/blender-skills/` (gitignored). Every
|
||||
3D workstream below names the skills to load; they are guidance only, and the project contract,
|
||||
`blender_assets.py audit`/`fit`/`inspect` and the capture gates stay authoritative.
|
||||
|
||||
## 3D model re-evaluation
|
||||
|
||||
Goal: re-judge every shipped 3D asset against the newly available Blender skill guidance, rank
|
||||
the real gaps, and fix only what the rankings justify. This is an unversioned development track:
|
||||
it must not change the `npc_base` contract, the renderer or the 0.10.0 release candidate.
|
||||
|
||||
Scope: 8 library characters (`npc_casual`, `npc_jacket`, `npc_coat`, `npc_vest`, `npc_dress`,
|
||||
`npc_hoodie`, `npc_firefighter`, `npc_guard`), the frozen `npc_base` fallback, 18 gear pieces,
|
||||
20 library props, 6 generated props, and the three venue families.
|
||||
|
||||
**Exit:** `docs/3d-model-review.md` scores every catalogue asset; every P0/P1 is fixed or
|
||||
explicitly accepted with a measurement; `inspect`/`audit`/`fit`/proportions, the 24-NPC budget
|
||||
and the capture gates are green; no runtime contract, dependency or release artifact changed
|
||||
except the rebuilt assets themselves.
|
||||
|
||||
### A. Baseline, scorecard and routing
|
||||
|
||||
- [x] Load `blender-director` plus the per-family specialists before touching geometry, and
|
||||
record the skill stack and the advice that changed a decision in `docs/3d-model-review.md`.
|
||||
(Routing, QA severity/verdict, texel-density tiers, character loop criteria and budget rules
|
||||
recorded in the review doc; MCP-only steps translated to headless flatpak runs.)
|
||||
- [x] Re-run the neutral NPC baseline (48 frames), the final-context review (96 frames), the
|
||||
prop gallery and the 24-NPC budget on the current build; keep raw frames in `tmp/` and do
|
||||
not replace committed sheets until a fix lands. (48/48 and 96/96 frames captured under
|
||||
`xvfb-run`, none blank; raw sets in `tmp/model-baseline` and `tmp/model-review`; committed
|
||||
sheets untouched. 24-NPC smoke still 6.94 ms / 168 meshes / 131,616 triangles.)
|
||||
- [x] Create `docs/3d-model-review.md` with a per-asset scorecard: silhouette, anatomy/proportion,
|
||||
topology/edge flow, UV/texel density, textures/materials, rig/weights, LOD/impostor,
|
||||
collision/grounding, export contract, provenance, skill route, verdict (P0/P1/P2/accept).
|
||||
(Doc created with character/gear/prop/venue tables, verdict columns and a ranked candidate
|
||||
list; unmeasured cells are marked `—` instead of being guessed.)
|
||||
- [x] Collect the baseline measurements in one table: `blender_assets.py inspect`, `audit`, `fit`,
|
||||
`npc_proportions.py`, texel density, triangles/meshes, 24-NPC frame time. (All seven captured;
|
||||
`texels` and `inspect --json --out` were added to make the measurements repeatable. All 46
|
||||
assets audit/inspect clean; texel density 64–122 px/m authored, 287–440 effective fabric.)
|
||||
- [ ] Review the scorecard with the project owner, rank the P0/P1 gaps and decide fix vs accept
|
||||
per item; record the decision and the reason in the review document.
|
||||
|
||||
### B. Character re-evaluation (8 variants + fallback)
|
||||
|
||||
*Skill route: `character-artist`, `retopology`, `rigging`, `uv-workflow`, `texture-workflow`,
|
||||
`materials`, `hair-groom`, `asset-optimization`, `lookdev`, `qa-review`.*
|
||||
|
||||
- [ ] Face/head: review eye/mouth loops, cheek/brow/chin transitions and faceting at the 2 m
|
||||
portrait distance against `character-artist` and `retopology`; write the concrete edits.
|
||||
- [ ] Hands/grip: re-check palm, finger segments, thumb and gear contact; decide between a
|
||||
finger-curl rig or the current open hand plus `HAND_ITEM_FORWARD`, and record the limit.
|
||||
- [ ] Weights/pose: capture idle, walk, sit and wave; inspect shoulder, hip and neck collapse;
|
||||
fix the worst offenders using `rigging` practice without changing the 13-bone contract.
|
||||
- [ ] UV/texel density: measure the 256² slot maps; decide whether atlas, UDIM or a resolution
|
||||
change is justified. Do not change `CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` without re-running the
|
||||
budget smoke and the Web/Compatibility path.
|
||||
- [ ] Cloth and hair: re-check coat/dress hem clipping and the hair/beard/headgear meshes against
|
||||
`hair-groom` guidance; decide explicitly whether cloth simulation stays out of scope.
|
||||
- [ ] Fix only the ranked P0/P1s, one asset family per commit; refresh the comparison sheet and
|
||||
re-run `inspect`, `audit`, `fit` and `npc_proportions.py` after each fix.
|
||||
- [ ] `qa-review` sign-off: a ship/no-ship verdict per variant with accepted limits recorded.
|
||||
|
||||
### C. Gear and prop re-evaluation
|
||||
|
||||
*Skill route: `prop-artist`, `hard-surface`, `vegetation-artist`, `materials`, `collision-proxy`,
|
||||
`lod-pipeline`, `qa-review`.*
|
||||
|
||||
- [ ] Gear: review the 18 pieces for silhouette, straps/buckles, helmet and lanyard readability;
|
||||
verify attach space and body clearance with `fit`; rebuild only what fails the scorecard.
|
||||
- [ ] Props: review the 20 library props plus the 6 generated props for bevels, chamfers,
|
||||
silhouette, ground contact and shared material language (`prop-artist`, `hard-surface`);
|
||||
list the rebuilds that are actually worth the triangle budget.
|
||||
- [ ] Foliage: replace the flat-shaded `_shrub()`/`_tree()` clumps with `vegetation-artist`-style
|
||||
carded clusters; measure triangles and keep the 60k `LIBRARY_TRIANGLE_BUDGET`.
|
||||
- [ ] Materials and emissives: `lookdev` pass on bulbs, signs, lanterns, the marquee and the
|
||||
sedan at day and late; confirm the flashlight stays a tight spot with no beam geometry.
|
||||
- [ ] Collision: verify every prop collider and navigation obstacle matches its silhouette and
|
||||
add the missing `collision-proxy` shapes where walking is blocked.
|
||||
- [ ] Exit: a prop/gear capture sheet, budgets and collision checks pass with no unreviewed asset.
|
||||
|
||||
### D. Venue and camera re-evaluation
|
||||
|
||||
*Skill route: `environment-artist`, `archviz`, `set-dressing`, `lighting`, `camera-cinematography`,
|
||||
`lookdev`, `rendering`, `qa-review`.*
|
||||
|
||||
- [ ] Facade/venue review: rank the top five shape-language fixes for Hollywood, the Joke Shop and
|
||||
the TV Studio (`environment-artist`, `archviz`) and check them against the map-budget tests.
|
||||
- [ ] Lighting: `lighting`/`lookdev` pass on sconces, canopy, marquee and street lamps with
|
||||
day/late captures; verify local attenuation stays at 1.0 and no constant-falloff light returns.
|
||||
- [ ] Set dressing: review entrance and sidewalk prop clusters (`set-dressing`) for occlusion,
|
||||
navigation obstacles and readable storytelling clusters.
|
||||
- [ ] Camera height: decide 1.7 m gameplay framing vs the ~1.55 m concept frames using
|
||||
`camera-cinematography`, and record the decision instead of leaving the mismatch open.
|
||||
- [ ] Hooded/winter portrait: close the deferred close-up by staging a types 5–7 capture
|
||||
(`--portrait-type`) or adding a scenario spawn that provides one.
|
||||
|
||||
### E. Gates and documentation
|
||||
|
||||
- [ ] Add the measurable checks the review needs (texel density, weight/pose sanity, collider
|
||||
coverage, LOD/impostor check) as stdlib or GDScript gates where possible; CI stays offline.
|
||||
- [ ] Add a coverage test so every catalogue asset has a scorecard row and verdict.
|
||||
- [ ] Update `docs/blender-assets.md`, `docs/npc-visual-review.md`, `docs/3d-model-review.md`,
|
||||
`docs/architecture.md` and `AGENTS.md` with the skill routes and the new gates.
|
||||
- [ ] Record accepted limits and deferred rebuilds in `docs/known-limitations.md` and `MEMORY.md`.
|
||||
|
||||
## 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.
|
||||
|
||||
The roster build is deliberately sequenced **after** the 3D model re-evaluation: the new variants
|
||||
must inherit the reviewed face/hand/gear baseline instead of multiplying whatever it finds.
|
||||
|
||||
*Skill route: `character-artist`, `retopology`, `rigging`, `uv-workflow`, `materials`,
|
||||
`asset-optimization`, `lowpoly-style`, `godot-export`, `qa-review`.*
|
||||
|
||||
### 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; all 46 library GLBs inspect clean. `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. Use `retopology`/`rigging`/`uv-workflow` guidance for the manual steps, but keep
|
||||
the Quaternius no-go: a contract rebuild needs a proportion warp, so this is not on the
|
||||
character 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.)
|
||||
- [ ] Re-derive the four approved specs against the re-evaluated character baseline (face, hands,
|
||||
proportions, gear offsets) so the new variants do not reintroduce the gaps the review found.
|
||||
- [ ] Add the four approved specs to `CHARACTERS`, build and commit their GLBs/AO maps, and
|
||||
update the manifest with roles and tint data. Build only the new assets, never a no-op full
|
||||
rebuild, and remember that `export_filter="all_resources"` packs committed GLBs into the
|
||||
next release build: keep this off `main` until after the 0.10.0 tag or accept the size.
|
||||
- [ ] Replace the fixed `character_model_for()` mapping with deterministic per-role pools and add
|
||||
tests for repeatability and duplicate avoidance (`godot-export` contract check included).
|
||||
- [ ] Rebuild and verify gear fit for every new variant (`blender_assets.py fit` plus the runtime
|
||||
gear checks) and re-run `inspect` on the four new GLBs.
|
||||
|
||||
### 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.
|
||||
|
||||
*Skill route: `environment-artist`, `archviz`, `hard-surface`, `geometry-nodes`,
|
||||
`procedural-modeling`, `vegetation-artist`, `set-dressing`, `lighting`, `materials`,
|
||||
`texture-workflow`, `lookdev`, `scene-assembly`, `qa-review`.*
|
||||
|
||||
### 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 (`texture-workflow`,
|
||||
`materials`, `lookdev`).
|
||||
|
||||
### 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 (`scene-assembly`,
|
||||
`asset-optimization`, `lod-pipeline`).
|
||||
- [ ] Build a reusable photoreal facade kit (window bays, cornices, storefronts, awnings, signage)
|
||||
with `environment-artist`/`hard-surface`/`geometry-nodes` 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 (`scene-assembly`).
|
||||
- [ ] Judge quality from held-out views and a render from the game's day/night camera poses
|
||||
(`camera-cinematography`, `lookdev`, `qa-review`), 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 (`texture-workflow`),
|
||||
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 (`retopology`, `prop-artist`), assign LODs (`lod-pipeline`), and keep it
|
||||
inside the venue triangle/material budgets with mesh-based colliders (`collision-proxy`).
|
||||
- [ ] **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.
|
||||
|
||||
## Maintenance and documentation hygiene
|
||||
|
||||
Unversioned housekeeping that keeps the planning sources trustworthy. Do these when they block a
|
||||
task, not on a schedule.
|
||||
|
||||
### Memory and status accuracy
|
||||
|
||||
- [ ] Trim `MEMORY.md` from ~970 lines to the durable set: drop the pre-0.8 host/disk/NSIS/release
|
||||
anecdotes, superseded v0.2.x handoff status and duplicated release notes, keep the gotchas
|
||||
that still bite (async/event wiring, import order, colliders, RNG, contracts), and keep the
|
||||
easter-egg note.
|
||||
- [ ] Re-check the remaining memory claims against code before deleting them; `MAX_LIVE_NPCS` is
|
||||
still 16 for gameplay while the separate budget smoke spawns 24, so do not "fix" that.
|
||||
- [ ] Fix stale status claims as they are found (the step-collider and flashlight-beam claims were
|
||||
corrected on 2026-11-03); add a documentation check or grep note only if the same class of
|
||||
drift returns.
|
||||
|
||||
### Blender skills upkeep
|
||||
|
||||
- [ ] Re-check the installed Blender pack (`version.json`) when updating it and record the version
|
||||
in `AGENTS.md` if it changes; keep `.agents/` gitignored and `.agents/.gdignore` in place.
|
||||
- [ ] Prune the skill routes in `AGENTS.md` if a skill stops matching the project (for example if
|
||||
a future second-rig path makes `rigging`/`animation` guidance authoritative).
|
||||
|
||||
## 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 + 108 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 +419,9 @@ release-candidate foundation, not yet a 0.10.0 release candidate.
|
||||
|
||||
| Area | Current baseline | Remaining gap | Risk | Milestone |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 3D model re-evaluation | 8 characters, 18 gear, 20+6 props audited against budgets/proportions; no skill-guided scorecard yet | Scorecard, ranked P0/P1 fixes, texel/weight/collider measurements and accept decisions | Unmeasured drift stays in the shipped library | Unversioned |
|
||||
| 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,88 +432,36 @@ 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.
|
||||
**Current priority:** the unversioned **3D model re-evaluation** runs first because its findings
|
||||
gate the crowd-variant roster; **Character development** and **Local map development** follow.
|
||||
The **0.10.0 release candidate** is 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`.
|
||||
|
||||
## 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
|
||||
## 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.
|
||||
verify every distribution path. This remains the next versioned release after the 0.9.x patch
|
||||
line, but follows the unversioned 3D re-evaluation, character and local-map tracks in planning
|
||||
order. Only a release-blocking defect from those tracks may enter this milestone before the tag.
|
||||
|
||||
### Content lock and external playtest
|
||||
|
||||
- [ ] Declare a content/feature freeze for 0.10.0; only release-blocking fixes and balance changes
|
||||
may land after the freeze.
|
||||
- [ ] Produce a release-candidate build with the tutorial, campaign, all three venues, Help,
|
||||
accessibility settings and checkpoint/resume enabled.
|
||||
- [x] Declare a content/feature freeze for 0.10.0; only release-blocking fixes and balance changes
|
||||
may land after the freeze. (Frozen 2026-11-03 after the 0.9.6 integration
|
||||
checkpoint; this pass is limited to release reliability, accessibility,
|
||||
balance evidence and packaging.)
|
||||
- [x] Produce a release-candidate build with the tutorial, campaign, all three venues, Help,
|
||||
accessibility settings and checkpoint/resume enabled. (Clean worktree
|
||||
`a9658ec` built `0.10.0-rc.1`: Linux 161.5 MB, Windows 197.2 MB, macOS
|
||||
148.0 MB, Web 97.8 MB and HQ 241.6 MB. The Web/HQ archives include the
|
||||
credits, privacy, accessibility and known-limitations pages; SHA-256
|
||||
manifests were written; the Linux and HQ binaries launched headless and the
|
||||
Web index/wasm/pck/docs returned HTTP 200. Not published because external
|
||||
playtest and legal approval remain open.)
|
||||
- [ ] Run at least five first-time-player sessions covering the tutorial and one full venue shift;
|
||||
record where players get stuck, misunderstand evidence or choose disproportionate escalation.
|
||||
- [ ] Triage findings by severity (P0/P1/P2) and fix every P0/P1 onboarding/accessibility issue;
|
||||
@@ -133,12 +471,17 @@ verify every distribution path. This is the next minor release after the 0.9.x p
|
||||
|
||||
### Balance and fairness pass
|
||||
|
||||
- [ ] Define measurable targets for routine compliance, access-decision accuracy, peaceful
|
||||
- [x] Define measurable targets for routine compliance, access-decision accuracy, peaceful
|
||||
resolution, justified emergency calls and false-positive/false-negative rates.
|
||||
(`docs/balance-acceptance.md` defines cohorts, denominators, 36 matrix cells,
|
||||
seeds, thresholds and matched emergency-first comparisons. Documentation verified;
|
||||
targets are not measured results.)
|
||||
- [ ] Run scripted venue sweeps across easy/normal/hard and all four phases; compare outcomes and
|
||||
tune event weights, reaction tables, compliance and report strictness where targets fail.
|
||||
- [ ] Review emergency calls against actual danger: no routine behavior should make emergency the
|
||||
dominant winning move, and genuine threats must remain answerable.
|
||||
dominant winning move, and genuine threats must remain answerable. Include the
|
||||
type-derived `_read_npc_truth()` mismatch with observed threats/forced entry;
|
||||
use independently labelled behavior evidence, not the helper as its own oracle.
|
||||
- [ ] Verify the reputation grades, debrief explanations and campaign progression still communicate
|
||||
the de-escalation-first design after tuning.
|
||||
- [ ] Record the balance evidence and any deliberate exceptions in `docs/development-status.md`
|
||||
@@ -146,39 +489,80 @@ verify every distribution path. This is the next minor release after the 0.9.x p
|
||||
|
||||
### Save migration and session reliability
|
||||
|
||||
- [ ] Add fixtures for a 0.8-era settings/campaign/history save and a 0.9.0 checkpoint; verify
|
||||
migration/default-filling and a clean first launch with no files.
|
||||
- [ ] Test fresh install → tutorial → campaign → pause → quit → resume → review → next venue on
|
||||
an isolated user-data directory.
|
||||
- [ ] Test corrupt JSON, partial temp files, missing optional fields and a failed atomic rename;
|
||||
confirm fallback behavior never destroys a valid save.
|
||||
- [ ] Verify checkpoint resume after all three venues, after an incident, with NPCs at the door and
|
||||
with flashlight/body-camera/door state set.
|
||||
- [ ] Document save locations, schema versions, reset behavior and rollback/export instructions in
|
||||
`docs/getting-started.md` and the release notes.
|
||||
- [x] Add fixtures for a 0.8-era settings/campaign/history save and a 0.9.0 checkpoint; verify
|
||||
migration/default-filling and a clean first launch with no files. (Committed
|
||||
fixtures under `src/test/fixtures/saves/`; 9 migration/failure tests cover
|
||||
old settings/campaign/history, the 0.9.0 checkpoint, all three venue
|
||||
checkpoint round-trips, clean install, corrupt JSON and a blocked atomic
|
||||
replace. Suite is now 272 tests.)
|
||||
- [x] Test fresh install → tutorial → campaign → pause → quit → resume → review → next venue on
|
||||
an isolated user-data directory. (Tutorial and campaign smokes shared one
|
||||
isolated `XDG_DATA_HOME`; tutorial completion/replay/skip then campaign
|
||||
resume/review-continue/help both passed. Pause/menu behavior is covered by
|
||||
`smoke_session_shell.gd`.)
|
||||
- [x] Test corrupt JSON, partial temp files, missing optional fields and a failed atomic rename;
|
||||
confirm fallback behavior never destroys a valid save. (Corrupt/partial files
|
||||
fall back to defaults; a blocked temp path reports an error and leaves the
|
||||
prior valid settings intact. `_load_json` now uses the non-logging JSON
|
||||
instance parser.)
|
||||
- [x] Verify checkpoint resume after all three venues, after an incident, with NPCs at the door and
|
||||
with flashlight/body-camera/door state set. (The migration suite round-trips a
|
||||
checkpoint for Hollywood, the Joke Shop and the TV Studio with an open
|
||||
incident, one NPC row, flashlight/body-camera/door flags; the campaign smoke
|
||||
resumes a live checkpoint through the real `GameManager`.)
|
||||
- [x] Document save locations, schema versions, reset behavior and rollback/export instructions in
|
||||
`docs/getting-started.md` and the release notes. (`getting-started.md` now
|
||||
documents the four files, version-1 checkpoint, atomic write/fallback,
|
||||
clean install, reset and backup/rollback.)
|
||||
|
||||
### Release engineering and distribution
|
||||
|
||||
- [ ] Run `tools/build_releases.sh` from a clean checkout and verify Linux/Windows/macOS/Web/HQ
|
||||
artifact names, sizes and checksums.
|
||||
- [x] Run `tools/build_releases.sh` from a clean checkout and verify Linux/Windows/macOS/Web/HQ
|
||||
artifact names, sizes and checksums. (Clean worktree `a9658ec` built the
|
||||
five artifacts listed above; `PUBLISH_DRY_RUN=1` produced and printed
|
||||
`build/SHA256SUMS-0.10.0-rc.1.txt` without touching the API.)
|
||||
- [ ] Smoke-test fresh Linux portable/AppImage, Windows portable/installer, macOS unzip/launch,
|
||||
Web static hosting and Desktop HQ Vulkan fallback on the target matrix.
|
||||
- [ ] Add a disk-space preflight and retention policy to the release/publish scripts so a full
|
||||
Gitea host cannot leave a partially uploaded release.
|
||||
- [ ] Verify release metadata, tag/version consistency, asset counts and download links; retain
|
||||
(Linux portable and Desktop HQ launched headless; Web files served over a
|
||||
local static server with HTTP 200. AppImage/installer were not built locally
|
||||
and Windows/macOS launch testing still needs the target matrix.)
|
||||
- [x] Add a disk-space preflight and retention policy to the release/publish scripts so a full
|
||||
Gitea host cannot leave a partially uploaded release. (`publish_release.sh`
|
||||
checks artifact size/free space, writes SHA-256 checksums, supports
|
||||
`PUBLISH_DRY_RUN`, rolls back uploaded assets/new releases on failure, and
|
||||
offers opt-in `RETAIN_RELEASES` pruning; dry-run verified.)
|
||||
- [x] Verify release metadata, tag/version consistency, asset counts and download links; retain
|
||||
at least the latest 0.8/0.9 releases and document older-asset pruning.
|
||||
- [ ] Decide and document whether macOS signing/notarization is required for 0.10.0 or explicitly
|
||||
(Source/version and tag are both 0.9.6; all seven published releases carry
|
||||
5–7 assets, including the retained v0.8.0/v0.8.1/v0.9.0/v0.9.1 line.
|
||||
`publish_release.sh` documents opt-in `RETAIN_RELEASES` pruning and
|
||||
`RELEASING.md` describes the seven-release policy.)
|
||||
- [x] Decide and document whether macOS signing/notarization is required for 0.10.0 or explicitly
|
||||
remains a future release blocker; record the quarantine instructions either way.
|
||||
(0.10.0 ships unsigned with quarantine instructions; signing/notarization
|
||||
remains an explicit public-release blocker in `RELEASING.md` and
|
||||
`known-limitations.md`.)
|
||||
|
||||
### Legal, credits and platform policy
|
||||
|
||||
- [ ] Audit AI-assisted asset provenance and licenses; record source/model usage and any
|
||||
- [x] Audit AI-assisted asset provenance and licenses; record source/model usage and any
|
||||
redistribution constraints for textures, characters, props and UI art.
|
||||
- [ ] Audit audio/SFX/music licensing (or confirm everything is procedural) and add a credits file.
|
||||
- [ ] Review body-camera/privacy language, mental-health-crisis representation, age/content rating
|
||||
(`CREDITS.md` records the Blender/OpenRouter toolchain, default model slugs,
|
||||
missing per-file generation metadata and the commercial-rights review as an
|
||||
explicit release blocker.)
|
||||
- [x] Audit audio/SFX/music licensing (or confirm everything is procedural) and add a credits file.
|
||||
(All runtime audio is procedural; no external music/SFX files are bundled.
|
||||
`CREDITS.md` records this.)
|
||||
- [x] Review body-camera/privacy language, mental-health-crisis representation, age/content rating
|
||||
and storefront requirements; fix any release-blocking copy or content.
|
||||
- [ ] Add the player-facing credits, known limitations, accessibility statement and privacy note to
|
||||
the release package/README.
|
||||
(`PRIVACY.md` clarifies the simulated body camera/local-only data; the
|
||||
accessibility and known-limitations pages record the remaining
|
||||
mental-health/rating/storefront review as a release blocker rather than
|
||||
claiming approval.)
|
||||
- [x] Add the player-facing credits, known limitations, accessibility statement and privacy note to
|
||||
the release package/README. (`README.md` links `CREDITS.md`, `PRIVACY.md`,
|
||||
`docs/accessibility.md` and `docs/known-limitations.md`; the Web and Desktop
|
||||
HQ package builders copy those documents into the shipped archives.)
|
||||
- [ ] Mark external legal/storefront approval as complete or explicitly block the 0.10.0 release on it.
|
||||
|
||||
### Documentation and visual sign-off
|
||||
@@ -189,7 +573,11 @@ verify every distribution path. This is the next minor release after the 0.9.x p
|
||||
`check_screenshots.sh` and the pinned vision judge, recording model/spend/verdicts.
|
||||
- [ ] Run the manual smoke checklist on every target and attach/link the release-candidate evidence.
|
||||
- [ ] Verify the tutorial/campaign/help text at maximum font scale and with the longest strings.
|
||||
- [ ] Remove or archive stale plans/notes and confirm no contradictory version/status claims remain.
|
||||
- [x] Remove or archive stale plans/notes and confirm no contradictory version/status claims remain.
|
||||
- [x] Document the character development toolchain in `docs/architecture.md`: base vs library
|
||||
sources, tool/artifact/gate table, runtime attach/animation contracts and rebuild discipline.
|
||||
(Added §5 "Character development tooling" plus risk and ownership-map updates;
|
||||
documentation check passed.)
|
||||
|
||||
### Release-candidate QA and exit
|
||||
|
||||
@@ -204,14 +592,6 @@ verify every distribution path. This is the next minor release after the 0.9.x p
|
||||
- [ ] Exit: the release candidate is content-locked, externally played, legally reviewed,
|
||||
documented, reproducibly built, and approved as the 0.10.0 release candidate.
|
||||
|
||||
## Future 0.x / non-blocking candidates
|
||||
|
||||
- [ ] Live 3D body-camera replay.
|
||||
- [ ] Additional venues or seasonal events.
|
||||
- [ ] Real voice/dialogue audio.
|
||||
- [ ] Image-to-mesh prop pipeline.
|
||||
- [ ] Rare easter eggs/cameos (e.g. Bob Dobbs) only after core content and QA are stable.
|
||||
|
||||
## Roadmap dependencies and risks
|
||||
|
||||
- Content volume is the largest schedule risk; tutorial and campaign work must not outrun the
|
||||
@@ -220,26 +600,26 @@ 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.
|
||||
|
||||
## Realism follow-ups (deferred)
|
||||
- The Blender skill pack is local, unversioned guidance; it can drift from the project contract or
|
||||
assume Blender MCP. Re-verify every adopted technique with `inspect`/`audit`/`fit` and never
|
||||
make a skill, asset pack or tool version a build or CI dependency.
|
||||
- The 3D re-evaluation can expand without bound (retopo every head, rig every finger). Cap it with
|
||||
the scorecard ranking: P0/P1 only, one asset family per commit, and an explicit accept for
|
||||
everything else.
|
||||
|
||||
- [x] Re-run the screen-design judge for `12-joke-shop`, `13-studio-arrival` and the
|
||||
Forward+ A/B with the valid key: **13/13 match, 0 diff**; the HQ A/B is in the report.
|
||||
- [ ] Capture a hooded/winter NPC close-up once a scenario spawns types 5-7 (the explicit
|
||||
`--portrait-type` override is already in `capture_realism.gd`).
|
||||
- [ ] Human-authored tree/hedge silhouettes: the whole scene now reads well except the foliage,
|
||||
which is still flat-shaded clumps (`_shrub()`/`_tree()` in `tools/blender_assets.py`).
|
||||
Only worth doing if a higher-poly organic builder is added to the library.
|
||||
- [ ] Camera height: the concept frames are shot at ~1.55 m with a slightly wider lens; the game
|
||||
camera sits at 1.7 m. Decide whether the first-person height should match the art direction
|
||||
or stay at the current gameplay value.
|
||||
## Future / deferred (not scheduled)
|
||||
|
||||
## Future ideas (optional, not scheduled)
|
||||
|
||||
- [ ] Live 3D body-camera replay (the timeline is text-only today).
|
||||
- [ ] Additional venues or seasonal events authored from the new `.tres` files.
|
||||
- [ ] Real voice/dialogue audio (the crowd bed and radio crackle are procedural).
|
||||
- [ ] Live 3D body-camera replay; the timeline is text-only today.
|
||||
- [ ] Additional venues or seasonal events authored from the `content/venues/` resources.
|
||||
- [ ] Real voice/dialogue audio; ambience, cues and radio crackle are procedural.
|
||||
- [ ] Swap the primitive prop builder for an image-to-mesh model API behind the same
|
||||
`assets/models/generated/<prop>.glb` contract.
|
||||
- [ ] Rare easter eggs/cameos (e.g. Bob Dobbs) only after core content and QA are stable.
|
||||
@@ -0,0 +1,207 @@
|
||||
# 3D model review (2026-11-03)
|
||||
|
||||
Status: **baseline measured; scorecard open.** This is the working document for the unversioned
|
||||
`3D model re-evaluation` track in [`TODO.md`](../TODO.md). It records what was measured, what the
|
||||
Blender skill guidance says the targets are, the ranked candidate gaps, and the decisions that
|
||||
still need the project owner. Nothing here re-opens the accepted 0.9.4 limits by itself.
|
||||
|
||||
Related documents: [`npc-visual-review.md`](npc-visual-review.md) (accepted 0.9.4 limits and
|
||||
before/after sheets), [`npc-proportion-audit.md`](npc-proportion-audit.md) (proportion guide),
|
||||
[`blender-assets.md`](blender-assets.md) (asset contract), [`asset-tooling.md`](asset-tooling.md)
|
||||
(commands).
|
||||
|
||||
## Method and skill routing
|
||||
|
||||
The review is screenshot- and measurement-backed, following `qa-review` (screenshot set, defect
|
||||
severity, explicit verdict) rather than an opinion pass:
|
||||
|
||||
| Family | Skills loaded | Criteria taken from them |
|
||||
| --- | --- | --- |
|
||||
| Routing and pipeline | `blender-director` | Plan → blockout → detail → validate → export; MCP steps translated to headless flatpak runs |
|
||||
| Characters | `character-artist`, `retopology`, `rigging` | Eye/mouth/jaw/shoulder loops, quad-dominant deformation surfaces, 3-loop joints, no triangles on primary deformers |
|
||||
| UVs and textures | `uv-workflow`, `texture-workflow`, `materials` | Texel-density tiers (hero 512–1024, standard 256–512, background 128–256 px/m), consistent density, no overlapping UVs |
|
||||
| Budgets and structure | `asset-optimization`, `lod-pipeline` | Triangle/vertex budgets, naming, LOD tiers |
|
||||
| Props, gear, foliage | `prop-artist`, `hard-surface`, `vegetation-artist` | Silhouette read, bevel/chamfer language, collision proxies, carded foliage |
|
||||
| Look and verdict | `lookdev`, `qa-review` | Day/night beauty captures, ship / ship-with-notes / no-ship verdict |
|
||||
|
||||
Project contract and gates stay authoritative; see `AGENTS.md` §Local Blender skills. The skills
|
||||
assume Blender MCP, which this project does not use: everything below came from the headless
|
||||
`tools/blender_assets.py` / `tools/npc_pipeline.py` pipelines plus the Godot capture harnesses.
|
||||
|
||||
## What was captured and measured (2026-11-03)
|
||||
|
||||
| Evidence | Command | Result |
|
||||
| --- | --- | --- |
|
||||
| Contract audit | `python3 tools/blender_assets.py audit --json` | 46/46 assets clean; library 59,012 tris |
|
||||
| Imported-GLB inspection | `blender_assets.py inspect` (all 46) | All clean; transforms applied, helpers ignored |
|
||||
| Gear fit | `python3 tools/blender_assets.py fit --json` | Head width 0.315 m on all eight; clothing radius 0.190–0.260 m |
|
||||
| Proportions | `python3 tools/npc_proportions.py report --json` | All eight inside the stylized guide |
|
||||
| Texel density | `python3 tools/blender_assets.py texels --json` | New measurement, table below |
|
||||
| Neutral baseline | `capture_npc_baseline.gd` → `tmp/model-baseline` (48 frames) + `npc_baseline.py sheet` | 48 frames, none blank; sheet in `tmp/` only |
|
||||
| Final context | `capture_npc_review.gd` → `tmp/model-review` (96 frames) | 96 frames, none blank; raw frames stay local |
|
||||
| Performance | `smoke_npc_budget.gd` | 24 avatars, 168 meshes, 131,616 tris, 6.94 ms of 50 ms |
|
||||
|
||||
Raw captures and JSON stay in the gitignored `tmp/`; committed review images remain the
|
||||
`docs/blender_previews/sheet_*.jpg` set from 0.9.4/0.9.5 until a fix actually lands.
|
||||
|
||||
## Character measurements
|
||||
|
||||
Contract numbers from `audit` + `inspect`; density from `texels` (256² slot maps, authored and
|
||||
with the runtime 4× fabric repeat).
|
||||
|
||||
| Character | Tris / verts | Height / feet | Materials | Authored td (px/m) | Fabric effective (px/m) | Verdict |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| `npc_casual` | 4,482 / 4,260 | 1.824 / +0.000 | 7 | 106.8–122.2 | 429–440 | Contract pass; visual accepted 0.9.4 |
|
||||
| `npc_jacket` | 4,872 / 4,662 | 1.824 / +0.000 | 7 | 100.3–113.7 | 409–413 | Contract pass; visual accepted 0.9.4 |
|
||||
| `npc_coat` | 5,474 / 5,265 | 1.824 / +0.000 | 7 | 75.3–81.4 | 307–310 | Contract pass; visual accepted 0.9.4 |
|
||||
| `npc_vest` | 4,980 / 4,879 | 1.819 / +0.000 | 9 | 85.5–99.3 | 362–366 | Contract pass; visual accepted 0.9.4 |
|
||||
| `npc_dress` | 4,900 / 4,719 | 1.825 / +0.000 | 7 | 81.9–89.0 | 338–339 | Contract pass; visual accepted 0.9.4 |
|
||||
| `npc_hoodie` | 5,304 / 4,950 | 1.856 / +0.000 | 7 | 89.5–98.6 | 359–364 | Contract pass; visual accepted 0.9.4 |
|
||||
| `npc_firefighter` | 5,344 / 5,494 | 1.819 / +0.000 | 9 | 64.0–75.1 | 287–290 | Contract pass; lowest density |
|
||||
| `npc_guard` | 5,046 / 5,096 | 1.819 / +0.000 | 11 | 90.0–100.8 | 374–374 | Contract pass; visual accepted 0.9.4 |
|
||||
| `npc_base` (fallback) | 1,204 / 2,488 | 1.836 / +0.000 | 5 | 70.1–72.9 | 4× on fabric | Contract pass; far below its 6,000/4,000 cap |
|
||||
|
||||
Blender-side `inspect` counts can differ by ±1 vertex from the glTF accessor count in `audit`
|
||||
(normal-split rounding); both are recorded and neither changes a verdict.
|
||||
|
||||
Readings:
|
||||
|
||||
- Every character is inside 8,000/8,000 with the farthest at 5,474 tris (68% of budget) and 5,494
|
||||
verts (69%), so there is headroom for targeted topology detail without a cap change.
|
||||
- Surface area tracks clothing volume: the firefighter coat is the largest body (4.28 m²) and has
|
||||
the lowest authored density; `npc_casual` is the smallest (2.44 m²) and the highest.
|
||||
- **Authored density (64–122 px/m) sits below the skill's background tier (128–256 px/m).** The
|
||||
runtime tint controls most of the look: `skin`, `hair`, `accessory` and the fixed detail slots
|
||||
are neutral maps tinted at runtime, not painted skin, so density mostly controls subtle
|
||||
weave/noise. Fabric is effective 287–440 px/m (standard tier) because of the 4× repeat. This is
|
||||
a recorded note, not a defect, until a portrait shows texture starvation.
|
||||
- Material counts 7–11 per character are fixed detail slots (eyes, rubber, hivis, reflective,
|
||||
leather, metal), not extra tint slots.
|
||||
|
||||
## Scorecard skeleton
|
||||
|
||||
Verdicts use `qa-review` semantics: **P0** (blocker: wrong scale, broken normals, budget fail),
|
||||
**P1** (major: silhouette/readability), **P2** (minor), **accept** (recorded limit). `—` means not
|
||||
yet reviewed; this document is the place to fill it.
|
||||
|
||||
### Characters
|
||||
|
||||
| Character | Silhouette | Face @1.85 m | Hands/grip | Weights/pose | Cloth/hair | LOD | Verdict |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `npc_casual` | accept (0.9.4) | P1 (faceted planes) | accept (open palm) | — | accept | accept (two-quad impostor) | pending owner |
|
||||
| `npc_jacket` | accept | P1 | accept | — | accept | accept | pending owner |
|
||||
| `npc_coat` | accept | P1 | accept | — | accept (hem 0.76 stride) | accept | pending owner |
|
||||
| `npc_vest` | accept | P1 | accept | — | accept | accept | pending owner |
|
||||
| `npc_dress` | accept; performer props dominate | P1 | accept | — | accept | accept | pending owner |
|
||||
| `npc_hoodie` | accept (hooded criterion) | accept (lower face only) | accept | — | accept | accept | pending owner |
|
||||
| `npc_firefighter` | accept | P1 | accept | — | accept | accept | pending owner |
|
||||
| `npc_guard` | accept | P1 | accept | — | accept | accept | pending owner |
|
||||
|
||||
### Gear (18)
|
||||
|
||||
| Gear | Attach | Tris | Verdict |
|
||||
| --- | --- | --- | --- |
|
||||
| `cap` | head | 328 | — (fit measured, visual pending) |
|
||||
| `beanie` | head | 460 | — (unused by any character) |
|
||||
| `hood` | head | 804 | — |
|
||||
| `sunglasses` | head | 164 | accept (0.9.4 clearance) |
|
||||
| `firefighter_helmet` | head | 672 | accept (0.9.4 + 3 cm runtime offset) |
|
||||
| `backpack` | back | 380 | accept (0.9.4 strap refit) |
|
||||
| `shoulder_bag` | back | 188 | — |
|
||||
| `scba_tank` | back | 362 | — |
|
||||
| `lanyard` | chest | 100 | accept (0.9.4 + 4 cm runtime offset) |
|
||||
| `phone` | hand | 96 | accept (forward offset) |
|
||||
| `cup` | hand | 188 | accept |
|
||||
| `clipboard` | hand | 84 | accept |
|
||||
| `umbrella` | hand | 134 | accept |
|
||||
| `camera` | hand | 318 | accept |
|
||||
| `protest_sign` | hand | 92 | accept (performer/protester read) |
|
||||
| `flashlight` | hand | 168 | accept |
|
||||
| `wand` | hand | 244 | accept |
|
||||
| `radio` | hand | 220 | accept |
|
||||
|
||||
Gear total: 5,002 tris across 18 pieces (all far below the 1,500-per-piece cap).
|
||||
|
||||
### Props (20 library + 6 generated fallbacks)
|
||||
|
||||
Library total 13,608 tris; `marquee` (2,804) and `street_tree` (2,592) are the two heaviest and are
|
||||
the first candidates for a `vegetation-artist`/lookdev pass.
|
||||
|
||||
| Prop | Tris | Verdict |
|
||||
| --- | --- | --- |
|
||||
| `bollard` | 138 | — |
|
||||
| `bench` | 560 | — |
|
||||
| `trash_bin` | 228 | — |
|
||||
| `planter_box` | 1,324 | — |
|
||||
| `newspaper_box` | 244 | — |
|
||||
| `fire_hydrant` | 466 | — |
|
||||
| `street_lamp` | 428 | — |
|
||||
| `street_tree` | 2,592 | P1 candidate (flat-shaded clumps) |
|
||||
| `palm_tree` | 400 | P1 candidate (same foliage language) |
|
||||
| `round_planter` | 1,240 | — |
|
||||
| `a_frame_sign` | 212 | — |
|
||||
| `wall_lantern` | 180 | — |
|
||||
| `traffic_cone` | 210 | — |
|
||||
| `crowd_barrier` | 388 | — |
|
||||
| `stanchion` | 434 | — |
|
||||
| `handrail` | 268 | — |
|
||||
| `security_podium` | 184 | — |
|
||||
| `double_door` | 380 | — |
|
||||
| `marquee` | 2,804 | — (emissive check pending) |
|
||||
| `sedan` | 928 | — |
|
||||
|
||||
Generated fallbacks (`assets/models/generated/`): bench, bollard, fire_hydrant, newspaper_box,
|
||||
planter_box, trash_bin. They are only used when the library prop is missing, so they are audited
|
||||
for presence, not re-reviewed for art quality.
|
||||
|
||||
### Venues
|
||||
|
||||
| Venue | Measured (map-budget test) | Status |
|
||||
| --- | --- | --- |
|
||||
| Hollywood Boulevard | 58,884 tris / 409 meshes | shape-language review pending |
|
||||
| The Joke Shop | 22,680 tris / 454 meshes | shape-language review pending |
|
||||
| TV Studio | — | shape-language review pending |
|
||||
|
||||
## Ranked candidate gaps
|
||||
|
||||
Ordered by expected player-visible gain per unit of work. None is approved for fixing yet; the
|
||||
owner review decides.
|
||||
|
||||
1. **Face plane structure at portrait distance (P1, all eight).** The neutral sheet shows flat,
|
||||
faceted cheek/brow planes and slit-like eyes at 1.85 m. `character-artist`/`retopology` guidance
|
||||
asks for concentric eye loops and a 3-loop mouth. This is the largest readability gap and also
|
||||
the most expensive (head rebuild + re-UV + re-AO per variant). Needs a targeted single-variant
|
||||
prototype before committing to eight.
|
||||
2. **No pose/weight evidence (unknown).** Idle, walk, sit, block and turn deformation has never been
|
||||
captured as a review set, so shoulder/hip/neck collapse is unmeasured. Cheap to add and it
|
||||
gates any claim that the rig is acceptable.
|
||||
3. **Foliage language (P1, `street_tree`, `palm_tree`, `planter_box`, `round_planter`).** Flat-shaded
|
||||
clumps against an otherwise readable set, already deferred once. `vegetation-artist` carded
|
||||
clusters would fix it inside the existing triangle budget.
|
||||
4. **Texel density below the background tier (note).** 64–122 px/m authored vs the skill's 128–256
|
||||
background tier. Fabric is already standard-tier at runtime. Decide: accept because the maps are
|
||||
neutral tinted detail, or raise only `skin`/`hair` to 512² for portrait distance.
|
||||
5. **`npc_firefighter` density floor (P2).** 64 px/m with the largest coat area; split the coat UV
|
||||
shell or raise the map size before the next character pass, whichever the density decision says.
|
||||
6. **Hands at conversation distance (accept, revisit only if face work opens the budget).** Open
|
||||
palm with `HAND_ITEM_FORWARD`; no finger bones exist, so a curl rig is a contract decision, not
|
||||
a modelling tidy-up.
|
||||
7. **Gear never reviewed as silhouette language (P2).** 18 pieces pass fit/contract but have no
|
||||
prop-artist verdict; `beanie` is unused and could be retired or reassigned.
|
||||
8. **Venue shape language (P2).** Hollywood and the Joke Shop have budget data but no
|
||||
`environment-artist`/`archviz` review; the TV Studio has no recorded map-budget number in this
|
||||
document yet.
|
||||
|
||||
## Decisions needed from the owner
|
||||
|
||||
- Accept the measured texel density as a stylized limitation, or fund a skin/hair resolution pass.
|
||||
- Approve or reject a single-variant face/eye-loop prototype (and pick which variant to prototype
|
||||
on) before any eight-character rebuild.
|
||||
- Decide whether the pose/weight capture is required before the crowd-variant roster.
|
||||
- Confirm foliage rebuild priority versus the map track (both touch the same street trees).
|
||||
|
||||
## Next actions
|
||||
|
||||
Tracked in [`TODO.md`](../TODO.md) `3D model re-evaluation`: the remaining `A. Baseline…` item is
|
||||
this owner review; `B`/`C`/`D` carry the per-family work, and `E` adds the pose/weight,
|
||||
collider-coverage and scorecard-coverage gates this document found missing.
|
||||
+19
-5
@@ -2,19 +2,33 @@
|
||||
|
||||
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
|
||||
- [`3d-model-review.md`](3d-model-review.md) — 3D model re-evaluation: measurements, scorecard and ranked 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
@@ -138,9 +143,90 @@ 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
|
||||
python3 tools/blender_assets.py texels # per-slot texel density (px/m) of the characters
|
||||
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 (write to a file: Blender prints a banner first)
|
||||
... -- inspect --file "$PWD/tmp/x.glb" --json --out "$PWD/tmp/x.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. Triangles match `audit` on project assets; the seam-split
|
||||
vertex count can differ by ±1 because the importer rounds normals. `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.
|
||||
- `--json` also captures the importer's INFO log in `stats.import_log` instead of printing it.
|
||||
Blender still prints its own version banner before the script runs, so intake scripts should use
|
||||
`--json --out <file>` rather than parsing stdout.
|
||||
|
||||
### Texel density (`texels`)
|
||||
|
||||
`texel_density()` and the `texels` command compute `resolution * sqrt(UV area / world area)` per
|
||||
material slot straight from the GLB, so the number does not depend on island packing. The NPC slot
|
||||
maps are 256², and cloth/pants are reported twice: authored density and effective density with the
|
||||
runtime 4× repeat from `Materials.NPC_FABRIC_UV_SCALE`. Current values and interpretation are in
|
||||
[`3d-model-review.md`](3d-model-review.md).
|
||||
|
||||
```bash
|
||||
python3 tools/blender_assets.py texels # all eight characters
|
||||
python3 tools/blender_assets.py texels --json # machine-readable
|
||||
python3 tools/blender_assets.py texels --only npc_coat
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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 |
@@ -1,11 +1,17 @@
|
||||
# Development Status
|
||||
|
||||
Current planning priorities are the unversioned **3D model re-evaluation**, **Character
|
||||
development** and **Local map development** tracks in [`TODO.md`](../TODO.md); none is part of the
|
||||
implemented list below yet. The re-evaluation scores the existing characters, gear, props and
|
||||
venues against the locally installed Blender skill guidance before the roster expands. 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 +48,10 @@ 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;
|
||||
Python tooling is 108/108 after the GLB `inspect` intake coverage.
|
||||
- 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>.
|
||||
@@ -59,7 +69,8 @@ Implemented so far:
|
||||
- NPC P1 portrait fixes (0.9.4 work): tourists now hide their photo-pose camera bag outside `photo`/`film` poses, removing the duplicate-backpack silhouette; the backpack gear was rebuilt with flattened, forward chest straps and a fresh AO map. The before/after sheet is [`blender_previews/sheet_npc_baseline_compare-p1.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg); the remaining face/hand work was closed under the 0.9.4 acceptance limits.
|
||||
- NPC visual baseline (0.9.4 work): `src/test/capture_npc_baseline.gd` captures all eight library characters from front, three-quarter, profile, back, full-body and crowd views in a neutral studio (48 frames). The criteria, measurements and ranked gaps are in [`npc-visual-review.md`](npc-visual-review.md); the committed sheet is [`blender_previews/sheet_npc_baseline.jpg`](blender_previews/sheet_npc_baseline.jpg). No model changes were made in this baseline step.
|
||||
- UI-state capture pass (0.9.3 work): `src/test/capture_ui_states.gd` adds committed captures 14–21 for the start menu, pause, settings, shift review, emergency radio, action wheel, crowd panic and responder arrival. Every one of the 21 catalogue entries now has a current-build capture; `tools/check_screenshots.sh` reports 21 good / 0 bad and the automatic judge reports 21/21 match, 0 diff. Crowd panic and responder arrival are staged presentation frames, not gameplay changes.
|
||||
- Realism adoption (post-v0.6.0): v2 per-phase skylines and surface tiles with normal maps, retuned sun/exposure/fog, wet asphalt, denser foliage, a visible additive flashlight beam, and refreshed staging captures. `tools/check_screenshots.sh` is the repeatable capture/compare gate; judge is clean of `diff` verdicts.
|
||||
- Realism adoption (post-v0.6.0): v2 per-phase skylines and surface tiles with normal maps, retuned sun/exposure/fog, wet asphalt, denser foliage, a tight additive flashlight spot with no beam geometry, and refreshed
|
||||
staging captures. `tools/check_screenshots.sh` is the repeatable capture/compare gate; judge is clean of `diff` verdicts.
|
||||
- Input/reliability fixes: selecting a venue releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status. Mouse look now reads at `_input`, restores capture on window focus/click-back, and the HUD is mouse-transparent. Normal radio reports submit on click; emergency calls still require the hold-to-confirm path.
|
||||
- Production-quality pass (v0.9.0): mixer/accessibility settings, audio volume clamping, 24-NPC budget smoke, shortened coat/dress stride, and a unified Godot 4.7.2 CI/local support baseline with documented upgrade/rollback requirements.
|
||||
- Campaign and first-hour pass (v0.8.0): campaign progression, checkpoint resume, difficulty presets, a searchable Help overlay, recovery events, contextual dialogue, policy/debrief lines and tutorial/replay/skip. Scripted QA bypasses the tutorial unless `--tutorial` is passed.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -13,6 +13,10 @@ the conversation-distance hand pass
|
||||
[`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg), and the
|
||||
forward-grip pass [`sheet_npc_hands_grip_compare.jpg`](blender_previews/sheet_npc_hands_grip_compare.jpg).
|
||||
|
||||
The 2026-11-03 skill-guided re-evaluation, its measurements (texel density, budgets, imported
|
||||
bounds) and the open scorecard live in [`3d-model-review.md`](3d-model-review.md). It does not
|
||||
change the accepted limits below.
|
||||
|
||||
## What was captured
|
||||
|
||||
`src/test/capture_npc_baseline.gd` stages all eight library characters in a
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ config_version=5
|
||||
|
||||
config/name="Gig Line"
|
||||
config/description="Workplace security simulation about observation, access verification, and proportionate escalation."
|
||||
config/version="0.9.6"
|
||||
config/version="0.10.0-rc.1"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+12
@@ -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
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"index": 1,
|
||||
"next_venue": "joke_shop",
|
||||
"shift_number": 2,
|
||||
"difficulty": "hard",
|
||||
"complete": false,
|
||||
"completed": [
|
||||
"hollywood_building"
|
||||
],
|
||||
"last_grade": "B"
|
||||
}
|
||||
+15
@@ -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
@@ -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
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -32,6 +35,8 @@ Conventions (Blender is Z-up; the glTF exporter converts to Godot Y-up):
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import struct
|
||||
@@ -1788,6 +1793,412 @@ def audit_asset(category, name):
|
||||
return res
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Texel density (UV area vs world area, per material slot)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# The NPC slot maps are 256² (`npc_pipeline.TEX_SIZE`); cloth and pants repeat
|
||||
# the authored UVs 4× at runtime via `Materials.NPC_FABRIC_UV_SCALE`, so their
|
||||
# effective density is four times the authored number.
|
||||
TEXEL_RESOLUTION = 256
|
||||
TEXEL_RUNTIME_UV_SCALE = {"cloth": 4.0, "pants": 4.0}
|
||||
|
||||
|
||||
def _tri_area3(a, b, c) -> float:
|
||||
ux, uy, uz = b[0] - a[0], b[1] - a[1], b[2] - a[2]
|
||||
vx, vy, vz = c[0] - a[0], c[1] - a[1], c[2] - a[2]
|
||||
cx = uy * vz - uz * vy
|
||||
cy = uz * vx - ux * vz
|
||||
cz = ux * vy - uy * vx
|
||||
return 0.5 * math.sqrt(cx * cx + cy * cy + cz * cz)
|
||||
|
||||
|
||||
def _tri_area2(a, b, c) -> float:
|
||||
return 0.5 * abs((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]))
|
||||
|
||||
|
||||
def texel_density(name: str, path: Path | None = None) -> dict | None:
|
||||
"""Per-slot texel density (px/m) for a character GLB.
|
||||
|
||||
texel density = resolution * sqrt(UV area / world surface area), so the
|
||||
number is independent of how the island is packed. Effective density also
|
||||
applies the runtime UV repeat for cloth/pants.
|
||||
"""
|
||||
path = path or asset_path("npc", name)
|
||||
if not path.is_file():
|
||||
return None
|
||||
try:
|
||||
_magic, _version, _length, _size, gltf, binary = _read_glb(path)
|
||||
except (struct.error, ValueError, KeyError):
|
||||
return None
|
||||
materials = [m.get("name", "") for m in gltf.get("materials", [])]
|
||||
slots: dict[str, dict] = {}
|
||||
for mesh in gltf.get("meshes", []):
|
||||
for prim in mesh.get("primitives", []):
|
||||
attrs = prim.get("attributes", {})
|
||||
if ("POSITION" not in attrs or "TEXCOORD_0" not in attrs
|
||||
or prim.get("indices") is None):
|
||||
continue
|
||||
mat_index = prim.get("material")
|
||||
slot = materials[mat_index] if mat_index is not None and mat_index < len(materials) else ""
|
||||
positions = _accessor(gltf, binary, attrs["POSITION"])
|
||||
uvs = _accessor(gltf, binary, attrs["TEXCOORD_0"])
|
||||
indices = [row[0] for row in _accessor(gltf, binary, prim["indices"])]
|
||||
row = slots.setdefault(slot or "unassigned",
|
||||
{"triangles": 0, "world_area_m2": 0.0, "uv_area": 0.0})
|
||||
for k in range(0, len(indices) - 2, 3):
|
||||
i0, i1, i2 = indices[k], indices[k + 1], indices[k + 2]
|
||||
if max(i0, i1, i2) >= len(positions) or max(i0, i1, i2) >= len(uvs):
|
||||
continue
|
||||
row["world_area_m2"] += _tri_area3(positions[i0], positions[i1], positions[i2])
|
||||
row["uv_area"] += _tri_area2(uvs[i0], uvs[i1], uvs[i2])
|
||||
row["triangles"] += 1
|
||||
for slot, row in slots.items():
|
||||
world = row["world_area_m2"]
|
||||
uv = row["uv_area"]
|
||||
authored = TEXEL_RESOLUTION * math.sqrt(uv / world) if world > 0.0 else 0.0
|
||||
row["td_px_per_m"] = round(authored, 1)
|
||||
row["td_runtime_px_per_m"] = round(authored * TEXEL_RUNTIME_UV_SCALE.get(slot, 1.0), 1)
|
||||
row["world_area_m2"] = round(world, 3)
|
||||
row["uv_area"] = round(uv, 4)
|
||||
values = [row["td_px_per_m"] for row in slots.values() if row["td_px_per_m"] > 0.0]
|
||||
return {
|
||||
"name": name,
|
||||
"resolution": TEXEL_RESOLUTION,
|
||||
"slots": slots,
|
||||
"min_td": min(values) if values else 0.0,
|
||||
"max_td": max(values) if values else 0.0,
|
||||
"total_world_area_m2": round(sum(r["world_area_m2"] for r in slots.values()), 3),
|
||||
}
|
||||
|
||||
|
||||
def texel_report() -> list[dict]:
|
||||
return [row for row in (texel_density(name) for name in CHARACTERS) if row]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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()
|
||||
# The glTF importer logs per-mesh INFO lines to stdout, which would corrupt
|
||||
# `--json` output; keep the log in stats instead of printing it.
|
||||
import_log = io.StringIO()
|
||||
try:
|
||||
with contextlib.redirect_stdout(import_log), contextlib.redirect_stderr(import_log):
|
||||
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,
|
||||
"import_log": import_log.getvalue().strip().splitlines()[-3:],
|
||||
"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 +2401,64 @@ 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")
|
||||
insp.add_argument("--out", default="",
|
||||
help="with --json, write to this file (Blender prints a startup banner)")
|
||||
tex = sub.add_parser("texels", help="per-slot texel density of the character GLBs")
|
||||
tex.add_argument("--only", default="")
|
||||
tex.add_argument("--json", action="store_true")
|
||||
sub.add_parser("sheet")
|
||||
sub.add_parser("list")
|
||||
args = ap.parse_args(raw)
|
||||
|
||||
if args.cmd == "texels":
|
||||
names = [n.strip() for n in args.only.split(",") if n.strip()] or list(CHARACTERS)
|
||||
report = [row for row in (texel_density(n) for n in names) if row]
|
||||
if not report:
|
||||
print("no built character assets; run build first", file=sys.stderr)
|
||||
return 2
|
||||
if args.json:
|
||||
print(json.dumps(report, indent=2))
|
||||
else:
|
||||
for entry in report:
|
||||
print(f"{entry['name']:16s} {entry['resolution']}px maps "
|
||||
f"area={entry['total_world_area_m2']:.2f} m2 "
|
||||
f"td={entry['min_td']:.0f}-{entry['max_td']:.0f} px/m")
|
||||
for slot, row in sorted(entry["slots"].items()):
|
||||
note = " (runtime x%.0f)" % TEXEL_RUNTIME_UV_SCALE[slot] \
|
||||
if slot in TEXEL_RUNTIME_UV_SCALE else ""
|
||||
print(f" {slot:12s} tris={row['triangles']:5d} area={row['world_area_m2']:5.2f} m2 "
|
||||
f"uv={row['uv_area']:.3f} td={row['td_px_per_m']:6.0f} "
|
||||
f"effective={row['td_runtime_px_per_m']:6.0f} px/m{note}")
|
||||
return 0
|
||||
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:
|
||||
payload = json.dumps(results, indent=2)
|
||||
if args.out:
|
||||
Path(args.out).write_text(payload + "\n", encoding="utf-8")
|
||||
print(f"wrote {args.out}")
|
||||
else:
|
||||
# Blender prints its version banner before this script runs, so
|
||||
# scripts should prefer --out for machine-readable output.
|
||||
print(payload)
|
||||
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 "
|
||||
|
||||
@@ -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" .)
|
||||
|
||||
@@ -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"
|
||||
@@ -95,5 +95,141 @@ class BuiltAssetTests(unittest.TestCase):
|
||||
self.assertTrue((ba.SRC_DIR / ".gdignore").is_file())
|
||||
|
||||
|
||||
@unittest.skipUnless(ba.MANIFEST.is_file(), "Blender assets not built")
|
||||
class TexelDensityTests(unittest.TestCase):
|
||||
def test_character_slots_report_density_and_area(self):
|
||||
report = ba.texel_density("npc_casual")
|
||||
self.assertEqual(report["resolution"], 256)
|
||||
self.assertEqual(set(report["slots"]), set(ba.SLOTS) | {"eye_white", "rubber"})
|
||||
for slot in ba.SLOTS:
|
||||
row = report["slots"][slot]
|
||||
self.assertGreater(row["triangles"], 0, slot)
|
||||
self.assertGreater(row["world_area_m2"], 0.0, slot)
|
||||
self.assertGreater(row["td_px_per_m"], 30.0, slot)
|
||||
self.assertGreater(report["total_world_area_m2"], 1.0)
|
||||
self.assertLess(report["total_world_area_m2"], 6.0)
|
||||
|
||||
def test_fabric_slots_apply_the_runtime_uv_repeat(self):
|
||||
report = ba.texel_density("npc_casual")
|
||||
for slot, factor in ba.TEXEL_RUNTIME_UV_SCALE.items():
|
||||
row = report["slots"][slot]
|
||||
self.assertAlmostEqual(row["td_runtime_px_per_m"],
|
||||
row["td_px_per_m"] * factor, places=0)
|
||||
skin = report["slots"]["skin"]
|
||||
self.assertEqual(skin["td_px_per_m"], skin["td_runtime_px_per_m"])
|
||||
|
||||
def test_base_fallback_is_measurable_from_its_own_path(self):
|
||||
from pathlib import Path
|
||||
path = ba.ROOT / "assets" / "models" / "generated" / "npc_base.glb"
|
||||
report = ba.texel_density("npc_base", path)
|
||||
self.assertIsNotNone(report)
|
||||
self.assertGreater(report["min_td"], 30.0)
|
||||
|
||||
|
||||
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()
|
||||
Reference in new issue
Block a user