Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3acf370fc | ||
|
|
04d211b90b | ||
|
|
a6ccd86f85 | ||
|
|
1ce173e9d1 | ||
|
|
90f189a269 | ||
|
|
fb3e75aba3 | ||
|
|
cd285b3a39 | ||
|
|
68f49253fe | ||
|
|
b012c6a675 | ||
|
|
2a6f29f5d0 | ||
|
|
bb54ef3eec | ||
|
|
11f53f66a3 | ||
|
|
9b65a93587 | ||
|
|
56a14747d4 | ||
|
|
8b1bf736af | ||
|
|
f121167e52 | ||
|
|
140a7ada0f | ||
|
|
5508f5510f | ||
|
|
7baa2c4f54 | ||
|
|
0cd12af994 | ||
|
|
271f10ac4d | ||
|
|
e2f52fbfd1 | ||
|
|
f5fa321a8f | ||
|
|
a2ae9e03c3 | ||
|
|
cff3937c16 | ||
|
|
1ed9e05d9b | ||
|
|
8c69f367b0 | ||
|
|
2a7921bae6 | ||
|
|
b419923637 | ||
|
|
c7fd4b59cc |
No files matched your search
@@ -90,12 +90,16 @@ specialists the task needs:
|
||||
|
||||
### Project rules that override a skill
|
||||
|
||||
1. **The project contract wins.** `npc_base` mesh node, the 13 required core bones at their rest
|
||||
positions, five tint slots, front on Blender −Y / Godot +Z, grounded feet, 24,000
|
||||
triangles / 24,000 glTF vertices per character, 1,500 per gear, 4,000 per prop. Optional
|
||||
extra bones (fingers, face, toes) are allowed if they do not move the core rest positions. 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.
|
||||
1. **The project contract wins.** `npc_base` mesh node, five tint slots, front on Blender −Y /
|
||||
Godot +Z, grounded feet, 24,000 triangles / 24,000 glTF vertices per character, 1,500 per
|
||||
gear, 4,000 per prop. The shipped procedural library uses the 13 required core bones at their
|
||||
rest positions; optional extra bones (fingers, face, toes) are allowed if they do not move the
|
||||
core rest positions. The one documented exception is the accepted Quaternius runtime path
|
||||
([ADR-0018](docs/adr/0018-quaternius-stage2-runtime-rig.md)): it keeps the 62-joint source rig
|
||||
at runtime while the 0.9.9 face/silhouette gate is open, and it must still supply the five tint
|
||||
slots, valid gear attach points and the same budgets. Both paths stay tested until that gate
|
||||
closes. 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. **Blender MCP is optional interactive tooling.** The headless flatpak scripts
|
||||
(`tools/blender_assets.py`, `tools/npc_pipeline.py`) remain authoritative. MCP may be used
|
||||
for local part exploration; setup and safety are in [`docs/asset-tooling.md#optional-interactive-blender-mcp`](docs/asset-tooling.md#optional-interactive-blender-mcp).
|
||||
@@ -104,8 +108,9 @@ specialists the task needs:
|
||||
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.
|
||||
generated meshes; ADR-0017 (the CC0 Quaternius NPC base) supersedes the generator-first
|
||||
decision in ADR-0015, and the CC0/CC-BY rule in ADR-0015 (attribution in `CREDITS.md`) plus
|
||||
the provenance rules in `docs/npc-model-sourcing.md` still apply to any new source.
|
||||
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.
|
||||
|
||||
@@ -6,11 +6,33 @@ Semantic Versioning.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.9.9] — 2026-11-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Raised the character mesh budget from 16,000 to 24,000 triangles and glTF
|
||||
vertices so the Quaternius NPC base, the five-slot clothing variants and the
|
||||
retargeted animation can fit without heavy decimation. Gear and prop budgets are unchanged.
|
||||
vertices so the Quaternius base and the role-dressed variants fit without
|
||||
heavy decimation. Gear and prop budgets are unchanged.
|
||||
- Replaced the whole NPC roster with the CC0 Quaternius characters. All twelve
|
||||
characters (tourist, jacket, coat, vest, dress, hoodie, firefighter, guard and
|
||||
the four crowd variants) now use one of three source outfits — casual tee,
|
||||
hoodie or work clothes — dressed by role with the five tint slots and the gear
|
||||
library. The procedural library and the MakeHuman head remain in the build as
|
||||
the fallback.
|
||||
- Characters stand with their arms down instead of a T/A pose, and the seated,
|
||||
idle and walking poses swing from the shoulder and hip the way the animation
|
||||
code intends on the new skeleton.
|
||||
- Head gear (cap, hood, sunglasses, firefighter helmet) and the camera were
|
||||
resized for the new head and arm. The camera now hangs from a wrist strap
|
||||
instead of a neck strap that floated next to the head.
|
||||
- The backpack straps were pulled in so they sit on the chest instead of
|
||||
hovering in front of it.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Characters no longer render with black legs or black patches: the new base
|
||||
carried a collapsed UV map that made the ambient-occlusion bake come out
|
||||
black, and the roster is now unwrapped and baked correctly.
|
||||
|
||||
## [0.9.8] — 2026-11-04
|
||||
|
||||
|
||||
@@ -2143,3 +2143,52 @@ must inherit the reviewed face/hand/gear baseline instead of multiplying whateve
|
||||
project-owned clothing; keep out of CI/build.)
|
||||
- [x] Body-shape diversity decision (2026-11-04, owner): **yes** — use MakeHuman for the next
|
||||
character pass. Promoted to the **MakeHuman character pipeline** track.
|
||||
|
||||
## NPC pose harness (v0.9.8)
|
||||
|
||||
Completed 2026-11-04. The pose harness bug that made every staged capture save a rest frame
|
||||
was found and fixed; it unblocked the weights/pose evidence and the seated/walking deformation
|
||||
fixes. User-facing changes are recorded under 0.9.7 and 0.9.8 in `CHANGELOG.md`.
|
||||
|
||||
Goal: `src/test/capture_npc_pose.gd` reports a pose but saves rest frames; the root cause is
|
||||
narrowed to the skinned mesh ignoring its `Rig` parent transform. Fixing it unblocks the
|
||||
weights/pose column of the 3D review; the evidence now feeds the Quaternius base bake.
|
||||
|
||||
*Skill route: `rigging`; evidence in `docs/3d-model-review.md` §"Pose evidence is blocked".*
|
||||
|
||||
- [x] Root cause found (2026-11-04): the harness never disabled the avatar's `_physics_process`,
|
||||
so `_advance_fsm` + `_move_toward_target` ran every tick and reset `arrived`/velocity, which
|
||||
made `theater_pose()` return "idle" and reverted the pose to rest. (The earlier "moving the
|
||||
`Rig` child does not render" probe was a red herring: it set `rig.position.y` inside `_shoot()`
|
||||
in the same frame as `root.get_texture().get_image()`, so it never got a rendered frame.)
|
||||
- [x] Harness rebuilt: `capture_npc_pose.gd` now calls `set_physics_process(false)`/
|
||||
`set_process(false)` after staging the avatar; the pose survives because only the direct
|
||||
`_animate()` ticks drive it. Verified: sit vs idle diff 5.63, walk vs idle diff 2.29, and the
|
||||
vision judge confirms the seated frame reads as crouched.
|
||||
- [x] Captured all poses for all 12 variants (idle / walk_contact / walk_pass / sit × full +
|
||||
shoulder views = 96 frames) and built `sheet_pose.jpg`/`sheet_shoulder.jpg` under `tmp/`.
|
||||
(No separate "wave" theater pose exists; the escalation raise-hand gesture is not a distinct
|
||||
capture in this harness.)
|
||||
|
||||
## Milestone 0.9.7 — Character development (patch)
|
||||
|
||||
Completed 2026-11-04 and published as v0.9.7. The detailed character-development record already
|
||||
lives under "Character development" above; this milestone checklist is kept verbatim for the
|
||||
release record.
|
||||
|
||||
Goal: release the finished procedural character development — the realistic face on all eight
|
||||
variants, the four crowd-variety characters and the per-role pools — as a **patch** on top of
|
||||
0.9.6. This keeps the 3D model and map improvements out of the patch; they land together in the
|
||||
0.10.0 minor.
|
||||
|
||||
- [x] Rig pose harness fixed and the weights/pose evidence captured (see the top track).
|
||||
- [x] Character work done: 12-character library, realistic face, crowd variants, per-role pools,
|
||||
24-NPC budget 6.94 ms (152,464 tris / 168 meshes).
|
||||
- [x] QA + local build for 0.9.7: Godot 277/277, Python 161/161, audit 49 OK, fit/proportions
|
||||
12/12, release smoke matrix green, gallery 21/0, clean worktree build (Linux 172 MB,
|
||||
Windows 206 MB, macOS 159 MB, Web 111 MB, HQ 266 MB), checksums verified, Linux binary
|
||||
launches headless. Tagged `v0.9.7` (CI build/publish follows).
|
||||
|
||||
**Exit:** met — 0.9.7 ships the 12-character library (face realism + crowd variety) with the pose
|
||||
harness fixed; the runtime contract is unchanged. The Quaternius NPC base and the map rebuild are
|
||||
not in this patch (they target 0.10.0).
|
||||
@@ -77,6 +77,18 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
tested fallbacks until the Quaternius rollout ships. Quaternius source: CC0, attribution in
|
||||
`CREDITS.md`; the three researched GLBs and licence notes are kept in
|
||||
`tmp/npc-research/quaternius/`. MPFB/MakeHuman body work is closed; do not add new work there.
|
||||
- **Quaternius bake keeps source detail (2026-11-05, owner):** during the contract bake (§2),
|
||||
keep the Quaternius mesh as close to source as possible — no unnecessary decimation or LOD
|
||||
reduction. The canonical base is only 3,199 verts / 6,206 tris, far under the 24,000 cap, so
|
||||
there is no budget reason to decimate before the silhouette gate. Do not reuse the old
|
||||
7,000-triangle or MPFB decimation targets.
|
||||
- **Quaternius runtime rig (2026-11-05, owner, ADR-0018 option 2):** the Stage 1 proportion warp
|
||||
failed the silhouette gate (arm ×1.63 / leg ×0.74 / torso ×1.61; vision judge: paddle hands,
|
||||
lost forearms, stretched shins). The owner chose to **adopt the Quaternius 62-joint rig at
|
||||
runtime** instead of warping: `NpcAvatar` maps the Quaternius joint names, gear attach points
|
||||
are re-derived from the Quaternius skeleton (hip ≈0.87, shoulder ≈0.148/1.39, wrist ≈0.99,
|
||||
knee ≈0.52), the mesh keeps source LOD (arm-down rest pose baked), and Idle/Walk play directly
|
||||
on the 62-joint rig (no retarget).
|
||||
- **Engine baseline:** Godot 4.7.2 stable (official editor `~/.local/bin/godot`). The system's
|
||||
`godot-runner` cannot load arbitrary project paths (compiled without path overrides); use the
|
||||
official editor binary.
|
||||
@@ -95,6 +107,26 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
cleared, storefront/rating approval pending, external playtests and full hardware matrix pending.
|
||||
Recorded in `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md`, `docs/known-limitations.md`.
|
||||
|
||||
- **Release roadmap (2026-11-05, owner):** `0.9.9` Quaternius pilot base (rig/export fix,
|
||||
rest-relative pose math, owner face/silhouette gate), `0.9.10` 12-role roster, `0.9.11`
|
||||
scheduled venue/prop/lighting polish, `0.9.12` performance/Web/accessibility verification,
|
||||
`0.10.0` public-readiness minor, then `0.10.1`–`0.10.3` fixes, `0.11.0` replay + fourth venue +
|
||||
localization foundation, `0.12.0` signed/storefront builds + save v2, `0.13.x` content complete
|
||||
and release candidates, `1.0.0` stable. The full gap analysis and roadmap live in `PLAN.md`; the
|
||||
character track is versioned now, so a blocked rig cannot block the release. Planning only —
|
||||
none of these versions is built yet.
|
||||
- **Quaternius face/readability gate (2026-11-05):** the base is 6.2k tris total and replaces a
|
||||
library with a sculpted realistic face, so the 0.9.9 gate is a product decision, not just an
|
||||
art check. Owner picks full swap / crowd-only / extra head detail. Do not lower character
|
||||
fidelity silently.
|
||||
- **Pose composition is rest-relative (resolved 2026-11-05):** Godot's `set_bone_pose_rotation`
|
||||
offsets from the bone rest, so the correct swing around a world axis is
|
||||
`rest * Quaternion(rest.inverse() * axis, angle)`. For the old 180° rest this reduces to the
|
||||
legacy `rest * Quaternion(axis, angle)`, so the 13-core fallback is unchanged (commit `8b1bf73`).
|
||||
- **One character source, one fallback:** no new NPC source without an ADR. MPFB body work stays
|
||||
closed, the Stage-1 proportion warp is not reopened, and the procedural `npc_base` + MakeHuman
|
||||
head stay tested until the Quaternius base passes its gate.
|
||||
|
||||
## Gotchas / Known Issues
|
||||
|
||||
- **Two character builders — do not confuse them.** `tools/blender_assets.py` is the real builder
|
||||
@@ -115,6 +147,44 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
out of a portrait frame (park the rest of the crowd away instead).
|
||||
- Blender: `bisect_plane` and thin bevels leave zero-area triangles (Godot "non-finite normal in
|
||||
LOD generation"); `_clean()` + a weld after bisect fixes them and the audit rejects them.
|
||||
- Quaternius arm-down bake (working recipe, `tmp/npc-research/bake_armdown_v5.py`): pose-rotate
|
||||
`UpperArm.L/R` −90° local X → read the pose matrices → apply the Armature modifier → **reset the
|
||||
pose to identity** → set each arm bone's rest matrix from its pose matrix → re-add the modifier →
|
||||
export `export_apply=False`. Missing the pose reset double-applies the arm-down (mesh ±0.53
|
||||
instead of ±0.23). `armature_apply` in Blender 5.2 is broken (deforms the mesh but does not bake
|
||||
the rest). The canonical base must be **join-only** (`build_base_minimal.py`: no `remove_doubles`
|
||||
weld, no scale/ground transform, keep the Armature modifiers) — the weld and the scale/ground
|
||||
transform corrupt the skin weights (arm poses to ±0.51 instead of −0.23).
|
||||
- Quaternius `NpcAvatar` wiring (commits `2a6f29f`, `cd285b3`): `RIG_BONES` +
|
||||
`NpcAvatar.bone_name(joint, side)` hold the **only** side mapping — the Quaternius labels `L`/`R`
|
||||
opposite the project, so proxy side 0 (`-x`) drives `UpperLeg.R`/`UpperArm.R` while side 1 drives
|
||||
the `.L` bones. The arm/elbow swing axis is `Vector3.LEFT` (legs stay `RIGHT`). The hand proxy is
|
||||
`global_wrist − global_elbow` (subtracting the parent-relative `elbow.position` misplaces
|
||||
handheld gear). Old 13-core names resolve through the same table.
|
||||
- **Bone posing must write the global pose** (`NpcAvatar._swing_bone`): `reset_bone_pose()` then
|
||||
`set_bone_global_pose(Transform3D(Basis(swing * global_rot), current_origin))`, conjugated with
|
||||
`get_bone_global_rest()`. Both rejected alternatives fail: the rest-relative
|
||||
`set_bone_pose_rotation(rest * Quaternion(rest⁻¹·axis, angle))` swings about the parent-relative
|
||||
rest (the glTF importer already folded the axis conversion into it) and splayed the Quaternius
|
||||
arms sideways; the plain `rest⁻¹ * swing * rest` conjugation flips the limbs 180°. Verified by
|
||||
measuring `get_bone_global_pose()` after the call — posed bone axis matches the proxy pivot at
|
||||
dot 1.000 on the Quaternius and the procedural rigs render unchanged.
|
||||
Two caveats: children need the parent's swing passed in (the write is absolute), and
|
||||
`set_bone_global_pose()` is a **no-op outside the scene tree**, so the headless runner (tests run
|
||||
in `_initialize()`) cannot assert on it.
|
||||
- **The Quaternius source UV map is degenerate** — every loop at (0,0), flat-colour materials, no
|
||||
atlas. The AO/texture bake silently produces a 100 % black map, which tints every AO'd slot
|
||||
(skin, hair, accessory) black in Godot: a black leg is the symptom. `build_base_minimal.py` now
|
||||
Smart-UV-unwraps and repacks the layout (texel density 0 → 73–74 px/m on 256²), and the base
|
||||
pipeline is two explicit steps with distinct files: `build_base_minimal.py`
|
||||
(`quaternius/Casual_Hoodie.glb` → `quaternius_tpose.glb`, joins without welding, renames the
|
||||
materials to the five tint slots, unwraps) then `bake_armdown_v5.py` (`quaternius_tpose.glb` →
|
||||
`quaternius_base.glb` + `assets/models/blender_src/npc_casual.blend`). Re-running v5 on its own
|
||||
output double-bakes the rest.
|
||||
- Hand gear must be self-contained: the camera carried a 0.42 m neck-strap loop that floats when the
|
||||
item is parented to the wrist. Hand items get a short wrist strap, not a neck strap. The backpack
|
||||
straps were authored for the wider procedural chest and floated ~5 cm proud of the Quaternius
|
||||
torso; the chest points now sit at x 0.12 / Godot z 0.15, which fits both.
|
||||
- Image models have per-model size floors/caps (Seedream needs 1920+; `GEN_SIZE=2048`, do not
|
||||
lower; gpt-5-image-mini caps lower). `generate_image` applies model-specific sizing; concept
|
||||
generation defaults to `--request-size 1536`.
|
||||
@@ -188,20 +258,32 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
## Open Questions
|
||||
|
||||
- Whether the responder avatar should be playable/controllable or purely scripted.
|
||||
- Localization target languages beyond English.
|
||||
- macOS notarization approach for broad distribution.
|
||||
- Quaternius scope: full 12-role swap, crowd-only, or extra head detail (owner decision at 0.9.9).
|
||||
- Localization target languages beyond English (0.11.0/0.12.0).
|
||||
- macOS notarization approach for broad distribution (0.12.0).
|
||||
- Fourth-venue choice for 0.11.0 (candidate: TCL Chinese Theatre forecourt).
|
||||
|
||||
## Current State
|
||||
|
||||
The playable build covers the three venues, tutorial/campaign/checkpoints, the 0.9.3–0.9.6
|
||||
polish/texture/integration passes and the Blender + realism asset pipeline (see `DONE.md` and
|
||||
`CHANGELOG.md`). The 12-character library (realistic face + four crowd variants) and the
|
||||
Comedy Store map rebuild are complete. **Priority (2026-11-05, owner):** 1) the **Quaternius NPC
|
||||
base migration** (bake the CC0 Quaternius character to the runtime contract), 2) the 3D model and
|
||||
map improvements, 3) ship it all together in the **0.10.0 minor**. The 0.9.7/0.9.8
|
||||
character patches are published; `TODO.md` is the priority source. The active implementation is
|
||||
the Quaternius base migration (ADR-0017); planning/docs were rewritten and `tmp/` cleaned on
|
||||
2026-11-05, but no character GLB has been rebuilt yet. Test counts as of 2026-11-05: Python
|
||||
167/167 (last run), Godot 277/277 (last run before the docs-only pivot), `check_docs` OK.
|
||||
v0.9.8 is published. The build covers the three venues, the shift loop, tutorial/campaign/
|
||||
checkpoints, the 0.9.3–0.9.8 polish/texture/integration/character passes and the Blender +
|
||||
realism asset pipeline (see `DONE.md` and `CHANGELOG.md`). The 12-character procedural library
|
||||
(realistic face + four crowd variants) and the Comedy Store map rebuild are complete.
|
||||
|
||||
**Planning pivot (2026-11-05, owner):** the character and map work was re-sequenced into
|
||||
patches (`0.9.9`–`0.9.12`) with `0.10.0` as the public-readiness minor. The gap analysis and the
|
||||
three release roadmaps now live in `PLAN.md`; `TODO.md` is the executable task list. ADR-0017
|
||||
Stage 1 (proportion warp) is dead; ADR-0018 option 2 (62-joint runtime) is accepted.
|
||||
|
||||
**Quaternius pilot (2026-11-05, commits `cd285b3`, `fb3e75a`, `90f189a`):** `npc_casual` ships as
|
||||
the Quaternius base. It renders arm-down, carries the five tint slots, plays the Idle/Walk/Sit pose
|
||||
code on the 62-joint rig, and the procedural rigs are visually unchanged. Verified: `inspect`
|
||||
(no issues), `audit` OK (6,206 tris / 12,988 verts, 62 bones), `fit` head 0.201 m, texel density
|
||||
73–74 px/m, `npc_proportions` measured, 24-NPC budget smoke OK (168 meshes, 162,304 tris,
|
||||
0.82 ms avg frame). The face/silhouette gate sheet and the three options are in
|
||||
`docs/screens/npc-face-gate/`; the **owner decision is the only thing left before the 0.9.9
|
||||
rollout**.
|
||||
|
||||
Test counts as of 2026-11-05: Python 167/167, Godot 278/278, `check_docs` OK.
|
||||
|
||||
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
|
||||
@@ -16,22 +16,91 @@ The game is about **judgment, communication, access verification, and proportion
|
||||
|
||||
## Development Priorities
|
||||
|
||||
The active planning order is set in [`TODO.md`](TODO.md) and in `AGENTS.md`:
|
||||
The active task order is in [`TODO.md`](TODO.md); the gap analysis and the release roadmap are
|
||||
below. The current order is:
|
||||
|
||||
1. **Quaternius NPC base (unversioned).** Use the CC0 **Quaternius Ultimate Modular
|
||||
Characters** as the primary NPC mesh and animation source. Bake a canonical Quaternius base
|
||||
onto the runtime `npc_base` contract (13 core bones, five tint slots, 24,000/24,000) so the
|
||||
existing gear, `NpcAvatar` and gameplay poses keep working. If the proportion warp fails the
|
||||
silhouette gate, stop and decide on the 62-joint source rig in a new ADR. See
|
||||
[ADR-0017](docs/adr/0017-quaternius-npc-base.md) and
|
||||
[`docs/npc-model-sourcing.md`](docs/npc-model-sourcing.md). MakeHuman/MPFB body work is closed.
|
||||
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.
|
||||
1. **0.9.9–0.9.12 patches.** Finish the character base (the accepted Quaternius 62-joint runtime),
|
||||
roll it out to the 12 roles, close the scheduled venue/prop/lighting polish, and verify
|
||||
performance, Web and accessibility.
|
||||
2. **0.10.0 minor.** Public readiness: external playtests, balance acceptance, legal/storefront
|
||||
approval, the platform matrix and the documentation/picture lock.
|
||||
3. **0.10.x patches → 0.11.0.** Content and systems depth: live body-camera replay, a fourth
|
||||
venue and the localization foundation.
|
||||
4. **0.12.0 → 1.0.0.** Platform/distribution, content complete, release candidates and the stable
|
||||
release.
|
||||
|
||||
Versions are assigned only when a track's scope is accepted into a release (see
|
||||
[Release Roadmap](#release-roadmap)). Unversioned tracks — the 3D review evidence base,
|
||||
maintenance and the Gaussian-splatting study — do not receive a placeholder version.
|
||||
|
||||
## Plan Evaluation (2026-11-05)
|
||||
|
||||
The game plan itself is sound and largely built: three venues, the escalation ladder, access
|
||||
artifacts, the radio/report system, the shift review and the campaign loop all exist and are
|
||||
tested. What needed fixing was the **delivery plan**, not the design.
|
||||
|
||||
- **Character sourcing churned.** The project moved procedural → MakeHuman → Quaternius, and the
|
||||
Quaternius Stage-1 warp failed its own gate. That leaves a correct decision (ADR-0018) with a
|
||||
half-finished implementation. The plan now makes the rig correction one small version, gates the
|
||||
roster behind a face/readability decision, and keeps exactly one fallback.
|
||||
- **One milestone carried too much.** The old 0.10.0 mixed character work, map work and every
|
||||
public-release gate, so one blocked export blocked the release. Character and venue polish are
|
||||
now patches; 0.10.0 is only the release gate.
|
||||
- **Unversioned work had no deadline.** The 3D review backlog could grow without bound. It is now
|
||||
capped: the ranked venue/prop/lighting items are scheduled into 0.9.11, the budget checks into
|
||||
0.9.12, and everything else is an explicit accept or deferred. Gaussian splatting stays
|
||||
unscheduled because no product need justifies its risk yet.
|
||||
- **Legal, signing and storefront work is release work, not polish.** It now has named checkboxes
|
||||
in 0.10.0 and 1.0.0 instead of living in prose.
|
||||
- **What is deliberately not planned:** new genres or mechanics, a live-service model, or a
|
||||
runtime splat renderer. The de-escalation-first, rare-emergency design stays frozen.
|
||||
|
||||
Feasibility: the remaining character work is small in geometry terms (6.2k triangles against a
|
||||
24k cap) and larger in rig/export terms; the venue work is polish; most release gates are
|
||||
evidence gathering rather than engineering. The main schedule risk is external review and
|
||||
approval, not code.
|
||||
|
||||
## Character Creation Re-Evaluation (2026-11-05)
|
||||
|
||||
**Where it stands.** The shipped library is 12 procedural Blender variants (4.7k–5.8k triangles
|
||||
each) with a sculpted face, five tint slots and 18 gear pieces. The owner then chose the CC0
|
||||
**Quaternius Ultimate Modular Characters** as the primary source. Stage 1 (warp the Quaternius
|
||||
mesh onto the 13-core contract) failed its silhouette gate — arm ×1.63, leg ×0.74, torso ×1.61,
|
||||
judged distorted. [ADR-0018](docs/adr/0018-quaternius-stage2-runtime-rig.md) therefore adopts the
|
||||
source **62-joint rig at runtime**: no warp, source LOD (3,199 verts / 6,206 tris), source
|
||||
Idle/Walk clips, and gear pivots re-derived from the Quaternius skeleton.
|
||||
|
||||
**What is already built.** The canonical `quaternius_base.glb` (welded, grounded, arm-down rest),
|
||||
the 62→13 joint map, the material→five-slot map, the clip inventory (24 clips; Idle and Walk in
|
||||
scope) and the `NpcAvatar` joint-name mapping with rest-derived gear pivots.
|
||||
|
||||
**What actually blocks it.** Two technical items about the export and the pose math, not the art:
|
||||
|
||||
1. the arm-down GLB renders in A-pose in Godot because the bake writes wrong inverse bind
|
||||
matrices; and
|
||||
2. `_pose_bone()` composes rotations as `rest * Quaternion(axis, angle)`, which only cancels for
|
||||
the old 180° rest, not the Quaternius ~76° rest.
|
||||
|
||||
**Re-evaluation findings.**
|
||||
|
||||
- Triangle budget is not the problem (6.2k against a 24k cap). Do not decimate the source.
|
||||
- **Face fidelity is the real risk.** The 0.9.7 library has a sculpted realistic face; the
|
||||
Quaternius head is low-poly. A blanket swap would regress every close-up. The plan therefore
|
||||
makes the owner choose at 0.9.9 between (a) a full swap, (b) Quaternius for crowd roles with the
|
||||
procedural library for named roles, or (c) extra head subdivision/hair cards on the Quaternius
|
||||
head.
|
||||
- **Animation** becomes clip-driven, which removes the hand-built pose composition as the main
|
||||
path. Poses the clips do not cover (seated, raise-hand, head turn) must be decided explicitly
|
||||
rather than patched per scene.
|
||||
- **One fallback, no third source.** The procedural `npc_base` and the MakeHuman head stay tested
|
||||
until the new base passes the gate. MPFB body work stays closed and the Stage-1 warp is not
|
||||
reopened. A new character source needs a new ADR.
|
||||
- **Sequence matters.** Fix the rig first (0.9.9), then the roster (0.9.10). Rolling a broken rig
|
||||
across 12 variants would waste the whole pass.
|
||||
|
||||
**Reversal condition.** If the skin cannot be exported correctly without a new exporter path, or
|
||||
if the owner rejects the low-poly face in every option, the plan falls back to the procedural
|
||||
library and retires Quaternius with a recorded ADR. That decision is made at 0.9.9, not later.
|
||||
|
||||
## Core Gameplay Loop
|
||||
|
||||
@@ -171,7 +240,7 @@ The action wheel prioritizes de-escalation: Stop, Look, Point, No, Ask, Inspect,
|
||||
|
||||
The visual direction is readable stylized realism:
|
||||
|
||||
- **Character direction:** 12 skinned NPC variants, 18 gear pieces and 20 street/venue props. The active character track is the Quaternius base migration ([ADR-0017](docs/adr/0017-quaternius-npc-base.md)): use the CC0 Quaternius mesh/animation source, bake it to the runtime contract, and retire the bespoke procedural body work.
|
||||
- **Character direction:** 12 skinned NPC variants, 18 gear pieces and 20 street/venue props. The active character track is the Quaternius base at runtime ([ADR-0018](docs/adr/0018-quaternius-stage2-runtime-rig.md)): the CC0 source mesh keeps its proportions on the 62-joint rig, Idle/Walk run as clips, and the procedural library stays as the tested fallback until the face/silhouette gate passes. See [Character Creation Re-Evaluation](#character-creation-re-evaluation-2026-11-05).
|
||||
- **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.
|
||||
@@ -198,6 +267,106 @@ Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [AD
|
||||
|
||||
`config/version` in `project.godot` is the single version source (currently `0.9.8`). 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.
|
||||
|
||||
## Gap Analysis
|
||||
|
||||
**Published baseline:** v0.9.8 (2026-11-04) — three playable venues, tutorial/campaign/checkpoint
|
||||
resume, the access/radio/escalation loop, shift review, the 12-character Blender library, 277
|
||||
Godot + 162 Python tests, a 21/21 screen-design judge and Compatibility/Web/Desktop HQ builds.
|
||||
|
||||
**Current source:** 0.9.8. The 0.10.0-rc.1 package built on 2026-11-03 is stale and is superseded
|
||||
by the patch sequence below; it is not a published release.
|
||||
|
||||
| Area | Remaining gap | Milestone |
|
||||
| --- | --- | --- |
|
||||
| Character base | Quaternius arm-down skin export, rest-relative pose composition, face/silhouette acceptance | 0.9.9 |
|
||||
| Character roster | 12-role mapping, modular outfits, gear fit, AO/texels, budget evidence | 0.9.10 |
|
||||
| Venue and asset polish | Ranked prop/foliage/lighting/set-dressing fixes, collider verification | 0.9.11 |
|
||||
| Performance and Web | Frame/memory/load evidence after the content changes, LOD/impostor verification | 0.9.12 |
|
||||
| Onboarding and balance | External first-time-player evidence, balance sweeps/tuning, content lock | 0.10.0 |
|
||||
| Accessibility and localization | Acceptance pass, caption coverage, pseudo-locale/length checks | 0.9.12 / 0.10.0 |
|
||||
| QA and release engineering | Full smoke matrix, clean build/checksum verification, target-matrix launches | 0.10.0 |
|
||||
| Legal, signing, storefront | macOS signing/notarization, AI-provider rights, storefront/rating approval, privacy sign-off | 0.10.0 / 0.12.0 |
|
||||
| Systems and content depth | Live incident replay, fourth venue, localization foundation | 0.11.0 |
|
||||
| Distribution | Signed storefront builds, frozen save contract, launch languages, hardware matrix | 0.12.0 |
|
||||
| Stable release | Content complete, accessibility audit, release candidates, 1.0.0 decision | 0.13.0 / 1.0.0 |
|
||||
| Future | More venues/seasonal content, image-to-mesh props, other deferred ideas | Unscheduled |
|
||||
|
||||
The 0.9.3 UI, 0.9.4 animation, 0.9.5 texture, 0.9.6 integration, 0.9.7 character development and
|
||||
0.9.8 deformation fixes are complete in [`DONE.md`](DONE.md); they are not remaining gaps. The
|
||||
2026-11-01 documentation audit is also closed.
|
||||
|
||||
## Release Roadmap
|
||||
|
||||
Every version below follows the release procedure in `AGENTS.md`. Patch versions contain fixes and
|
||||
polish; minor versions add user-facing features; the major version is the stable release with a
|
||||
compatibility promise. Each version's executable checklist is in [`TODO.md`](TODO.md).
|
||||
|
||||
### Roadmap to 0.10.0
|
||||
|
||||
The goal of this line is to finish the character and venue work and clear every public-release
|
||||
gate. The character track is versioned rather than unversioned so a blocked rig cannot hold the
|
||||
whole release.
|
||||
|
||||
| Version | Type | Scope | Exit criterion |
|
||||
| --- | --- | --- | --- |
|
||||
| 0.9.9 | patch | Quaternius pilot: correct skin export, rest-relative pose math, Idle/Walk verification, owner face/silhouette gate, one-role (or crowd-only) rollout | A Quaternius NPC renders and animates correctly and ships behind a tested procedural fallback |
|
||||
| 0.9.10 | patch | 12-role roster, modular outfits, five slots, AO/texels, gear fit, 12×6 baseline | The full roster passes contract, gear, budget and owner-baseline gates |
|
||||
| 0.9.11 | patch | Scheduled 3D review fixes: props/foliage, gear silhouette, venue shape language, lighting, set dressing, colliders | Every scheduled P1/P2 item is fixed or accepted with a measurement |
|
||||
| 0.9.12 | patch | Performance/Web/LOD/memory verification plus accessibility and pseudo-locale checks | No regression against the pre-character baselines |
|
||||
| 0.10.0 | minor | Public readiness: external playtests, balance acceptance, legal/storefront/signing decisions, platform matrix, docs and picture lock | 0.10.0 is content-locked, externally played, legally reviewed, documented and reproducibly built |
|
||||
|
||||
### Roadmap to 0.11.0
|
||||
|
||||
This line turns a stable 0.10.x into a deeper game: the incident timeline becomes a live replay, a
|
||||
fourth venue enters the campaign, and the text catalogue becomes a real localization pipeline.
|
||||
|
||||
| Version | Type | Scope | Exit criterion |
|
||||
| --- | --- | --- | --- |
|
||||
| 0.10.1 | patch | Onboarding and accessibility issues deferred from the 0.10.0 playtests | First-hour and accessibility defects closed |
|
||||
| 0.10.2 | patch | Balance/fairness sweeps, review-text and difficulty-progression tuning | Every balance-acceptance cell is in target or explicitly accepted |
|
||||
| 0.10.3 | patch | Save/checkpoint edge cases, gamepad/remap, pause/resume, small quality-of-life fixes | No known P1 stability defect remains |
|
||||
| 0.11.0 | minor | Live body-camera replay, fourth venue (candidate: TCL Chinese Theatre forecourt), localization foundation with one secondary language | The new systems ship without regressing performance, accessibility or balance |
|
||||
|
||||
### Roadmap to 1.0.0
|
||||
|
||||
This line is about distribution, contracts and completeness: signed builds on the launch stores, a
|
||||
frozen save format, finished content and release candidates.
|
||||
|
||||
| Version | Type | Scope | Exit criterion |
|
||||
| --- | --- | --- | --- |
|
||||
| 0.11.1 | patch | Replay/venue/localization fixes from wider play | No P1 defect in the 0.11.0 feature set |
|
||||
| 0.11.2 | patch | New-venue balance, campaign pacing and content readability | The 0.11.x line is stable enough to freeze its contracts |
|
||||
| 0.12.0 | minor | macOS signing/notarization, storefront packages/metadata/ratings, save-format v2 freeze, hardware matrix, launch languages | Signed, storefront-ready builds exist on every launch platform |
|
||||
| 0.12.1 | patch | Signing, installer, metadata and save-migration fixes from storefront dry runs | Every storefront build installs, launches and migrates cleanly |
|
||||
| 0.13.0 | minor | Content complete: all planned venues/events/dialogue, audio decision, final tutorial, accessibility audit, balance freeze | All planned content is implemented and frozen |
|
||||
| 0.13.1 | patch | Release candidate 1: full smoke/performance/save/accessibility matrix | A candidate passes every gate except final approval |
|
||||
| 0.13.2 | patch | Release candidate 2: verify fixes, freeze release notes/credits/privacy/known limitations | The final candidate is reproducible and documented |
|
||||
| 1.0.0 | major | Stable release: legal/storefront/rating approval, hardware/accessibility/localization sign-off, no P0/P1, publish on every platform | 1.0.0 is published with verified checksums and no open blocker |
|
||||
|
||||
## Roadmap Dependencies and Risks
|
||||
|
||||
- Content volume is the largest schedule risk; tutorial and campaign work must not outrun the
|
||||
event/dialogue matrix.
|
||||
- The vision judge is a dev-time aid with an external key and budget; CI must never make it a hard
|
||||
gate.
|
||||
- Godot engine changes can alter lighting and imports; do not combine an engine baseline move with
|
||||
a content or feature milestone.
|
||||
- Signed macOS builds, AI provenance and storefront approval are release blockers, not polish.
|
||||
- The character pipeline is the current critical path. The rig/export fix must be proven on one
|
||||
variant before the roster rollout; otherwise 0.9.10 multiplies a defect.
|
||||
- Photo-derived map assets add a 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; keep it offline,
|
||||
version-pinned and outside CI, and never make a runtime splat renderer a release dependency.
|
||||
- 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. Cap it with the scorecard ranking: P0/P1 only, one
|
||||
asset family per commit, and an explicit accept for everything else.
|
||||
- Keep the de-escalation-first rules and rare-emergency design unchanged while adding progression,
|
||||
difficulty and feedback.
|
||||
|
||||
## Design Direction
|
||||
|
||||
The game should feel like a tense workplace simulation with occasional crisis moments. The most satisfying outcome is not overpowering an NPC; it is recognizing the situation early and resolving it calmly.
|
||||
@@ -208,4 +377,5 @@ The game should feel like a tense workplace simulation with occasional crisis mo
|
||||
|
||||
- A rare Bob Dobbs cameo (Easter-egg appearance; placement note in `MEMORY.md`).
|
||||
- Specific unusual NPC behaviors, such as slapping their own face, using hands as scissors, or interacting with props unexpectedly.
|
||||
- Live 3D body-camera replay; additional venues or seasonal events; real voice/dialogue audio; an image-to-mesh prop pipeline.
|
||||
- A fifth venue or seasonal events after the 0.11.0 fourth venue proves the venue pipeline.
|
||||
- Real voice/dialogue audio if the 0.13.0 audio decision does not fund it; an image-to-mesh prop pipeline remains unscheduled.
|
||||
@@ -8,11 +8,12 @@ The central design goal is simple:
|
||||
|
||||
## Development Priorities
|
||||
|
||||
The active planning order lives in [`TODO.md`](TODO.md) and [`AGENTS.md`](AGENTS.md):
|
||||
The active planning order lives in [`PLAN.md`](PLAN.md) (gap analysis and release roadmap), [`TODO.md`](TODO.md) (executable checklist) and [`AGENTS.md`](AGENTS.md):
|
||||
|
||||
1. **3D model and local map re-evaluation** — score every shipped asset, then evaluate licensed OpenStreetMap/3D data, reference-derived photoreal facades, AI-assisted composition and a Gaussian-splatting development evaluation.
|
||||
2. **Quaternius NPC base** — use the CC0 Quaternius Ultimate Modular character as the primary mesh and animation source, baked to the existing runtime contract. Source evaluation and the licence decision are in [`docs/npc-model-sourcing.md`](docs/npc-model-sourcing.md) and [ADR-0017](docs/adr/0017-quaternius-npc-base.md).
|
||||
3. **Versioned release hardening** — the 0.10.0 release candidate and later semantic versions follow the unversioned development tracks.
|
||||
1. **`0.9.9`–`0.9.12` patches** — finish the Quaternius NPC base on the accepted 62-joint runtime, roll it out to the 12 roles, close the scheduled venue/prop/lighting polish, then verify performance, Web and accessibility.
|
||||
2. **`0.10.0` minor** — public readiness: external playtests, balance acceptance, legal/storefront/signing decisions, the platform matrix and the documentation/picture lock.
|
||||
3. **`0.10.x` → `0.11.0`** — live incident replay, a fourth venue and the localization foundation.
|
||||
4. **`0.12.0` → `1.0.0`** — signed storefront builds, a frozen save contract, content complete, release candidates and the stable release.
|
||||
|
||||
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.
|
||||
|
||||
@@ -33,7 +34,9 @@ Versions are assigned only when a track's scope is accepted into a release. Ever
|
||||
> 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.8** — the character-development patch: the
|
||||
> **Latest published: v0.9.9** — the character-roster patch: all twelve NPCs now
|
||||
> use the CC0 Quaternius base with the five tint slots, role gear and baked maps.
|
||||
> Superseded: v0.9.8, the character-development patch: the
|
||||
> 12-character library, realistic face pass, four crowd-variety variants and the
|
||||
> wrist/hip/knee deformation fixes. Previous 0.9.x patches added the
|
||||
> Hollywood/Joke Shop maps, UI/accessibility polish, NPC visual optimization,
|
||||
@@ -95,13 +98,14 @@ Every capture is rendered from the current build by [`tools/check_screenshots.sh
|
||||
|
||||
### 3D asset progress
|
||||
|
||||
The current character library is Blender-authored and audited before use. The active character
|
||||
migration moves the NPC base to the CC0 Quaternius modular character; the sheets below show the
|
||||
The character roster is built from the CC0 Quaternius bases (ADR-0017/ADR-0018) and audited
|
||||
before use; the procedural library and the MakeHuman head stay as the tested fallback. The
|
||||
sheets below show the
|
||||
current library and will be replaced after the 12 × 6 baseline review.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
|  | **Character library.** Eight skinned variants (4.5–5.5k triangles each) with five tint slots, attach-point gear and baked maps. |
|
||||
|  | **Character library.** Twelve role-dressed variants (4.5–6.4k triangles each) on three CC0 Quaternius bases, with five tint slots, attach-point gear and baked maps. |
|
||||
|  | **Street and venue props.** Twenty library props with colliders, navigation avoidance and colour atlases. |
|
||||
|  | **Neutral baseline.** 8 characters × 6 views (front, three-quarter, profile, back, full body, crowd) used for the 0.9.4 pass and the current re-evaluation. |
|
||||
|  | **Texture refinement.** 128 → 256 px NPC maps and a 4× fabric weave, shown before/after on the same camera. |
|
||||
@@ -132,7 +136,7 @@ decisions are recorded as ADRs in [`docs/adr/`](docs/adr/).
|
||||
## Tech Stack
|
||||
|
||||
- **Engine:** Godot 4.7.2 · **Language:** GDScript
|
||||
- **NPCs:** Blender-authored skinned library (`assets/models/blender/`) with procedural fallbacks; Quaternius CC0 base migration in progress; lightweight FSMs on `NpcProfile`
|
||||
- **NPCs:** CC0 Quaternius skinned library (`assets/models/blender/`, ADR-0018) with a tested procedural fallback; lightweight FSMs on `NpcProfile`
|
||||
- **World:** procedural `WorldBuilder` + generated prop models
|
||||
- **Art:** v2 skyline/surface textures in `assets/textures/ai/game_ready/v2/picks/`; UI/scene art in `assets/images/`
|
||||
- **Asset tooling:** `asset_pipeline.py`, `model_pipeline.py`, `npc_pipeline.py`, `blender_assets.py`, `scene_concepts.py`, `screen_designs.py`, `check_screenshots.sh`
|
||||
@@ -191,9 +195,9 @@ v0.9.8 is the current source version: the character-development patch with the
|
||||
and save-migration/release hardening. It sits on the 0.9.6 integration
|
||||
checkpoint, 0.9.5 texture refinement, 0.9.4 NPC visual optimization, 0.9.3
|
||||
UI/accessibility polish, 0.9.2 Hollywood/Joke Shop maps and the 0.9.x engine
|
||||
baseline. The active priorities are the Quaternius NPC base migration and the 3D
|
||||
model/map re-evaluation; the 0.10.0 release
|
||||
candidate follows those tracks. See
|
||||
baseline. The active work is the `0.9.9`–`0.9.12` patch line (Quaternius NPC
|
||||
base, roster, venue/asset polish, performance) followed by the `0.10.0`
|
||||
public-readiness minor. 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
|
||||
[`docs/release-qa.md`](docs/release-qa.md).
|
||||
|
||||
@@ -1,127 +1,43 @@
|
||||
# TODO
|
||||
|
||||
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.
|
||||
Active task list. Work is grouped by milestone and workstream; every item is small enough to
|
||||
complete and verify on its own. The product plan, gap analysis and release roadmap live in
|
||||
[`PLAN.md`](PLAN.md); durable decisions and gotchas live in [`MEMORY.md`](MEMORY.md);
|
||||
[`AGENTS.md`](AGENTS.md) defines the workflow.
|
||||
|
||||
**Priority order (2026-11-05, owner):** 1) **Quaternius NPC base migration** — use the CC0
|
||||
Quaternius character as the mesh/animation source and bake it to the runtime contract; 2) the 3D
|
||||
model and local map improvements; 3) ship it together in the **0.10.0 minor**. The 0.9.7/0.9.8
|
||||
character patches are published. See `AGENTS.md` §Priority and Versioning and §Local Blender skills.
|
||||
**Current state (2026-11-05):** v0.9.8 is published. Three venues, the shift loop, the
|
||||
tutorial/campaign, the access/escalation system and the 12-character procedural library are
|
||||
shipped. The character plan changed twice and landed on the CC0 Quaternius base; the owner
|
||||
accepted the 62-joint runtime rig ([ADR-0018](docs/adr/0018-quaternius-stage2-runtime-rig.md)),
|
||||
but the base does not yet render or animate correctly in Godot.
|
||||
|
||||
**Roadmap (detail in [`PLAN.md`](PLAN.md#release-roadmap)):**
|
||||
|
||||
| Version | Type | Scope |
|
||||
| --- | --- | --- |
|
||||
| `0.9.9` | patch | Quaternius pilot base: rig/export fix, pose composition, face/silhouette gate |
|
||||
| `0.9.10` | patch | 12-role roster on the approved base, gear fit, budget evidence |
|
||||
| `0.9.11` | patch | Scheduled venue, prop, foliage, lighting and set-dressing polish |
|
||||
| `0.9.12` | patch | Performance/Web/accessibility verification before the minor |
|
||||
| `0.10.0` | minor | Public-readiness release: playtests, balance, legal, platform matrix, docs |
|
||||
| `0.10.1`–`0.10.3` | patch | Onboarding, balance and stability follow-ups |
|
||||
| `0.11.0` | minor | Live body-camera replay, fourth venue, localization foundation |
|
||||
| `0.12.0` | minor | Signed/storefront builds, frozen save contract, launch languages |
|
||||
| `0.13.0` | minor | Content complete and feature freeze for 1.0.0 |
|
||||
| `0.13.1`–`0.13.2` | patch | Release candidates |
|
||||
| `1.0.0` | major | Stable release with a documented compatibility promise |
|
||||
|
||||
**Priority now:** the `0.9.9` rig-and-export correction. Do not start the 12-role roster
|
||||
(`0.9.10`) before the single-variant pilot renders, animates and passes the owner face gate.
|
||||
|
||||
The unversioned tracks (3D review evidence, maintenance, Gaussian splatting) stay at the top of
|
||||
this file because their work is not release-scoped; each milestone below is versioned and must
|
||||
follow the release procedure in `AGENTS.md`.
|
||||
|
||||
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.
|
||||
|
||||
## Quaternius NPC base migration (top priority)
|
||||
|
||||
Goal: replace the generator-first NPC base with the CC0 **Quaternius Ultimate Modular
|
||||
Characters** as the primary mesh and animation source, while keeping one committed
|
||||
`npc_base` skinned mesh, the five tint slots and a runtime contract that Godot, gear
|
||||
and tests can consume. The detailed decision is [ADR-0017](docs/adr/0017-quaternius-npc-base.md);
|
||||
it supersedes the generator-first sourcing decision in ADR-0015.
|
||||
|
||||
**Why:** the research pack already has a coherent low-poly human silhouette, 21 modular
|
||||
outfits and 24 animations. The current procedural body needs a separate head, hands, neck
|
||||
and shoe pipeline to look human; using Quaternius as the base removes that bespoke body work.
|
||||
|
||||
**Default path (Stage 1):** retarget and proportion-warp the Quaternius mesh onto the
|
||||
existing 13-core contract, so `NpcAvatar`, gear attach points and the current animation
|
||||
composition keep working. The 62-joint Quaternius rig and its 24 clips are an authoring
|
||||
source, not a new runtime dependency.
|
||||
|
||||
**Stage 2 gate:** if the proportion warp cannot preserve the Quaternius silhouette and
|
||||
deformation at an acceptable level, stop the rollout and write a new ADR for adopting the
|
||||
62-joint rig at runtime. Do not ship a hybrid rig or silently change `NpcAvatar`.
|
||||
|
||||
**Exit:** one committed Quaternius-derived source GLB produces all 12 variants inside
|
||||
24,000 tris / 24,000 glTF vertices, with five slots, gear fit, Idle/Walk deformation,
|
||||
`audit`/`fit`/`inspect` clean, the 24-NPC budget measured, and an owner-approved 12 × 6
|
||||
baseline sheet.
|
||||
|
||||
### 1. Intake and canonical base
|
||||
|
||||
- [x] Keep the three researched CC0 GLBs (`Casual`, `Casual_Hoodie`, `Worker`) plus the
|
||||
licence/mirror notes in `tmp/npc-research/quaternius/`; record the source URL,
|
||||
licence, SHA-256 and download date in `docs/npc-model-sourcing.md`.
|
||||
Verified: the three GLBs are in `tmp/npc-research/quaternius/`, SHA-256 re-hash matches the
|
||||
recorded table, the mirror HTML/README are retained next to the folder, and the download
|
||||
date (2026-11-03) is now recorded.
|
||||
- [ ] Inventory the four meshes, 62 joints, 24 clips and material names in each GLB; map
|
||||
Quaternius materials to the five project slots (`skin`, `cloth`, `pants`, `accessory`,
|
||||
`hair`).
|
||||
- [ ] Choose the initial outfit scope: build the three full GLBs first; only use the 21-part
|
||||
modular builder if the three looks cannot cover the 12 roles.
|
||||
- [ ] Build a canonical `quaternius_base.glb`: one welded, triangulated, grounded 1.81 m
|
||||
humanoid with front on Blender −Y, no animation, and a documented bone/part inventory.
|
||||
|
||||
### 2. Contract bake and animation
|
||||
|
||||
- [ ] Author the 62-joint → 13-core joint map, including optional finger/face extras if
|
||||
they survive within 24,000 vertices; record it in `docs/npc-model-sourcing.md`.
|
||||
- [ ] Bake the rest pose and proportion warp to the contract hip, knee, shoulder, elbow,
|
||||
wrist and head positions. Measure the warp at every joint and record the silhouette
|
||||
deltas; do not accept a distorted deltoid or shortened arm.
|
||||
- [ ] Skin the Quaternius mesh and verify Idle, Walk, seated and arm-down poses. The old
|
||||
MPFB/arm-retarget warps are not a reference; judge the Quaternius result directly.
|
||||
- [ ] Retarget Idle and Walk from the 24 Quaternius clips to the contract rig; discard
|
||||
combat, gun and sword clips. Verify looping, foot contact and frame rate in Godot.
|
||||
- [ ] Map the five slots, unwrap per slot, bake 256² AO/albedo/normal/ORM maps and check
|
||||
texel density before any roster build.
|
||||
|
||||
### 3. Silhouette gate
|
||||
|
||||
- [ ] Render the base at 1.5 m and 1.85 m in front, three-quarter and profile; compare
|
||||
against the Quaternius source and the current `npc_casual`. Use the vision judge for a
|
||||
written verdict, but the owner decides.
|
||||
- [ ] Confirm gear attach points (head, hand, back, chest) and test cap, helmet, backpack,
|
||||
shoulder bag, lanyard, clipboard and firefighter tank against the new shoulders/hips.
|
||||
- [ ] Go/no-go: if the warp fails the silhouette gate, stop and write the 62-joint runtime
|
||||
ADR (Stage 2). Do not start the 12-variant rebuild.
|
||||
|
||||
### 4. Roster and rollout
|
||||
|
||||
- [ ] Map the 12 roles to Quaternius outfits/parts, build them in asset-family batches,
|
||||
and keep every variant inside the 24,000/24,000 cap.
|
||||
- [ ] Run `audit`, `fit`, `inspect`, `texels` and `npc_proportions.py report`; record the
|
||||
12-variant table and the warp measurements.
|
||||
- [ ] Re-run the 24-NPC budget smoke and the Web/Compatibility capture; record frame time
|
||||
before and after the base swap.
|
||||
- [ ] Capture the 12 × 6 baseline (`capture_npc_baseline.gd` → `npc_baseline.py sheet`) and
|
||||
get owner approval before replacing the committed character GLBs.
|
||||
- [ ] Rebuild AO maps, manifest entries, credits and review docs; commit one asset family per
|
||||
change and never run a no-op full-library rebuild.
|
||||
|
||||
### 5. Retire the old character tracks
|
||||
|
||||
- [x] Remove the MPFB/MakeHuman development scripts, renders and review directories from
|
||||
`tmp/` (done 2026-11-05). Keep only the Quaternius source pack and its licence notes.
|
||||
- [x] Keep the committed `makehuman_head.glb` and the procedural `npc_base` only as tested
|
||||
fallbacks until the Quaternius rollout is complete; do not add new MPFB work.
|
||||
- [ ] Update `docs/development-status.md`, `CHANGELOG.md` and `CREDITS.md` when the new base
|
||||
ships. MPFB must never become a CI, build or runtime dependency.
|
||||
|
||||
## Fix the Rig pose harness
|
||||
|
||||
Goal: `src/test/capture_npc_pose.gd` reports a pose but saves rest frames; the root cause is
|
||||
narrowed to the skinned mesh ignoring its `Rig` parent transform. Fixing it unblocks the
|
||||
weights/pose column of the 3D review; the evidence now feeds the Quaternius base bake.
|
||||
|
||||
*Skill route: `rigging`; evidence in `docs/3d-model-review.md` §"Pose evidence is blocked".*
|
||||
|
||||
- [x] Root cause found (2026-11-04): the harness never disabled the avatar's `_physics_process`,
|
||||
so `_advance_fsm` + `_move_toward_target` ran every tick and reset `arrived`/velocity, which
|
||||
made `theater_pose()` return "idle" and reverted the pose to rest. (The earlier "moving the
|
||||
`Rig` child does not render" probe was a red herring: it set `rig.position.y` inside `_shoot()`
|
||||
in the same frame as `root.get_texture().get_image()`, so it never got a rendered frame.)
|
||||
- [x] Harness rebuilt: `capture_npc_pose.gd` now calls `set_physics_process(false)`/
|
||||
`set_process(false)` after staging the avatar; the pose survives because only the direct
|
||||
`_animate()` ticks drive it. Verified: sit vs idle diff 5.63, walk vs idle diff 2.29, and the
|
||||
vision judge confirms the seated frame reads as crouched.
|
||||
- [x] Captured all poses for all 12 variants (idle / walk_contact / walk_pass / sit × full +
|
||||
shoulder views = 96 frames) and built `sheet_pose.jpg`/`sheet_shoulder.jpg` under `tmp/`.
|
||||
(No separate "wave" theater pose exists; the escalation raise-hand gesture is not a distinct
|
||||
capture in this harness.)
|
||||
|
||||
## 3D model and local map re-evaluation
|
||||
|
||||
Goal: re-judge every shipped 3D asset against the newly available Blender skill guidance, rank
|
||||
@@ -130,14 +46,15 @@ venues against licensed real-world data — OSM footprints and building massing,
|
||||
photoreal facades and the photo-driven geometry pipeline (sections H–M). Evidence comes from three
|
||||
separate sources: licensed real-location photos, generated concept boards, and build captures. This
|
||||
is an unversioned development track: it must not change the `npc_base` contract, the renderer or
|
||||
the 0.10.0 release candidate.
|
||||
the public 0.10.0 release.
|
||||
|
||||
**Sequencing (2026-11-03, owner):** the asset-review fixes (A–G, notably the character P0/P1
|
||||
rebuilds and the face/foliage prototypes) stay **blocked** until the full local map rebuild
|
||||
(H–M) is done **and** a fresh 3D model review runs against the new maps. The measurements and
|
||||
scorecard in `docs/3d-model-review.md` stand as the pre-rebuild baseline, not a go-ahead to start
|
||||
character work now. (Character development has since shipped — see `DONE.md` — and the next
|
||||
character pass is the Quaternius base above.)
|
||||
**Sequencing (updated 2026-11-05):** the old block is cleared. The local map rebuild (H–M) is
|
||||
done, the 96/96 review ran against the new maps, and character work moved to the versioned
|
||||
`0.9.9`/`0.9.10` milestones. This section is now the **evidence, scorecard and measurement
|
||||
source**, not an open-ended backlog: the venue/prop/lighting fixes are scheduled into
|
||||
`0.9.11`, the budget/LOD checks into `0.9.12`, and the character items (C) are superseded by
|
||||
`0.9.9`/`0.9.10`. Fixes are made from the milestone checklists; the scorecard keeps the
|
||||
measurements and verdicts.
|
||||
|
||||
**Map rebuild status (2026-11-03):** H–M are complete — the Joke Shop is now a 6.0 m low hall
|
||||
(dark stucco, white signature fascia, tied-back curtains, hotel tower) and Hollywood/TV Studio
|
||||
@@ -224,7 +141,14 @@ licence-free zone only.
|
||||
per item; record the decision and the reason in the review document. **Deferred until the
|
||||
full map rebuild (H–M) lands and a fresh 3D review runs against the new maps.**
|
||||
|
||||
### C. Character re-evaluation (8 variants + fallback)
|
||||
### C. Character re-evaluation (superseded 2026-11-05)
|
||||
|
||||
The procedural character review is complete and its evidence stays in
|
||||
`docs/3d-model-review.md`. The next character pass is the Quaternius base in
|
||||
[`Milestone 0.9.9`](#milestone-099--quaternius-npc-base-swap-patch) and
|
||||
[`Milestone 0.9.10`](#milestone-0910--npc-roster-rollout-patch); the open items below are kept
|
||||
as evidence for what the new base must not regress (face read, hands, weights, cloth, texel
|
||||
density, crowd variety).
|
||||
|
||||
*Skill route: `character-artist`, `retopology`, `rigging`, `uv-workflow`, `texture-workflow`,
|
||||
`materials`, `hair-groom`, `asset-optimization`, `lookdev`, `qa-review`.*
|
||||
@@ -262,8 +186,8 @@ licence-free zone only.
|
||||
plus stride scaling are the accepted approach; a baked cloth-sim pipeline would only be
|
||||
reconsidered if a later close-up shows visible hem/coat stiffness.
|
||||
- [ ] 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. **Blocked until
|
||||
the map rebuild + second 3D review (see the track's sequencing note).**
|
||||
re-run `inspect`, `audit`, `fit` and `npc_proportions.py` after each fix. **Scheduled: the
|
||||
remaining character items are covered by Milestone 0.9.9/0.9.10; this row is evidence only.**
|
||||
- [x] Crowd-variety reference: five licence-cleared Commons photos fetched to
|
||||
`tmp/research/crowd-variety/` (CC0 ×2, CC BY 2.0, CC BY 3.0, PD) covering umbrella/dense
|
||||
crowds and a pedestrian mall — general build/hair/outer-layer variety context, reference
|
||||
@@ -672,11 +596,11 @@ pass. The current `_build_comedy_store_building()` stays as the fallback.*
|
||||
Unversioned housekeeping that keeps the planning sources trustworthy. Do these when they block a
|
||||
task, not on a schedule.
|
||||
|
||||
**Status (2026-11-03):** v0.10.0-rc.1 source; three venues; 277 headless + 161 Python tests; the
|
||||
**Status (2026-11-05):** v0.9.8 published; three venues; 277 headless + 162 Python tests; the
|
||||
Hollywood and Joke Shop facade-realism passes landed (`limestone_ashlar`, `dark_stucco`, banner
|
||||
night emissive, marquee emissive test); reference boards (`hollywood`, `joke-shop`) with sidecars;
|
||||
three-up boards; `tools/refboard.py` + reference-anchored generation (`--reference`, `anchor`) +
|
||||
`tools/three_up.py`. Gaussian splatting is deferred to the end of this file.
|
||||
`tools/three_up.py`. Gaussian splatting stays deferred at the end of this file.
|
||||
|
||||
### Memory and status accuracy
|
||||
|
||||
@@ -691,8 +615,8 @@ three-up boards; `tools/refboard.py` + reference-anchored generation (`--referen
|
||||
contradiction and the old 272/108 counts.)
|
||||
- [x] Fix stale status claims as they are found; add a documentation check or grep note only if
|
||||
the same class of drift returns. (Counts and the sign-in state corrected on 2026-11-03.)
|
||||
- [x] Keep the Gap Analysis baseline and the README status line in sync when a test count, version
|
||||
or feature milestone changes (the baseline was bumped to 277/161 on 2026-11-03).
|
||||
- [x] Keep the `PLAN.md` gap-analysis baseline and the README status line in sync when a test count,
|
||||
version or feature milestone changes (baseline bumped to 277/162 on 2026-11-05).
|
||||
|
||||
### Imagery and provenance upkeep
|
||||
|
||||
@@ -734,245 +658,6 @@ three-up boards; `tools/refboard.py` + reference-anchored generation (`--referen
|
||||
- [ ] 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
|
||||
|
||||
**Published baseline:** v0.9.6 (2026-11-03) — three playable venues, tutorial/campaign/checkpoint
|
||||
resume, access/radio/escalation loop, shift review, the Blender asset library, 277 Godot + 161
|
||||
Python tests, a 21/21 screen-design judge and Compatibility/Web/Desktop HQ builds.
|
||||
**Current source:** 0.10.0-rc.1. Its clean release-candidate build is verified, but it is not
|
||||
published while the gates below remain open.
|
||||
|
||||
### Open gaps
|
||||
|
||||
| Area | Remaining gap | Milestone |
|
||||
| --- | --- | --- |
|
||||
| 3D model and local map re-evaluation | Skill-guided asset scorecard/repairs; OSM/3D data, reference-derived facades, AI-assisted composition and map QA | Unversioned |
|
||||
| Character development | Complete (moved to `DONE.md`); the Quaternius NPC base migration is the next character pass | 0.10.0 |
|
||||
| Content and balance | External first-time-player evidence, balance sweeps/tuning and content lock | 0.10.0 |
|
||||
| Audio, accessibility and localization | Accessibility/localization acceptance, caption coverage and pseudo-locale/layout sign-off | 0.10.0 |
|
||||
| Performance and platform | Target-hardware matrix, Web/Desktop HQ profiling and frame/memory evidence | 0.10.0 |
|
||||
| QA and release engineering | Final smoke matrix, clean build/checksum verification and target-matrix launch tests | 0.10.0 |
|
||||
| Legal, signing and storefront | macOS signing/notarization, AI-provider rights, storefront/rating approval and privacy sign-off | 0.10.0 |
|
||||
| Documentation and sign-off | Final README/status/TODO/manual evidence and known-limitations sign-off | 0.10.0 |
|
||||
| Future 0.x | Live replay, more venues, VO/music, image-to-mesh props and seasonal content | Unscheduled |
|
||||
|
||||
The 0.9.3 UI, 0.9.4 animation, 0.9.5 texture, 0.9.6 integration and save-migration work are
|
||||
complete in `DONE.md`; they are not remaining gaps. The 2026-11-01 documentation audit
|
||||
(architecture ownership, stale-claim cleanup and obsolete-plan removal) is also closed.
|
||||
|
||||
**Priority (2026-11-05):** finish the `Rig` pose harness, then the Quaternius NPC base
|
||||
migration, then re-evaluate the 3D models, then ship the **0.10.0** minor release. The 0.10.0 candidate
|
||||
still accepts only release-blocking fixes until its external-playtest, legal/storefront, signing
|
||||
and platform gates clear.
|
||||
|
||||
## Milestone 0.10.0 — 3D model and map improvements (minor)
|
||||
|
||||
Goal: ship all 3D model and map improvements together as the **0.10.0 minor** — the Quaternius
|
||||
NPC base bake (contract armature + five slots), the rebuilt Comedy Store/Hollywood maps,
|
||||
and the re-evaluated 3D scorecard — plus the external playtest, legal/storefront, signing and
|
||||
platform gates from the old release candidate. The character-development patch (0.9.7) ships
|
||||
first; this minor contains the model/map work.
|
||||
|
||||
### Content lock and external playtest
|
||||
|
||||
### Content lock and external playtest
|
||||
|
||||
- [x] Declare a content/feature freeze for 0.10.0; only release-blocking fixes and balance changes
|
||||
may land after the freeze. (Frozen 2026-11-03 after the 0.9.6 integration
|
||||
checkpoint; this pass is limited to release reliability, accessibility,
|
||||
balance evidence and packaging.)
|
||||
- [x] Produce a release-candidate build with the tutorial, campaign, all three venues, Help,
|
||||
accessibility settings and checkpoint/resume enabled. (Clean worktree
|
||||
`a9658ec` built `0.10.0-rc.1`: Linux 161.5 MB, Windows 197.2 MB, macOS
|
||||
148.0 MB, Web 97.8 MB and HQ 241.6 MB. The Web/HQ archives include the
|
||||
credits, privacy, accessibility and known-limitations pages; SHA-256
|
||||
manifests were written; the Linux and HQ binaries launched headless and the
|
||||
Web index/wasm/pck/docs returned HTTP 200. Not published because external
|
||||
playtest and legal approval remain open.)
|
||||
- [ ] Run at least five first-time-player sessions covering the tutorial and one full venue shift;
|
||||
record where players get stuck, misunderstand evidence or choose disproportionate escalation.
|
||||
- [ ] Triage findings by severity (P0/P1/P2) and fix every P0/P1 onboarding/accessibility issue;
|
||||
keep the rest as explicit known limitations or post-0.10 items.
|
||||
- [ ] Re-run the affected tutorial/campaign smokes after each fix and capture a fresh first-hour
|
||||
walkthrough for the release notes.
|
||||
|
||||
### Balance and fairness pass
|
||||
|
||||
- [x] Define measurable targets for routine compliance, access-decision accuracy, peaceful
|
||||
resolution, justified emergency calls and false-positive/false-negative rates.
|
||||
(`docs/release-qa.md#balance-acceptance` defines cohorts, denominators, 36 matrix cells,
|
||||
seeds, thresholds and matched emergency-first comparisons. Documentation verified;
|
||||
targets are not measured results.)
|
||||
- [ ] Run scripted venue sweeps across easy/normal/hard and all four phases; compare outcomes and
|
||||
tune event weights, reaction tables, compliance and report strictness where targets fail.
|
||||
- [ ] Review emergency calls against actual danger: no routine behavior should make emergency the
|
||||
dominant winning move, and genuine threats must remain answerable. Include the
|
||||
type-derived `_read_npc_truth()` mismatch with observed threats/forced entry;
|
||||
use independently labelled behavior evidence, not the helper as its own oracle.
|
||||
- [ ] Verify the reputation grades, debrief explanations and campaign progression still communicate
|
||||
the de-escalation-first design after tuning.
|
||||
- [ ] Record the balance evidence and any deliberate exceptions in `docs/development-status.md`
|
||||
or `MEMORY.md`.
|
||||
|
||||
### Save migration and session reliability
|
||||
|
||||
- [x] Add fixtures for a 0.8-era settings/campaign/history save and a 0.9.0 checkpoint; verify
|
||||
migration/default-filling and a clean first launch with no files. (Committed
|
||||
fixtures under `src/test/fixtures/saves/`; 9 migration/failure tests cover
|
||||
old settings/campaign/history, the 0.9.0 checkpoint, all three venue
|
||||
checkpoint round-trips, clean install, corrupt JSON and a blocked atomic
|
||||
replace. Suite is now 272 tests.)
|
||||
- [x] Test fresh install → tutorial → campaign → pause → quit → resume → review → next venue on
|
||||
an isolated user-data directory. (Tutorial and campaign smokes shared one
|
||||
isolated `XDG_DATA_HOME`; tutorial completion/replay/skip then campaign
|
||||
resume/review-continue/help both passed. Pause/menu behavior is covered by
|
||||
`smoke_session_shell.gd`.)
|
||||
- [x] Test corrupt JSON, partial temp files, missing optional fields and a failed atomic rename;
|
||||
confirm fallback behavior never destroys a valid save. (Corrupt/partial files
|
||||
fall back to defaults; a blocked temp path reports an error and leaves the
|
||||
prior valid settings intact. `_load_json` now uses the non-logging JSON
|
||||
instance parser.)
|
||||
- [x] Verify checkpoint resume after all three venues, after an incident, with NPCs at the door and
|
||||
with flashlight/body-camera/door state set. (The migration suite round-trips a
|
||||
checkpoint for Hollywood, the Joke Shop and the TV Studio with an open
|
||||
incident, one NPC row, flashlight/body-camera/door flags; the campaign smoke
|
||||
resumes a live checkpoint through the real `GameManager`.)
|
||||
- [x] Document save locations, schema versions, reset behavior and rollback/export instructions in
|
||||
`docs/getting-started.md` and the release notes. (`getting-started.md` now
|
||||
documents the four files, version-1 checkpoint, atomic write/fallback,
|
||||
clean install, reset and backup/rollback.)
|
||||
|
||||
### Release engineering and distribution
|
||||
|
||||
- [x] Run `tools/build_releases.sh` from a clean checkout and verify Linux/Windows/macOS/Web/HQ
|
||||
artifact names, sizes and checksums. (Clean worktree `a9658ec` built the
|
||||
five artifacts listed above; `PUBLISH_DRY_RUN=1` produced and printed
|
||||
`build/SHA256SUMS-0.10.0-rc.1.txt` without touching the API.)
|
||||
- [ ] Smoke-test fresh Linux portable/AppImage, Windows portable/installer, macOS unzip/launch,
|
||||
Web static hosting and Desktop HQ Vulkan fallback on the target matrix.
|
||||
(Linux portable and Desktop HQ launched headless; Web files served over a
|
||||
local static server with HTTP 200. AppImage/installer were not built locally
|
||||
and Windows/macOS launch testing still needs the target matrix.)
|
||||
- [x] Add a disk-space preflight and retention policy to the release/publish scripts so a full
|
||||
Gitea host cannot leave a partially uploaded release. (`publish_release.sh`
|
||||
checks artifact size/free space, writes SHA-256 checksums, supports
|
||||
`PUBLISH_DRY_RUN`, rolls back uploaded assets/new releases on failure, and
|
||||
offers opt-in `RETAIN_RELEASES` pruning; dry-run verified.)
|
||||
- [x] Verify release metadata, tag/version consistency, asset counts and download links; retain
|
||||
at least the latest 0.8/0.9 releases and document older-asset pruning.
|
||||
(Source/version and tag are both 0.9.6; all seven published releases carry
|
||||
5–7 assets, including the retained v0.8.0/v0.8.1/v0.9.0/v0.9.1 line.
|
||||
`publish_release.sh` documents opt-in `RETAIN_RELEASES` pruning and
|
||||
`RELEASING.md` describes the seven-release policy.)
|
||||
- [x] Decide and document whether macOS signing/notarization is required for 0.10.0 or explicitly
|
||||
remains a future release blocker; record the quarantine instructions either way.
|
||||
(0.10.0 ships unsigned with quarantine instructions; signing/notarization
|
||||
remains an explicit public-release blocker in `RELEASING.md` and
|
||||
`known-limitations.md`.)
|
||||
|
||||
### Legal, credits and platform policy
|
||||
|
||||
- [x] Audit AI-assisted asset provenance and licenses; record source/model usage and any
|
||||
redistribution constraints for textures, characters, props and UI art.
|
||||
(`CREDITS.md` records the Blender/OpenRouter toolchain, default model slugs,
|
||||
missing per-file generation metadata and the commercial-rights review as an
|
||||
explicit release blocker.)
|
||||
- [x] Audit audio/SFX/music licensing (or confirm everything is procedural) and add a credits file.
|
||||
(All runtime audio is procedural; no external music/SFX files are bundled.
|
||||
`CREDITS.md` records this.)
|
||||
- [x] Review body-camera/privacy language, mental-health-crisis representation, age/content rating
|
||||
and storefront requirements; fix any release-blocking copy or content.
|
||||
(`PRIVACY.md` clarifies the simulated body camera/local-only data; the
|
||||
accessibility and known-limitations pages record the remaining
|
||||
mental-health/rating/storefront review as a release blocker rather than
|
||||
claiming approval.)
|
||||
- [x] Add the player-facing credits, known limitations, accessibility statement and privacy note to
|
||||
the release package/README. (`README.md` links `CREDITS.md`, `PRIVACY.md`,
|
||||
`docs/accessibility.md` and `docs/known-limitations.md`; the Web and Desktop
|
||||
HQ package builders copy those documents into the shipped archives.)
|
||||
- [ ] Mark external legal/storefront approval as complete or explicitly block the 0.10.0 release on it.
|
||||
|
||||
### Documentation and visual sign-off
|
||||
|
||||
- [ ] Update `README.md`, `PLAN.md`, `docs/architecture.md`, ADRs, `docs/development-status.md`,
|
||||
`docs/getting-started.md`, `docs/release-qa.md#manual-smoke-checklist`, `CHANGELOG.md` and this roadmap.
|
||||
- [ ] Regenerate the full screenshot gallery and realism baselines from the release-candidate
|
||||
build (not the dev tree) after content lock; run `check_screenshots.sh` and the pinned vision
|
||||
judge, recording model/spend/verdicts, and refresh the README and venue-doc images.
|
||||
- [ ] Include the reference/concept boards and the updated asset sheets in the release docs, and
|
||||
verify every README and docs-page image resolves from the tagged build.
|
||||
- [ ] Run the manual smoke checklist on every target and attach/link the release-candidate evidence.
|
||||
- [ ] Verify the tutorial/campaign/help text at maximum font scale and with the longest strings.
|
||||
- [x] Remove or archive stale plans/notes and confirm no contradictory version/status claims remain.
|
||||
- [x] Document the character development toolchain in `docs/architecture.md`: base vs library
|
||||
sources, tool/artifact/gate table, runtime attach/animation contracts and rebuild discipline.
|
||||
(Added §5 "Character development tooling" plus risk and ownership-map updates;
|
||||
documentation check passed.)
|
||||
|
||||
### Release-candidate QA and exit
|
||||
|
||||
- [ ] Run the complete headless suite, Python tooling, tutorial/campaign/session/scenario smokes,
|
||||
24-NPC budget smoke and release build from a clean checkout.
|
||||
- [ ] Run the manual QA matrix: fresh install, upgrade, offline launch, gamepad, accessibility
|
||||
settings, pause/resume, all venues and Desktop HQ fallback.
|
||||
- [ ] Confirm no P0/P1 defects remain; P2 items must be documented as known limitations or
|
||||
post-0.10 work.
|
||||
- [ ] Create the `v0.10.0` version bump, changelog entry and tag; build/publish the release and
|
||||
verify all assets and download links.
|
||||
- [ ] Exit: the release candidate is content-locked, externally played, legally reviewed,
|
||||
documented, reproducibly built, and approved as the 0.10.0 release candidate.
|
||||
|
||||
## Milestone 0.9.7 — Character development (patch)
|
||||
|
||||
Goal: release the finished procedural character development — the realistic face on all eight
|
||||
variants, the four crowd-variety characters and the per-role pools — as a **patch** on top of
|
||||
0.9.6. This keeps the 3D model and map improvements out of the patch; they land together in the
|
||||
0.10.0 minor.
|
||||
|
||||
- [x] Rig pose harness fixed and the weights/pose evidence captured (see the top track).
|
||||
- [x] Character work done: 12-character library, realistic face, crowd variants, per-role pools,
|
||||
24-NPC budget 6.94 ms (152,464 tris / 168 meshes).
|
||||
- [x] QA + local build for 0.9.7: Godot 277/277, Python 161/161, audit 49 OK, fit/proportions
|
||||
12/12, release smoke matrix green, gallery 21/0, clean worktree build (Linux 172 MB,
|
||||
Windows 206 MB, macOS 159 MB, Web 111 MB, HQ 266 MB), checksums verified, Linux binary
|
||||
launches headless. Tagged `v0.9.7` (CI build/publish follows).
|
||||
|
||||
**Exit:** met — 0.9.7 ships the 12-character library (face realism + crowd variety) with the pose
|
||||
harness fixed; the runtime contract is unchanged. The Quaternius NPC base and the map rebuild are
|
||||
not in this patch (they target 0.10.0).
|
||||
|
||||
## Roadmap dependencies and risks
|
||||
|
||||
- Content volume is the largest schedule risk; tutorial and campaign work must not outrun the
|
||||
event/dialogue matrix.
|
||||
- The vision judge is a dev-time aid with an external key/budget; CI must never make it a hard gate.
|
||||
- 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.
|
||||
|
||||
- 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.
|
||||
|
||||
## Future / deferred (not scheduled)
|
||||
|
||||
- [ ] 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.
|
||||
|
||||
## Gaussian splatting evaluation (deferred)
|
||||
|
||||
Postponed for further development on 2026-11-03. The local-map track ships the facade kit and
|
||||
@@ -1069,3 +754,450 @@ reference-image licence and privacy gates above rather than replacing them.
|
||||
- [ ] 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.
|
||||
|
||||
## Milestone 0.9.9 — Quaternius NPC base swap (patch)
|
||||
|
||||
Goal: make one Quaternius-derived NPC render and animate correctly in Godot and ship it as the
|
||||
pilot base, with the procedural library still selectable as the fallback. This is the first
|
||||
versioned character milestone; it contains the rig correction that has been blocking the
|
||||
ADR-0018 rollout.
|
||||
|
||||
**Why this is a patch:** it changes character presentation and animation quality but adds no new
|
||||
player-facing system. The 0.10.0 minor is reserved for the release gates (see below), so a
|
||||
blocked rig can no longer block the whole release.
|
||||
|
||||
### Rig and export correction
|
||||
|
||||
- [x] Reproduce and record the failure: the `armature_apply` + `export_apply=True` bake wrote
|
||||
wrong inverse bind matrices (Godot rendered A-pose while the rest was arm-down). Recorded
|
||||
in `MEMORY.md`; `bake_armdown_clean.py` was the failing recipe.
|
||||
- [x] Fix the arm-down bake so the exported skin matches the rest. `tmp/npc-research/bake_armdown_v2.py`
|
||||
(pose-rotate → apply modifier → reset pose → edit-mode rotate arms → re-add modifier →
|
||||
export `export_apply=False`) now yields a correct arm-down base: Blender deformed x ±0.221
|
||||
and the rest bone tail points down. Still local (`tmp/`), not shipped.
|
||||
- [x] Re-derive the pose composition for a non-180° rest. Superseded by the rig-agnostic
|
||||
`_swing_bone()` below: the rest-relative `set_bone_pose_rotation` form was never exact,
|
||||
because `get_bone_rest()` is parent-relative and already carries the importer's axis
|
||||
conversion. Kept in `MEMORY.md` as a rejected approach.
|
||||
- [x] Fix the skinning/rest bake end to end (commit `cd285b3`):
|
||||
(1) the canonical base is **join-only** (`tmp/npc-research/build_base_minimal.py`: no
|
||||
`remove_doubles` weld, no scale/ground transform, keep the Armature modifiers) — the weld
|
||||
and the transform corrupted the Quaternius weights;
|
||||
(2) the arm-down bake (`tmp/npc-research/bake_armdown_v5.py` — pose, read pose matrices,
|
||||
apply modifier, **reset pose to identity**, set rest matrices, re-add modifier) bakes the
|
||||
rest correctly (DEFORMED x −0.228/0.228, Godot bind pose arm-down);
|
||||
(3) `NpcAvatar._swing_bone()` now writes the **global** bone pose from
|
||||
`get_bone_global_rest()`, so one formula drives both the 62-joint Quaternius rig and the
|
||||
13-core fallback (measured: posed bone axis matches the proxy pivot at dot 1.000 on the
|
||||
Quaternius, and the procedural rigs are unchanged);
|
||||
(4) `RIG_BONES` + `NpcAvatar.bone_name()` hold the only side mapping (the Quaternius labels
|
||||
`L`/`R` opposite the project), and the hand proxy is `global_wrist − global_elbow`.
|
||||
Verified in Godot: idle/sit/walk captures on npc_casual plus the unchanged procedural rigs.
|
||||
- [x] Add regression tests: `test_shipped_rig_resolves_the_animation_joints` covers the joint map,
|
||||
the baked arm-down rest, the down-pointing arm axis and the proxy side mapping; the Blender
|
||||
audit/`inspect` and `npc_proportions` now understand both rigs. Godot 278/278, Python
|
||||
167/167.
|
||||
- [x] Give the Quaternius base the five tint slots: `build_base_minimal.py` renames the upstream
|
||||
materials to `skin`/`cloth`/`pants`/`hair`/`accessory` and keeps `Eye`/`Eyebrows` as
|
||||
fixed-colour detail. The source UV map is degenerate (every loop at 0,0), so the build now
|
||||
Smart-UV-unwraps and repacks the layout — texel density went from an unmeasurable 0 to
|
||||
73–74 px/m on the 256² maps and the 256² AO bake works again.
|
||||
- [ ] Verify Idle and Walk on the 62-joint rig: loop seam, foot contact, root motion and frame
|
||||
rate. Record which of the 24 clips are in scope and which stay unused.
|
||||
- [ ] Decide and record how poses the clips do not cover (seated, raise-hand, head turn) are
|
||||
driven at runtime.
|
||||
- [ ] Keep the fallback tested: `npc_base`, the MakeHuman head and `_build_body()` remain
|
||||
selectable; no MPFB work is reopened and no Stage-1 proportion warp returns. (The procedural
|
||||
library is still the majority of `CHARACTER_POOLS` and captures clean, but the fallback path
|
||||
itself has no dedicated test yet.)
|
||||
- [ ] Re-fit the remaining gear against the Quaternius torso: the `backpack` strap geometry was
|
||||
authored for the old body width and now hangs in front of the chest. The camera was fixed
|
||||
with a wrist strap (a neck strap floats when the item is hand-attached).
|
||||
|
||||
### Silhouette and face gate (owner)
|
||||
|
||||
- [x] Re-run the silhouette gate at gameplay distance and in portrait against the current
|
||||
`npc_casual`, front / three-quarter / profile. ADR-0018 removed the proportion warp, so
|
||||
source proportions are accepted by definition (head 13.4 % of height, measured by
|
||||
`npc_proportions`).
|
||||
- [x] Face-fidelity comparison sheet: [`docs/screens/npc-face-gate/`](../../docs/screens/npc-face-gate/README.md)
|
||||
captures the Quaternius base, the procedural + MakeHuman head and a procedural crowd variant
|
||||
at front / three-quarter / profile / 8 m. The page records the three options (full swap,
|
||||
crowd only, add head detail) and recommends the full swap.
|
||||
- [ ] **Owner decision:** pick (a) full swap, (b) crowd only or (c) add head detail from
|
||||
`docs/screens/npc-face-gate/README.md`, then apply it within 24,000 tris / 24,000 glTF
|
||||
vertices and re-run the gate.
|
||||
- [ ] Record the decision and its consequence in `MEMORY.md`, `docs/npc-model-sourcing.md` and
|
||||
(if it changes the contract) a new ADR. Do not silently lower character fidelity.
|
||||
|
||||
### Pilot rollout
|
||||
|
||||
- [x] Map the five tint slots onto the Quaternius materials and unwrap the degenerate source
|
||||
UV map; bake the 256² AO and check texel density (73–74 px/m).
|
||||
- [ ] Bake the per-slot albedo/normal/ORM maps and a per-slot atlas (the five slots currently
|
||||
share one whole-body UV set).
|
||||
- [-] Re-fit gear attach points against the Quaternius skeleton and test cap, helmet, backpack,
|
||||
shoulder bag, lanyard, clipboard and firefighter tank. (Camera done: wrist strap instead of
|
||||
the floating neck strap. Backpack straps still cut across the Quaternius chest.)
|
||||
- [ ] Wire the approved base into one role (or the crowd roles if option b is chosen) with the
|
||||
procedural model as an instant fallback; no runtime download or optional dependency.
|
||||
- [ ] Run `blender_assets.py inspect`/`audit`/`fit`, `npc_proportions.py`, the 24-NPC budget smoke
|
||||
and the capture gate; record the measurements before/after.
|
||||
- [ ] Release `0.9.9`: update the version source in `project.godot` (`config/version`), write the
|
||||
`CHANGELOG.md` entry, run `tools/build_releases.sh`, verify checksums, commit, tag and push.
|
||||
|
||||
**Exit:** one committed Quaternius GLB renders arm-down with correct skinning, plays Idle and
|
||||
Walk on the 62-joint rig, poses correctly under the new composition, passes the owner face and
|
||||
silhouette gate, fits the 18 gear pieces, stays inside budget, and ships as `0.9.9` with the
|
||||
procedural library still available as a fallback.
|
||||
|
||||
## Milestone 0.9.10 — NPC roster rollout (patch)
|
||||
|
||||
Goal: extend the approved base to the full 12-role roster in asset-family batches, without
|
||||
regressing gear fit, budget or readability.
|
||||
|
||||
### Roster build
|
||||
|
||||
- [x] Map the 12 roles to the three full Quaternius GLBs (owner chose the full swap): hoodie for
|
||||
casual/coat/hoodie/crowd_c, casual tee for jacket/dress/crowd_a/crowd_b, worker for
|
||||
vest/firefighter/guard/crowd_d. `npc_firefighter`, `npc_guard` and `npc_crowd_d` drop the
|
||||
upstream hivis vest and hard hat. All twelve built, AO-baked and committed (`a6ccd86`).
|
||||
- [ ] Add the modular part library (hair styles, outfits) so the roster regains the silhouette
|
||||
variety the twelve procedural meshes had; three shared meshes is the current ceiling.
|
||||
- [-] Build one asset family per commit, AO-baked and re-imported; never run a no-op full-library
|
||||
rebuild. Use the concept + vision-judge loop for head/outfit decisions and record model,
|
||||
prompt and spend. (Roster family landed in one commit because the three sources share one
|
||||
pipeline; the 12 × 6 baseline sheet is captured but not yet owner-approved.)
|
||||
- [ ] Keep every variant inside 24,000 tris / 24,000 glTF vertices and the five tint slots; no
|
||||
new bone beyond the accepted 62-joint set without an ADR.
|
||||
- [ ] Refresh `CHARACTERS`, the manifest roles/tint data, `CREDITS.md` and the review doc rows as
|
||||
each family lands.
|
||||
|
||||
### Gates and rollout
|
||||
|
||||
- [ ] Re-bake AO per rebuilt GLB and refresh the stale `.godot` import cache (scratch-copy
|
||||
import) before judging any face or body change.
|
||||
- [ ] Run `inspect`/`audit`/`fit`/`texels` and `npc_proportions.py report`; record the
|
||||
12-variant measurement table.
|
||||
- [ ] Capture the 12 × 6 baseline and get owner approval before replacing the committed GLBs.
|
||||
- [ ] Re-run the 24-NPC budget smoke and the Web/Compatibility capture; record frame time,
|
||||
mesh/triangle counts and Web load before/after.
|
||||
- [ ] Retire the procedural variants that are no longer used, keeping one tested fallback; update
|
||||
`docs/development-status.md`, `docs/3d-model-review.md`, `docs/blender-previews` sheets and
|
||||
the README character row.
|
||||
- [ ] Release `0.9.10` with the full procedure (version, changelog, local build, checksums, tag).
|
||||
- [ ] Shrink the release artifacts: `assets/` alone is ~273 MB in the Linux binary. Excluding
|
||||
`tmp/`, `build/`, `docs/`, `tools/`, `src/test/` and `.agents/` from the export presets
|
||||
already cut 426 MB to 273 MB; the remaining bulk is texture resolution.
|
||||
|
||||
**Exit:** all 12 roles use the approved base or an explicitly kept fallback, every variant passes
|
||||
the contract and gear gates, the 24-NPC and Web budgets are measured and documented, and the
|
||||
baseline sheet is owner-approved.
|
||||
|
||||
## Milestone 0.9.11 — Venue, prop and lighting polish (patch)
|
||||
|
||||
Goal: close the scheduled subset of the unversioned 3D review backlog. Only ranked P1/P2 items
|
||||
whose fix is visible at gameplay distance are in scope; the review doc stays the evidence and
|
||||
scorecard source, this milestone is the actionable list.
|
||||
|
||||
### Props, gear and foliage
|
||||
|
||||
- [ ] Rebuild the ranked prop candidates from §D: the foliage-language P1
|
||||
(`planter_box`/`round_planter` foliage, `street_tree`/`palm_tree`), plus the
|
||||
`newspaper_box` slot and `bollard` collar if the budget allows.
|
||||
- [ ] Replace the flat `_shrub()`/`_tree()` clumps with carded clusters and keep the 60k
|
||||
library triangle budget; measure before/after.
|
||||
- [ ] Review the 18 gear pieces for silhouette and strap/buckle readability and rebuild only what
|
||||
fails; re-run `fit` after every change.
|
||||
- [ ] Verify prop colliders and navigation obstacles against the final silhouettes.
|
||||
|
||||
### Venue, lighting and set dressing
|
||||
|
||||
- [ ] Joke Shop / Hollywood shape language: fix the ranked remaining items (west rounded corner,
|
||||
canopy pitch, facade reveal depth) only where a capture shows a readable gain.
|
||||
- [ ] `lighting`/`lookdev` pass: flashlight cone/energy for the `06-night-flashlight` stage,
|
||||
marquee/event-banner emissive read under the Compatibility renderer, street-lamp pools.
|
||||
- [ ] Set dressing: fix the Joke Shop queue story (podium/stanchion line at the door), rope
|
||||
barrier thickness, the west palm overlap and the empty sidewalk light pool; apply the same
|
||||
review to Hollywood and the TV Studio.
|
||||
- [ ] Build one real-photo comparison for street furniture and record the accepted mismatches.
|
||||
|
||||
### Evidence and release
|
||||
|
||||
- [ ] Refresh `sheet_props.jpg`, `sheet_npc_gear.jpg`, the three-up boards, the affected venue
|
||||
captures and the README asset rows.
|
||||
- [ ] Re-run `tools/check_screenshots.sh` and `python3 tools/check_docs.py`; record both results.
|
||||
- [ ] Release `0.9.11` with the full procedure.
|
||||
|
||||
**Exit:** every scheduled P1/P2 item is fixed or explicitly accepted with a measurement, the
|
||||
capture gallery and asset sheets are current, and no unreviewed asset remains in the shipped
|
||||
catalogue.
|
||||
|
||||
## Milestone 0.9.12 — Performance, Web and accessibility (patch)
|
||||
|
||||
Goal: prove the new character and venue content still meets the frame, memory, load and
|
||||
accessibility targets on every target before the minor release.
|
||||
|
||||
### Performance and platform
|
||||
|
||||
- [ ] Profile the 24-NPC budget scene on Compatibility/Web and Desktop HQ; record frame time,
|
||||
draw calls, texture memory and pck size against the pre-character baselines.
|
||||
- [ ] Verify LOD/impostor thresholds, the 60k library and 150k/700 venue budgets, and the Web
|
||||
load time after the character and texture changes.
|
||||
- [ ] Confirm the Compatibility/Web build still avoids compute shaders and Vulkan-only features;
|
||||
spot-check the HQ fallback message.
|
||||
- [ ] Re-run the save/session regression suite on an isolated user-data directory after the
|
||||
content changes.
|
||||
|
||||
### Accessibility and localization readiness
|
||||
|
||||
- [ ] Verify font scaling at maximum, the longest strings, caption coverage, reduced motion,
|
||||
colour-independent escalation cues and the hold-to-confirm emergency path.
|
||||
- [ ] Run the string catalogue through a pseudo-locale/length check; move remaining hardcoded UI
|
||||
strings into `content/strings.en.json`.
|
||||
- [ ] Fix only the issues the checks expose; new accessibility features are 0.11.0 scope.
|
||||
|
||||
### Release
|
||||
|
||||
- [ ] Regenerate the full capture gallery and the screen-design judge from the current build;
|
||||
record model, spend and verdict.
|
||||
- [ ] Release `0.9.12` with the full procedure.
|
||||
|
||||
**Exit:** the character and venue changes do not regress frame time, memory, Web load,
|
||||
save/session reliability or accessibility behaviour, and both automated gates are green.
|
||||
|
||||
## Milestone 0.10.0 — Public-readiness release (minor)
|
||||
|
||||
Goal: ship the new characters, the polished venues and the public-readiness gates as the
|
||||
**0.10.0 minor**, after the patch milestones above have landed. Content is frozen at 0.9.12;
|
||||
only release-blocking fixes and balance changes may land here.
|
||||
|
||||
### External playtest and onboarding
|
||||
|
||||
- [ ] Run at least five first-time-player sessions covering the tutorial and one full venue
|
||||
shift; record where players get stuck, misread evidence or over-escalate.
|
||||
- [ ] Triage by severity and fix every P0/P1 onboarding, evidence or accessibility issue; record
|
||||
the rest as known limitations or post-0.10 work.
|
||||
- [ ] Re-run the tutorial/campaign smokes after each fix and capture a first-hour walkthrough.
|
||||
|
||||
### Balance and fairness
|
||||
|
||||
- [ ] Run the scripted venue sweeps across easy/normal/hard and all four phases using the
|
||||
`docs/release-qa.md#balance-acceptance` targets; tune event weights, reaction tables,
|
||||
compliance and report strictness where targets fail.
|
||||
- [ ] Review emergency calls against independent behaviour labels, not `_read_npc_truth()` as its
|
||||
own oracle; no routine behaviour may make emergency the dominant winning move.
|
||||
- [ ] Verify reputation grades, debrief text and campaign progression still teach
|
||||
de-escalation-first after tuning; record the evidence and deliberate exceptions.
|
||||
|
||||
### Legal, signing and storefront
|
||||
|
||||
- [ ] Resolve the macOS signing/notarization decision and record the quarantine instructions.
|
||||
- [ ] Clear or explicitly block on AI-provider commercial rights, storefront/rating approval and
|
||||
the privacy/mental-health representation review.
|
||||
- [ ] Confirm every shipped image, texture, character and audio asset has provenance and credits.
|
||||
|
||||
### Platform and release engineering
|
||||
|
||||
- [ ] Smoke-test the target matrix: Linux portable/AppImage, Windows portable/installer, macOS
|
||||
unzip/launch, Web static hosting and the Desktop HQ Vulkan fallback.
|
||||
- [ ] Run `tools/build_releases.sh` from a clean checkout, verify artifact names, sizes, checksums
|
||||
and download links, and do a `PUBLISH_DRY_RUN`.
|
||||
- [ ] Run the complete headless suite, Python tooling tests, tutorial/campaign/session/scenario
|
||||
smokes and the 24-NPC budget smoke.
|
||||
- [ ] Confirm no P0/P1 defects remain; document P2 items as known limitations.
|
||||
|
||||
### Documentation and visual sign-off
|
||||
|
||||
- [ ] Update `README.md`, `PLAN.md`, `docs/architecture.md`, the ADRs, `docs/development-status.md`,
|
||||
`docs/getting-started.md`, `docs/release-qa.md`, `CHANGELOG.md` and this roadmap.
|
||||
- [ ] Regenerate the gallery and realism baselines from the release-candidate build, not the dev
|
||||
tree; verify every README/docs image resolves from the tagged build.
|
||||
- [ ] Run the manual smoke checklist on every target and link the evidence.
|
||||
- [ ] Release `0.10.0`: version, changelog, clean build, checksums, tag, push and post-release
|
||||
record.
|
||||
|
||||
**Exit:** 0.10.0 is content-locked, externally played, legally reviewed, documented,
|
||||
reproducibly built and approved for public download.
|
||||
|
||||
## Milestone 0.10.1 — Onboarding and accessibility fixes (patch)
|
||||
|
||||
- [ ] Fix the onboarding, tutorial and evidence-reading issues deferred from the 0.10.0 playtests
|
||||
that are cheap and non-structural.
|
||||
- [ ] Fix accessibility issues found at maximum font scale, in captions or in the colour-blind
|
||||
cue set.
|
||||
- [ ] Re-run the manual smoke checklist on the affected paths and release `0.10.1`.
|
||||
|
||||
**Exit:** first-hour comprehension and accessibility defects from the 0.10.0 evidence are closed.
|
||||
|
||||
## Milestone 0.10.2 — Balance and fairness follow-up (patch)
|
||||
|
||||
- [ ] Re-run the venue sweeps against the 0.10.0 targets and tune the remaining failing cells.
|
||||
- [ ] Review shift-review explanations, reputation grades and campaign difficulty progression.
|
||||
- [ ] Record the balance evidence and release `0.10.2`.
|
||||
|
||||
**Exit:** every balance-acceptance cell is inside target or explicitly accepted with a reason.
|
||||
|
||||
## Milestone 0.10.3 — Stability and quality-of-life (patch)
|
||||
|
||||
- [ ] Fix save/checkpoint edge cases, UI focus traps, gamepad remapping and pause/resume defects
|
||||
reported after 0.10.0.
|
||||
- [ ] Add the small quality-of-life items that need no new system (default focus, key hints,
|
||||
settings persistence).
|
||||
- [ ] Release `0.10.3` with the full procedure.
|
||||
|
||||
**Exit:** no known P1 stability defect remains before the 0.11.0 feature work starts.
|
||||
|
||||
## Milestone 0.11.0 — Systems and content depth (minor)
|
||||
|
||||
Goal: add the first post-1.0-track feature set on top of a stable 0.10.x line: a live incident
|
||||
replay, a fourth venue and the localization foundation.
|
||||
|
||||
### Live body-camera replay
|
||||
|
||||
- [ ] Replace the text-only incident timeline with a replay of recorded positions, actions and
|
||||
timestamps; define the recording format, memory cap and deterministic playback.
|
||||
- [ ] Keep the replay review-only (no new escalation mechanics) and verify it on Compatibility/Web.
|
||||
- [ ] Add a skip path and a captioned summary so replay is never a hard gate on the shift review.
|
||||
|
||||
### Fourth venue
|
||||
|
||||
- [ ] Evaluate and select the fourth real-site venue (candidate: the TCL Chinese Theatre
|
||||
forecourt at 6925 Hollywood Blvd) in an ADR covering layout, access policy, gameplay
|
||||
distinctness and asset budget.
|
||||
- [ ] Build the venue under `content/venues/` with licensed reference, sidecar provenance and the
|
||||
three-up board; enter it from venue selection and the campaign.
|
||||
- [ ] Add venue-specific events, access artifacts and escalation cases; extend the balance matrix
|
||||
to the new cells.
|
||||
|
||||
### Localization foundation
|
||||
|
||||
- [ ] Extract every remaining hardcoded string into the JSON catalogue and make the pseudo-locale
|
||||
length check a CI gate.
|
||||
- [ ] Add one full secondary language (text, layout, font coverage) with the translation workflow
|
||||
documented; no machine-translated release without review.
|
||||
- [ ] Verify layout at maximum font scale in both languages.
|
||||
|
||||
### Release
|
||||
|
||||
- [ ] Re-run the capture gallery, screen-design judge, budget and platform smokes for the new
|
||||
content; release `0.11.0` with the full procedure.
|
||||
|
||||
**Exit:** the replay, the fourth venue and one secondary language ship without regressing the
|
||||
0.10.x performance, accessibility or balance baselines.
|
||||
|
||||
## Milestone 0.11.1 — Replay and venue fixes (patch)
|
||||
|
||||
- [ ] Fix the replay, new-venue and localization defects found in wider play.
|
||||
- [ ] Re-run the new venue's balance cells and the replay capture gate; release `0.11.1`.
|
||||
|
||||
**Exit:** no P1 defect remains in the 0.11.0 feature set.
|
||||
|
||||
## Milestone 0.11.2 — Content and balance polish (patch)
|
||||
|
||||
- [ ] Tune the new venue events and the campaign pacing; fix the remaining content readability
|
||||
issues.
|
||||
- [ ] Refresh the affected captures, boards and asset rows; release `0.11.2`.
|
||||
|
||||
**Exit:** the 0.11.x line is stable enough to freeze its contracts.
|
||||
|
||||
## Milestone 0.12.0 — Platform and distribution (minor)
|
||||
|
||||
Goal: make the game distributable on the launch platforms with signed builds, a frozen save
|
||||
contract and complete localization.
|
||||
|
||||
### Signing and storefronts
|
||||
|
||||
- [ ] Sign and notarize the macOS build; record the exact commands and certificate handling.
|
||||
- [ ] Produce the storefront packages and metadata (rating, privacy, credits, screenshots) for the
|
||||
chosen stores; complete the AI-provenance and commercial-rights review.
|
||||
- [ ] Add the storefront-facing extras that are in scope (achievements, cloud-save decision,
|
||||
controller/Steam Deck verification) behind explicit feature flags.
|
||||
|
||||
### Contracts and hardware
|
||||
|
||||
- [ ] Freeze the save/checkpoint format as version 2 with export/import and forward-migration
|
||||
tests; document the compatibility promise.
|
||||
- [ ] Verify the target-hardware matrix (minimum GPU, Steam Deck, Web browsers, integrated
|
||||
graphics) and publish the measured results.
|
||||
- [ ] Finish the launch languages and the accessibility statement for the shipped feature set.
|
||||
|
||||
### Release
|
||||
|
||||
- [ ] Release `0.12.0` with the full procedure and the storefront artifacts.
|
||||
|
||||
**Exit:** signed, storefront-ready builds exist for every launch platform with a frozen save
|
||||
contract, complete localization and measured hardware support.
|
||||
|
||||
## Milestone 0.12.1 — Distribution fixes (patch)
|
||||
|
||||
- [ ] Fix signing, installer, storefront-metadata and save-migration defects found in the
|
||||
storefront dry runs; release `0.12.1`.
|
||||
|
||||
**Exit:** every storefront build installs, launches and migrates saves cleanly.
|
||||
|
||||
## Milestone 0.13.0 — Content complete (minor)
|
||||
|
||||
Goal: finish the planned content and lock it for the 1.0.0 release candidate.
|
||||
|
||||
### Content and presentation
|
||||
|
||||
- [ ] Finalise all planned venues, events, dialogue and access artifacts; close the content
|
||||
backlog with an explicit accept list.
|
||||
- [ ] Complete recorded audio (VO and music) if the 0.11.0 audio decision funded it; otherwise
|
||||
record the accepted procedural-only direction.
|
||||
- [ ] Finalise the tutorial and first-hour flow against the playtest evidence.
|
||||
|
||||
### Release candidate preparation
|
||||
|
||||
- [ ] Run a full accessibility audit against the shipped features and fix the remaining issues.
|
||||
- [ ] Run the balance acceptance protocol one final time and freeze the tuning.
|
||||
- [ ] Declare content and feature freeze; only release-blocking fixes may land after this point.
|
||||
- [ ] Release `0.13.0` as the content-complete milestone.
|
||||
|
||||
**Exit:** all planned content is implemented and frozen, and no known P0/P1 feature gap remains.
|
||||
|
||||
## Milestone 0.13.1 — Release candidate 1 (patch)
|
||||
|
||||
- [ ] Build release candidates on every platform from a clean worktree; run the complete smoke
|
||||
matrix, save migration, performance and accessibility checks.
|
||||
- [ ] Fix release-blocking defects only; release `0.13.1`.
|
||||
|
||||
**Exit:** a release candidate exists that passes every gate except final legal/storefront
|
||||
approval.
|
||||
|
||||
## Milestone 0.13.2 — Release candidate 2 (patch)
|
||||
|
||||
- [ ] Verify the 0.13.1 fixes on the target matrix and re-run the full gallery and judge.
|
||||
- [ ] Freeze the release notes, credits, privacy and known-limitations documents; release
|
||||
`0.13.2` as the final candidate.
|
||||
|
||||
**Exit:** the candidate is build-reproducible, documented and awaiting only the final 1.0.0
|
||||
release decision.
|
||||
|
||||
## Milestone 1.0.0 — Release (major)
|
||||
|
||||
Goal: publish the first stable release with a documented compatibility promise.
|
||||
|
||||
- [ ] Confirm legal, privacy, rating and storefront approval is complete in writing.
|
||||
- [ ] Confirm the hardware matrix, accessibility audit and localization sign-off are complete.
|
||||
- [ ] Confirm no P0/P1 defects remain and every P2 is a documented known limitation.
|
||||
- [ ] Update `README.md`, `PLAN.md`, `docs/development-status.md`, `CHANGELOG.md`, `CREDITS.md`,
|
||||
`PRIVACY.md` and the release notes for 1.0.0.
|
||||
- [ ] Run the full release procedure: version 1.0.0, changelog, clean build, checksum
|
||||
verification, tag, push and publish.
|
||||
- [ ] Record the post-release state in `MEMORY.md` and open the 1.0.x maintenance list.
|
||||
|
||||
**Exit:** 1.0.0 is published on every launch platform with complete documentation, verified
|
||||
checksums and no open release blocker.
|
||||
|
||||
## Future / deferred (not scheduled)
|
||||
|
||||
- [ ] Seasonal events or a fifth venue after the 0.11.0 fourth venue proves the venue pipeline.
|
||||
- [ ] Real voice/dialogue audio if the 0.13.0 audio decision does not fund it; ambience, cues and
|
||||
radio crackle stay procedural until then.
|
||||
- [ ] 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.
|
||||
- [ ] A second Chinese Theatre/forecourt style venue if the fourth venue is not enough.
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
"attach": "back",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.161,
|
||||
0.15,
|
||||
0.296,
|
||||
0.212
|
||||
0.172
|
||||
],
|
||||
"min": [
|
||||
-0.161,
|
||||
-0.15,
|
||||
-0.12,
|
||||
-0.319
|
||||
]
|
||||
@@ -46,7 +46,7 @@
|
||||
],
|
||||
"path": "assets/models/blender/npc_gear/backpack.glb",
|
||||
"triangles": 380,
|
||||
"vertices": 408
|
||||
"vertices": 410
|
||||
},
|
||||
"bench": {
|
||||
"bounds_godot": {
|
||||
@@ -98,14 +98,14 @@
|
||||
"attach": "hand",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.106,
|
||||
0.376,
|
||||
0.065,
|
||||
0.06,
|
||||
0.143
|
||||
],
|
||||
"min": [
|
||||
-0.106,
|
||||
-0.065,
|
||||
-0.053,
|
||||
-0.055
|
||||
0.005
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
@@ -114,21 +114,21 @@
|
||||
"lens"
|
||||
],
|
||||
"path": "assets/models/blender/npc_gear/camera.glb",
|
||||
"triangles": 318,
|
||||
"vertices": 406
|
||||
"triangles": 312,
|
||||
"vertices": 422
|
||||
},
|
||||
"cap": {
|
||||
"attach": "head",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.168,
|
||||
0.209,
|
||||
0.258
|
||||
0.108,
|
||||
0.204,
|
||||
0.165
|
||||
],
|
||||
"min": [
|
||||
-0.168,
|
||||
0.037,
|
||||
-0.186
|
||||
-0.108,
|
||||
0.094,
|
||||
-0.119
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
@@ -264,14 +264,14 @@
|
||||
"attach": "head",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.26,
|
||||
0.245,
|
||||
0.26
|
||||
0.166,
|
||||
0.227,
|
||||
0.166
|
||||
],
|
||||
"min": [
|
||||
-0.26,
|
||||
0.035,
|
||||
-0.351
|
||||
-0.166,
|
||||
0.092,
|
||||
-0.225
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
@@ -333,14 +333,14 @@
|
||||
"attach": "head",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.184,
|
||||
0.236,
|
||||
0.123
|
||||
0.118,
|
||||
0.221,
|
||||
0.079
|
||||
],
|
||||
"min": [
|
||||
-0.184,
|
||||
-0.146,
|
||||
-0.225
|
||||
-0.118,
|
||||
-0.024,
|
||||
-0.144
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
@@ -428,16 +428,17 @@
|
||||
"vertices": 648
|
||||
},
|
||||
"npc_casual": {
|
||||
"base": "hoodie",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.321,
|
||||
1.824,
|
||||
0.189
|
||||
0.228,
|
||||
1.866,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.321,
|
||||
0,
|
||||
-0.178
|
||||
-0.228,
|
||||
-0.004,
|
||||
-0.091
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -445,11 +446,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Eyebrows",
|
||||
"Eye",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_casual.glb",
|
||||
@@ -466,20 +467,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5034,
|
||||
"vertices": 4607
|
||||
"triangles": 6206,
|
||||
"vertices": 12988
|
||||
},
|
||||
"npc_coat": {
|
||||
"base": "hoodie",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.332,
|
||||
1.824,
|
||||
0.189
|
||||
0.228,
|
||||
1.866,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.332,
|
||||
0,
|
||||
-0.18
|
||||
-0.228,
|
||||
-0.004,
|
||||
-0.091
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -487,11 +489,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Eyebrows",
|
||||
"Eye",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_coat.glb",
|
||||
@@ -507,20 +509,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 6046,
|
||||
"vertices": 5645
|
||||
"triangles": 6206,
|
||||
"vertices": 12988
|
||||
},
|
||||
"npc_crowd_a": {
|
||||
"base": "casual",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.288,
|
||||
1.825,
|
||||
0.189
|
||||
0.18,
|
||||
1.843,
|
||||
0.271
|
||||
],
|
||||
"min": [
|
||||
-0.288,
|
||||
0,
|
||||
-0.195
|
||||
-0.18,
|
||||
-0.009,
|
||||
-0.099
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -528,11 +531,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Hair_Brown",
|
||||
"Brown",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_crowd_a.glb",
|
||||
@@ -547,20 +550,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5284,
|
||||
"vertices": 4908
|
||||
"triangles": 6424,
|
||||
"vertices": 13440
|
||||
},
|
||||
"npc_crowd_b": {
|
||||
"base": "casual",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.358,
|
||||
1.809,
|
||||
0.189
|
||||
0.18,
|
||||
1.843,
|
||||
0.271
|
||||
],
|
||||
"min": [
|
||||
-0.358,
|
||||
0,
|
||||
-0.155
|
||||
-0.18,
|
||||
-0.009,
|
||||
-0.099
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -568,11 +572,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Hair_Brown",
|
||||
"Brown",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_crowd_b.glb",
|
||||
@@ -587,20 +591,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 4812,
|
||||
"vertices": 4550
|
||||
"triangles": 6424,
|
||||
"vertices": 13440
|
||||
},
|
||||
"npc_crowd_c": {
|
||||
"base": "hoodie",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.332,
|
||||
1.824,
|
||||
0.189
|
||||
0.228,
|
||||
1.866,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.332,
|
||||
0,
|
||||
-0.18
|
||||
-0.228,
|
||||
-0.004,
|
||||
-0.091
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -608,11 +613,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Eyebrows",
|
||||
"Eye",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_crowd_c.glb",
|
||||
@@ -627,20 +632,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5690,
|
||||
"vertices": 5510
|
||||
"triangles": 6206,
|
||||
"vertices": 12988
|
||||
},
|
||||
"npc_crowd_d": {
|
||||
"base": "worker",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.293,
|
||||
1.856,
|
||||
0.189
|
||||
0.217,
|
||||
1.826,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.293,
|
||||
0,
|
||||
-0.225
|
||||
-0.217,
|
||||
-0.004,
|
||||
-0.065
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -648,11 +654,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"hair",
|
||||
"rubber"
|
||||
"Moustache",
|
||||
"Eye",
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_crowd_d.glb",
|
||||
@@ -667,20 +673,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5810,
|
||||
"vertices": 5444
|
||||
"triangles": 4458,
|
||||
"vertices": 9370
|
||||
},
|
||||
"npc_dress": {
|
||||
"base": "casual",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.288,
|
||||
1.825,
|
||||
0.208
|
||||
0.18,
|
||||
1.843,
|
||||
0.271
|
||||
],
|
||||
"min": [
|
||||
-0.288,
|
||||
0,
|
||||
-0.208
|
||||
-0.18,
|
||||
-0.009,
|
||||
-0.099
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -688,11 +695,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Hair_Brown",
|
||||
"Brown",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_dress.glb",
|
||||
@@ -706,20 +713,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5540,
|
||||
"vertices": 5073
|
||||
"triangles": 6424,
|
||||
"vertices": 13440
|
||||
},
|
||||
"npc_firefighter": {
|
||||
"base": "worker",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.37,
|
||||
1.819,
|
||||
0.213
|
||||
0.217,
|
||||
1.826,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.37,
|
||||
0,
|
||||
-0.213
|
||||
-0.217,
|
||||
-0.004,
|
||||
-0.065
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -727,13 +735,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"hair",
|
||||
"hivis",
|
||||
"reflective",
|
||||
"rubber"
|
||||
"Moustache",
|
||||
"Eye",
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_firefighter.glb",
|
||||
@@ -747,20 +753,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5888,
|
||||
"vertices": 5785
|
||||
"triangles": 4458,
|
||||
"vertices": 9370
|
||||
},
|
||||
"npc_guard": {
|
||||
"base": "worker",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.361,
|
||||
1.819,
|
||||
0.189
|
||||
0.217,
|
||||
1.826,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.361,
|
||||
0,
|
||||
-0.172
|
||||
-0.217,
|
||||
-0.004,
|
||||
-0.065
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -768,15 +775,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"hair",
|
||||
"rubber",
|
||||
"leather",
|
||||
"steel",
|
||||
"patch",
|
||||
"plastic_black"
|
||||
"Moustache",
|
||||
"Eye",
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_guard.glb",
|
||||
@@ -791,20 +794,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5670,
|
||||
"vertices": 5671
|
||||
"triangles": 4458,
|
||||
"vertices": 9370
|
||||
},
|
||||
"npc_hoodie": {
|
||||
"base": "hoodie",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.326,
|
||||
1.856,
|
||||
0.189
|
||||
0.228,
|
||||
1.866,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.326,
|
||||
0,
|
||||
-0.225
|
||||
-0.228,
|
||||
-0.004,
|
||||
-0.091
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -812,11 +816,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Eyebrows",
|
||||
"Eye",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_hoodie.glb",
|
||||
@@ -832,20 +836,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5864,
|
||||
"vertices": 5325
|
||||
"triangles": 6206,
|
||||
"vertices": 12988
|
||||
},
|
||||
"npc_jacket": {
|
||||
"base": "casual",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.332,
|
||||
1.824,
|
||||
0.189
|
||||
0.18,
|
||||
1.843,
|
||||
0.271
|
||||
],
|
||||
"min": [
|
||||
-0.332,
|
||||
0,
|
||||
-0.178
|
||||
-0.18,
|
||||
-0.009,
|
||||
-0.099
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -853,11 +858,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Hair_Brown",
|
||||
"Brown",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_jacket.glb",
|
||||
@@ -873,34 +878,35 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5432,
|
||||
"vertices": 5225
|
||||
"triangles": 6424,
|
||||
"vertices": 13440
|
||||
},
|
||||
"npc_vest": {
|
||||
"base": "worker",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.321,
|
||||
1.819,
|
||||
0.189
|
||||
0.217,
|
||||
1.863,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.321,
|
||||
0,
|
||||
-0.172
|
||||
-0.217,
|
||||
-0.004,
|
||||
-0.103
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
"layer": "vest",
|
||||
"materials": [
|
||||
"skin",
|
||||
"Worker_Yellow",
|
||||
"Worker_Vest",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"hair",
|
||||
"rubber",
|
||||
"hivis",
|
||||
"reflective"
|
||||
"Moustache",
|
||||
"Eye",
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_vest.glb",
|
||||
@@ -914,8 +920,8 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5532,
|
||||
"vertices": 5195
|
||||
"triangles": 5240,
|
||||
"vertices": 10936
|
||||
},
|
||||
"palm_tree": {
|
||||
"bounds_godot": {
|
||||
@@ -1239,14 +1245,14 @@
|
||||
"attach": "head",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.101,
|
||||
0.059,
|
||||
0.158
|
||||
0.065,
|
||||
0.108,
|
||||
0.101
|
||||
],
|
||||
"min": [
|
||||
-0.101,
|
||||
0.011,
|
||||
0.003
|
||||
-0.065,
|
||||
0.077,
|
||||
0.002
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,33 @@
|
||||
# Quaternius build sources (CC0 1.0)
|
||||
|
||||
These are the **build inputs** for the shipped NPC roster, not runtime assets.
|
||||
`blender_src/.gdignore` keeps Godot from importing this directory.
|
||||
|
||||
Source: Quaternius *Ultimate Modular Characters* and *Ultimate Modular Women*
|
||||
(<https://quaternius.com/packs/ultimatemodularcharacters.html>), licence CC0 1.0,
|
||||
attribution recorded in the repository `CREDITS.md`.
|
||||
|
||||
| File | SHA-256 | Bones | Meshes |
|
||||
| --- | --- | --- | --- |
|
||||
| `Casual.glb` | `bc2d84fba249ebaa0629cb9d133ad37234196e4b8004aa205a6397abed04e05c` | 62 | 4 skinned + Icosphere helper |
|
||||
| `Casual_Hoodie.glb` | `f5b722199f7628b24550989af4230f65d5230727df9c326fd2b3e360effe5249` | 62 | 4 skinned + Icosphere helper |
|
||||
| `Worker.glb` | `8d0f454336c2effc5c08f63a177af3a82d1a7af025ca9588b40e4c90c4703534` | 62 | 4 skinned + Icosphere helper |
|
||||
|
||||
Each file also carries 24 animation clips; the build exports **no** animation data, so
|
||||
the shipped GLBs contain the mesh, the 62-joint skin and the material slots only.
|
||||
|
||||
Reproducing the shipped characters:
|
||||
|
||||
flatpak run org.blender.Blender --background \
|
||||
--python "$PWD/tools/blender_assets.py" -- build --only npc
|
||||
|
||||
Upstream quirks the build compensates for (see `docs/npc-model-sourcing.md`):
|
||||
|
||||
1. every mesh ships a **degenerate UV map** (all loops at `0,0`) with flat-colour
|
||||
materials, so the build Smart-UV-unwraps and repacks before export;
|
||||
2. the meshes must be joined **without welding** — `remove_doubles` corrupts the
|
||||
skin weights;
|
||||
3. the rest pose is a T-pose, so the arm-down rest is baked by
|
||||
`_bake_quaternius_rest()`;
|
||||
4. the upstream material names differ per file, so each source carries a slot map
|
||||
down to the project's five tint slots.
|
||||
@@ -0,0 +1,96 @@
|
||||
# ADR-0018: Quaternius Stage 2 — adopt source proportions at runtime
|
||||
|
||||
- **Status:** Accepted (2026-11-05, owner chose option 2)
|
||||
- **Date:** 2026-11-05
|
||||
- **Supersedes:** ADR-0017 decision 2 (Stage 1 keeps the 13-core contract)
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0017 made Quaternius the NPC base and staged the migration: Stage 1 bakes a
|
||||
proportion-warped mesh onto the existing 13-core contract; Stage 2 adopts the
|
||||
62-joint rig at runtime only if the warp fails.
|
||||
|
||||
The Stage 1 proportion warp was executed and measured (`docs/npc-model-sourcing.md`
|
||||
§Rest-pose and proportion-warp measurement). The arm-down rest pose bakes cleanly, but the
|
||||
warp to the contract proportions does not:
|
||||
|
||||
| Joint | Quaternius | Contract | Delta |
|
||||
| --- | --- | --- | --- |
|
||||
| Hips height | 0.869 | 0.62 | −0.249 |
|
||||
| Shoulder x | 0.148 | 0.27 | +0.122 |
|
||||
| Arm (shoulder→hand tip) | 0.429 | 0.70 | ×1.63 |
|
||||
| Leg (hip→ankle) | 0.835 | 0.62 | ×0.74 |
|
||||
| Torso (hip→head) | 0.745 | 1.20 | ×1.61 |
|
||||
|
||||
The vision judge reads the warped mesh as distorted: paddle/claw hands, forearms lost into the
|
||||
torso, stretched shins, "long legs + short torso". The arm either stays short (a shortened arm
|
||||
against the contract hand) or stretches 1.63× (a distorted deltoid + hand). Both are the gate's
|
||||
rejection conditions.
|
||||
|
||||
## Decision
|
||||
|
||||
Stop the Stage 1 rollout and **adopt the Quaternius 62-joint rig at runtime** (owner chose
|
||||
option 2). The mesh is not proportion-warped: it keeps its source LOD (3,199 welded verts /
|
||||
6,206 tris) and only the arm-down rest pose is baked.
|
||||
|
||||
- `NpcAvatar` maps the Quaternius joint names instead of the 13-core names.
|
||||
- Gear attach points are re-derived from the Quaternius skeleton (hip ≈0.87, shoulder ≈0.148/1.39,
|
||||
wrist ≈0.99, knee ≈0.52).
|
||||
- The Quaternius Idle and Walk clips are used directly — they are authored for the 62-joint rig,
|
||||
so no retargeting pass is needed. Combat/gun/sword clips stay unused.
|
||||
|
||||
Option 1 (keep the 13-core names with Quaternius rest positions) is rejected in favour of the
|
||||
full source rig.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- The Quaternius silhouette, deltoid, hand and leg proportions are preserved — no paddle hands,
|
||||
no shortened arm, no torso/leg distortion.
|
||||
- The mesh stays at source LOD (far under the 24,000 cap), so no decimation is needed.
|
||||
|
||||
### Negative
|
||||
|
||||
- The runtime contract numbers (hip 0.62, shoulder 0.27, hand 0.78) change; every gear attach
|
||||
point, `npc_proportions` guide, fit measurement and pose-composition constant must be
|
||||
re-derived and re-tested.
|
||||
- `NpcAvatar`'s bone mapping and pose composition are rewritten for the 62-joint rig, a larger
|
||||
change than the mesh swap.
|
||||
- The 12-variant rollout and the 12×6 baseline are still blocked until the new rest positions
|
||||
and gear fit are owner-approved.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **Ship the warped mesh.** Rejected by the gate: the 1.63× arm / 0.74× leg warp visibly
|
||||
distorts and the owner directed keeping the Quaternius as close to source as possible.
|
||||
- **Keep the old contract and accept a short arm.** Rejected for the same reason: the hand ends
|
||||
at ≈0.99 instead of 0.78, so handheld gear floats unless attach points move — which is option 1.
|
||||
- **Abandon Quaternius and keep the procedural generator.** Rejected in ADR-0017.
|
||||
|
||||
## Open question
|
||||
|
||||
Resolved: option 2 (62-joint runtime) is the owner's choice. The first implementation step is
|
||||
the `NpcAvatar` bone-map + gear-attach re-derivation, then the slot/AO bake and the 12-variant
|
||||
rollout.
|
||||
|
||||
## Follow-up decision: full roster swap
|
||||
|
||||
- **Date:** 2026-11-05 (owner, at the `0.9.9` face/silhouette gate)
|
||||
|
||||
The owner chose **option (a) full swap** from
|
||||
[`docs/screens/npc-face-gate/`](../../screens/npc-face-gate/README.md): every roster character
|
||||
moves onto the Quaternius base, and the procedural generator plus the MakeHuman head retire to
|
||||
the tested fallback rather than staying in the shipped roster.
|
||||
|
||||
Scope consequences:
|
||||
|
||||
- The shipped roster keeps its twelve public character names and `CHARACTER_POOLS` mapping; the
|
||||
geometry behind them comes from the three CC0 full-body sources already researched
|
||||
(`Casual`, `Casual_Hoodie`, `Worker`), aliased per role.
|
||||
- Variety now comes from the five tint slots, the gear library and the per-role outfit rules
|
||||
instead of twelve distinct meshes. Roster silhouettes are therefore *less* varied than the
|
||||
procedural library until the modular part library (hair, outfits) is added — tracked as a
|
||||
`0.9.10` task, not hidden.
|
||||
- The procedural library, `_build_body()` and the MakeHuman head stay in the tree and tested as
|
||||
the instant fallback; no MPFB or Stage-1 proportion-warp work reopens.
|
||||
@@ -21,3 +21,4 @@ Each ADR records the context, decision, consequences and alternatives for a dura
|
||||
- [0015-npc-model-variety-sourcing](0015-npc-model-variety-sourcing.md)
|
||||
- [0016-npc-character-budget-24k](0016-npc-character-budget-24k.md)
|
||||
- [0017-quaternius-npc-base](0017-quaternius-npc-base.md)
|
||||
- [0018-quaternius-stage2-runtime-rig](0018-quaternius-stage2-runtime-rig.md)
|
||||
@@ -142,11 +142,22 @@ render about 2.5 min.
|
||||
|
||||
## Characters
|
||||
|
||||
All library variants, including the incoming Quaternius-derived base, share the
|
||||
All library variants share the
|
||||
[character and gear asset contract](architecture.md#character-and-gear-asset-contract):
|
||||
`npc_base` mesh node, required 13 core bones (optional extras allowed), five
|
||||
tint slots, +Z visual front, 24,000/24,000 character budget and the same 256²
|
||||
slot-map/AO rules. Role selection and look notes:
|
||||
`npc_base` mesh node, five tint slots, +Z visual front, 24,000/24,000 character budget and the
|
||||
same 256² slot-map/AO rules. The shipped roster is on the 62-joint Quaternius skeleton
|
||||
(ADR-0018); the procedural fallback built by `build --source procedural` keeps the 13 core bones.
|
||||
Both are accepted by `audit`, `inspect`, the runtime bone map and the tests.
|
||||
|
||||
The three Quaternius sources (`casual`, `hoodie`, `worker`) live in
|
||||
`assets/models/blender_src/quaternius/` as build inputs. `build` (default
|
||||
`--source quaternius`) does a two-pass bake per character: a cached T-pose round trip that joins
|
||||
without welding, maps the upstream materials onto the five slots and unwraps the degenerate
|
||||
source UVs, then an arm-down rest bake exported with `export_apply=False`. The per-character slot
|
||||
map, triangle counts and measurements are in
|
||||
[`npc-model-sourcing.md#shipped-roster-099-owner-chose-the-full-swap`](npc-model-sourcing.md#shipped-roster-099-owner-chose-the-full-swap).
|
||||
|
||||
Role selection and look notes (tint + gear now carry the role, not the mesh):
|
||||
|
||||
| Character | `layer_for()` / look | Suggested roles | Preview gear |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -165,6 +176,11 @@ colour texture. The current `_apply_outfit_materials()` sets
|
||||
`albedo_color = WHITE` on any slot it does not recognise, and these keep their
|
||||
colour through that. When integrating, tint only the five contract slots.
|
||||
|
||||
**Head gear fit.** Head gear is authored for the shipped Quaternius head:
|
||||
`HEAD_GEAR_SCALE = 0.64` (0.201 m base head against the 0.315 m procedural one) and
|
||||
`HEAD_GEAR_LIFT = 0.07` (its centre sits higher). The procedural fallback therefore wears slightly
|
||||
undersized caps — cosmetic and fallback-only.
|
||||
|
||||
**Runtime tint notes (implemented).** Only the five contract slots are tinted;
|
||||
fixed detail materials keep their authored colour. On `npc_vest` the vest is the
|
||||
fixed `hivis` material and `cloth` is the undershirt, so the yellow override is
|
||||
|
||||
@@ -34,9 +34,14 @@ and the active order/gaps in [`TODO.md`](../TODO.md).
|
||||
## Known gaps
|
||||
|
||||
- External first-time-player sessions and balance sweeps are not completed.
|
||||
- The Quaternius NPC base migration is planned, not shipped. The current 12-character
|
||||
procedural library and the MakeHuman head remain fallbacks until the Quaternius
|
||||
12-variant baseline passes review.
|
||||
- The Quaternius roster is shipped (`0.9.9`): all twelve characters are baked from the three CC0
|
||||
sources (casual tee, hoodie, work clothes), 4,458–6,424 triangles each, 62-joint rig at source
|
||||
proportions, arm-down rest, five tint slots, unwrapped UVs and baked 256² AO. The procedural
|
||||
library and the MakeHuman head remain the tested fallback.
|
||||
- Roster silhouette variety is narrower than the procedural library (three meshes for twelve
|
||||
roles, differentiated by tint and gear) until the modular part library is added.
|
||||
- Head gear is authored for the shipped Quaternius head; the procedural fallback therefore wears
|
||||
slightly undersized caps. Cosmetic, fallback-only.
|
||||
- Accessibility/localization acceptance, target-hardware profiling, macOS
|
||||
signing/notarization and AI/storefront legal approval remain open.
|
||||
- Live 3D replay, recorded VO/music, more venues and seasonal content are future
|
||||
|
||||
@@ -61,6 +61,14 @@ The Ducz mirror is a browser builder over the same packs; it adds 21
|
||||
outfits and a shared skeleton. The three full GLBs are the initial scope; the
|
||||
21-part builder is a fallback for role coverage, not the default.
|
||||
|
||||
**Initial outfit scope (2026-11-05):** the three full GLBs (`Casual`,
|
||||
`Casual_Hoodie`, `Worker`) are the canonical-base and silhouette-gate scope.
|
||||
They cover only ~3 of the 12 roles (casual, hood, vest); the jacket, coat,
|
||||
dress, responder (firefighter) and guard looks are not in the three GLBs, so
|
||||
§4 roster coverage must pull those from the 21-part modular builder or author
|
||||
new clothing. Do not start the modular builder until the canonical base passes
|
||||
§3.
|
||||
|
||||
The first prototype measured 12,518 imported vertices, 3,199 after welding and
|
||||
2,423 vertices / 4,654 triangles after a 0.75 decimate. Idle and Walk read well.
|
||||
The source is a T-pose with a 62-joint realistic rig, roughly 0.96 m hip,
|
||||
@@ -70,6 +78,207 @@ The 24,000 cap leaves room to stay much closer to the source LOD than the
|
||||
prototype's 4,654 triangles. Do not decimate to the old 7,000-triangle target
|
||||
before the silhouette gate.
|
||||
|
||||
## Quaternius GLB inventory (2026-11-05)
|
||||
|
||||
All three GLBs share one 62-joint rig and the same 24 animation clips; only the meshes and
|
||||
materials differ. Each import also adds a 42-vert/80-tri `Icosphere` bone-display helper — ignore it.
|
||||
|
||||
### Meshes (verts / tris)
|
||||
|
||||
| GLB | Body | Feet | Head | Legs | Total |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `Casual` | 5,338 / 2,654 | 1,082 / 538 | 5,122 / 2,526 | 1,426 / 706 | 12,968 / 6,424 |
|
||||
| `Casual_Hoodie` | 6,132 / 3,048 | 1,024 / 512 | 3,836 / 1,888 | 1,526 / 758 | 12,518 / 6,206 |
|
||||
| `Worker` | 6,074 / 3,018 | 928 / 464 | 1,862 / 936 | 1,670 / 822 | 10,534 / 5,240 |
|
||||
|
||||
### Joints (62)
|
||||
|
||||
- Spine: `Root, Hips, Torso, Body, Abdomen, Chest, Neck, Head` (8).
|
||||
- Arms: `Shoulder.L/R, UpperArm.L/R, LowerArm.L/R, Wrist.L/R` (8) plus `PT.L/R` (2 pole targets).
|
||||
- Fingers: `Thumb×3, Index×4, Middle×4, Ring×4, Pinky×4` × L/R (30).
|
||||
- Legs: `UpperLeg.L/R, LowerLeg.L/R, Foot.L/R` (6).
|
||||
|
||||
### Animation clips (24)
|
||||
|
||||
`Idle, Idle_Neutral, Walk, Run, Run_Back, Run_Left, Run_Right, Wave, Interact, Death,
|
||||
Gun_Shoot, Idle_Gun, Idle_Gun_Pointing, Idle_Gun_Shoot, Idle_Sword, Run_Shoot, Sword_Slash,
|
||||
HitRecieve, HitRecieve_2, Kick_Left, Kick_Right, Punch_Left, Punch_Right, Roll`.
|
||||
|
||||
The retarget scope is Idle and Walk; combat/gun/sword clips are discarded (see §2).
|
||||
|
||||
### Material → slot map
|
||||
|
||||
The five project slots (`skin`, `cloth`, `pants`, `accessory`, `hair`) are assigned by mesh
|
||||
region first, then material — the same material name can mean `cloth` on the body and
|
||||
`accessory` on the feet.
|
||||
|
||||
| Material | Where | Project slot |
|
||||
| --- | --- | --- |
|
||||
| `Skin` | all meshes | `skin` |
|
||||
| `Hair_Blond`, `Hair_Brown`, `Hair`, `Brown` (head), `Eyebrows`, `Moustache` | Head | `hair` |
|
||||
| `Eye` | Head | `accessory` |
|
||||
| `Worker_Yellow` | Worker body (vest stripe) + head (helmet) | `cloth` / `accessory` |
|
||||
| `White`, `Grey`, `Purple`, `Black` | Body / Feet | `cloth` (body) / `accessory` (feet) |
|
||||
| `Orange`, `LightBlue`, `LightBrown`, `Brown`, `Brown2` | Legs | `pants` |
|
||||
| `Worker_Vest` | Worker body | `cloth` |
|
||||
|
||||
### Canonical base (built 2026-11-05)
|
||||
|
||||
`tmp/npc-research/quaternius_base.glb` is the welded, triangulated, grounded, animation-free
|
||||
canonical base built from `Casual_Hoodie.glb` (the most detailed body) by
|
||||
`tmp/npc-research/build_quaternius_base.py`:
|
||||
|
||||
- one `npc_base` mesh — 3,199 welded verts / 6,206 tris (Body + Feet + Head + Legs joined);
|
||||
- 62 joints, skinned (no unskinned faces);
|
||||
- height 1.81 m, feet at y 0, front on Godot +Z / Blender −Y (T-pose rest);
|
||||
- no animation clips; `inspect` clean (no issues).
|
||||
|
||||
This is the §2 contract-bake input: the 62-joint rig is authoring-only and is mapped to the
|
||||
13-core contract next, not shipped as-is.
|
||||
|
||||
## 62-joint → 13-core joint map (2026-11-05)
|
||||
|
||||
The Quaternius hierarchy (measured from `quaternius_base.glb`):
|
||||
|
||||
- `Root → Body → Hips → Abdomen → Torso → Chest → Neck → Head` (spine);
|
||||
- `Chest → Shoulder.L/R → UpperArm.L/R → LowerArm.L/R → Wrist.L/R → fingers` (arms);
|
||||
- `Body → UpperLeg.L/R → LowerLeg.L/R` and `Root → Foot.L/R` (legs — the foot is a separate
|
||||
child of Root, not of LowerLeg);
|
||||
- `Root → PT.L/R` (IK pole targets, no deforming weights).
|
||||
|
||||
The contract is 14 bones (13 core + `Hips` root): `Hips, Spine, Neck, Head, Shoulder.L/R,
|
||||
Elbow.L/R, Hand.L/R, Hip.L/R, Knee.L/R`. The map:
|
||||
|
||||
| Contract bone | Quaternius source | Notes |
|
||||
| --- | --- | --- |
|
||||
| `Hips` | `Hips` | `Root` + `Body` collapse into `Hips` |
|
||||
| `Spine` | `Abdomen`, `Torso`, `Chest` | three collapse into one |
|
||||
| `Neck` | `Neck` | |
|
||||
| `Head` | `Head` | |
|
||||
| `Shoulder.L/R` | `UpperArm.L/R` | the collar `Shoulder.L/R` collapses into `Spine` |
|
||||
| `Elbow.L/R` | `LowerArm.L/R` | |
|
||||
| `Hand.L/R` | `Wrist.L/R` | |
|
||||
| `Hip.L/R` | `UpperLeg.L/R` | |
|
||||
| `Knee.L/R` | `LowerLeg.L/R` | |
|
||||
|
||||
Optional extras (allowed by the contract; kept if they survive within 24,000 vertices):
|
||||
|
||||
- the 30 finger bones (`Thumb×3, Index×4, Middle×4, Ring×4, Pinky×4` × L/R) → optional finger
|
||||
bones;
|
||||
- `Foot.L/R` → optional toe bones.
|
||||
|
||||
Dropped (never shipped): `PT.L/R` (pole targets carry no deforming weights), and the
|
||||
`Root`/`Body`/collar-`Shoulder` collapse bones above.
|
||||
|
||||
## Rest-pose and proportion-warp measurement (2026-11-05)
|
||||
|
||||
The rest-pose bake (T-pose → arm-down) works: rotate the `UpperArm.L/R` pose bones −90° around
|
||||
their local X, apply the Armature modifier to bake the deformation, then `armature_apply` to bake
|
||||
it as the new rest pose (see `tmp/npc-research/warp_full.py`). The mesh deforms cleanly to the
|
||||
arm-down pose (deformed bounds x ±0.221 vs the T-pose ±0.811).
|
||||
|
||||
The proportion warp to the contract positions is measured and it **distorts the silhouette**:
|
||||
|
||||
| Joint | Quaternius | Contract | Delta |
|
||||
| --- | --- | --- | --- |
|
||||
| Hips height | 0.869 | 0.62 | −0.249 (lower hip) |
|
||||
| Head top | 1.614 | 1.82 | +0.206 (raise head) |
|
||||
| Shoulder x | 0.148 | 0.27 | +0.122 (widen) |
|
||||
| Arm (shoulder→wrist) | 0.402 | 0.60 | ×1.49 |
|
||||
| Arm (shoulder→hand tip) | 0.429 | 0.70 | ×1.63 |
|
||||
| Leg (hip→ankle) | 0.835 | 0.62 | ×0.74 |
|
||||
| Torso (hip→head) | 0.745 | 1.20 | ×1.61 |
|
||||
|
||||
The vision judge reads the warped mesh as: paddle/claw hands (the 1.63× arm stretch blows the
|
||||
hand out), forearms lost into the torso, stretched shins, and "long legs + short torso". The arm
|
||||
must either stay short (a "shortened arm" vs the contract hand) or stretch 1.63× (a distorted
|
||||
deltoid + hand) — both are the gate's rejection conditions.
|
||||
|
||||
**Finding:** the Stage 1 proportion warp cannot preserve the Quaternius silhouette at the
|
||||
contract proportions; the gate fails and Stage 2 (a 62-joint runtime ADR) is indicated. Do not
|
||||
start the 12-variant rebuild on the warped base.
|
||||
|
||||
[ADR-0018](adr/0018-quaternius-stage2-runtime-rig.md) records the Stage 2 decision (accepted
|
||||
2026-11-05, owner chose option 2): adopt the **Quaternius 62-joint rig at runtime**, keep the
|
||||
mesh at source LOD, re-derive gear attach points from the Quaternius skeleton, and use the
|
||||
Quaternius Idle/Walk clips directly (no retarget).
|
||||
|
||||
## Pilot bake and shipped state (2026-11-05)
|
||||
|
||||
`npc_casual` is the first Quaternius-derived character. The reproducible pipeline lives in
|
||||
`tmp/npc-research/` (local tooling, not shipped) and has two steps:
|
||||
|
||||
1. `build_base_minimal.py` — import `quaternius/Casual_Hoodie.glb`, join the four skinned meshes
|
||||
**without welding**, rename the upstream materials to the project tint slots, Smart-UV-unwrap
|
||||
and repack, export `quaternius_tpose.glb`.
|
||||
2. `bake_armdown_v5.py` — pose `UpperArm.L/R` −90° local X, read the posed matrices, apply the
|
||||
Armature modifier, **reset the pose to identity**, set the rest matrices, re-add the modifier,
|
||||
export `quaternius_base.glb` and save `assets/models/blender_src/npc_casual.blend` for the AO
|
||||
bake.
|
||||
|
||||
Then `tools/bake_ao.py --only npc_casual` and a copy of the GLB to
|
||||
`assets/models/blender/npc/npc_casual.glb`.
|
||||
|
||||
| Measurement | npc_casual (Quaternius) | procedural library |
|
||||
| --- | --- | --- |
|
||||
| Triangles | 6,206 | 4,812–6,046 |
|
||||
| glTF vertices | 12,988 | 4,908–5,785 |
|
||||
| Bones | 62 | 13 |
|
||||
| Head height / body height | 0.134 | 0.16–0.22 (stylized band) |
|
||||
| Shoulder width | 0.201 m head width, `fit` OK | `fit` OK |
|
||||
| Texel density (256² maps) | 73–74 px/m | 75–96 px/m |
|
||||
| Materials | `skin`, `cloth`, `pants`, `hair`, `accessory` + fixed `Eye`, `Eyebrows` | five slots + fixed detail |
|
||||
|
||||
Two source quirks shaped the bake:
|
||||
|
||||
- the upstream UV map is degenerate (every loop at 0,0) and its materials are flat colours, so the
|
||||
AO/texture bake must follow a real unwrap or it silently writes a black map;
|
||||
- the upstream materials are named `Skin`/`Purple`/`LightBlue`/`White`/`Hair`, so the five-slot
|
||||
contract is applied as a rename, not a re-author.
|
||||
|
||||
The face and silhouette gate sheet is in [`screens/npc-face-gate/`](screens/npc-face-gate/README.md).
|
||||
|
||||
## Shipped roster (0.9.9, owner chose the full swap)
|
||||
|
||||
Every public character name is baked from one of the three CC0 sources. The build is
|
||||
`tools/blender_assets.py build` (default `--source quaternius`); the sources live in
|
||||
`assets/models/blender_src/quaternius/` and the geometry pipeline is two passes:
|
||||
|
||||
1. `_quaternius_tpose_glb()` — import, drop the Icosphere helper, join **without welding**,
|
||||
repoint the upstream materials onto the five tint slots, Smart-UV-unwrap + repack, export an
|
||||
intermediate T-pose GLB. The round trip also normalises the mirrored upstream bone rolls,
|
||||
which is what makes the next pass symmetric.
|
||||
2. `_bake_quaternius_rest()` — pose `UpperArm.L/R` -90° local X, read the posed matrices, apply
|
||||
the Armature modifier, reset the pose, write the rest matrices, re-add the modifier, export
|
||||
with `export_apply=False`.
|
||||
|
||||
Then `tools/bake_ao.py --only <name>`.
|
||||
|
||||
| Character | Source | Tris | Material slots |
|
||||
| --- | --- | --- | --- |
|
||||
| `npc_casual` | hoodie | 6,206 | skin, cloth, pants, hair, accessory + Eye, Eyebrows |
|
||||
| `npc_jacket` | casual | 6,424 | five slots + eyes/brows |
|
||||
| `npc_coat` | hoodie | 6,206 | five slots + eyes/brows |
|
||||
| `npc_vest` | worker | 5,240 | five slots + hivis vest/hat, eyes, moustache |
|
||||
| `npc_dress` | casual | 6,424 | five slots + eyes/brows |
|
||||
| `npc_hoodie` | hoodie | 6,206 | five slots + eyes/brows |
|
||||
| `npc_firefighter` | worker (vest + hat dropped) | 4,458 | five slots + eyes, moustache |
|
||||
| `npc_guard` | worker (vest + hat dropped) | 4,458 | five slots + eyes, moustache |
|
||||
| `npc_crowd_a` | casual | 6,424 | five slots + eyes/brows |
|
||||
| `npc_crowd_b` | casual | 6,424 | five slots + eyes/brows |
|
||||
| `npc_crowd_c` | hoodie | 6,206 | five slots + eyes/brows |
|
||||
| `npc_crowd_d` | worker (vest + hat dropped) | 4,458 | five slots + eyes, moustache |
|
||||
|
||||
Source slot maps (`QUATERNIUS_SOURCES`): the `casual` tee is `Skin`/`White`/`Orange`/`Grey`/
|
||||
`Hair_Blond`; the hoodie is `Skin`/`Purple`/`LightBlue`/`Hair`/`White`; the worker is `Skin`/
|
||||
`LightBrown`/`Brown`+`Brown2`/`Grey`+`Black`/`Eyebrows`. Everything not in the map keeps its
|
||||
authored colour (eyes, brows, moustache, hivis vest and hard hat), so the contract still holds if
|
||||
upstream adds a material — it shows up as an extra fixed-colour slot instead of vanishing.
|
||||
|
||||
Head gear is authored for this head: `HEAD_GEAR_SCALE = 0.64` and `HEAD_GEAR_LIFT = 0.07` map the
|
||||
0.315 m procedural head onto the 0.201–0.214 m Quaternius head. The procedural fallback therefore
|
||||
wears slightly small hats.
|
||||
|
||||
## Contract mismatch
|
||||
|
||||
The expensive part is the rig and proportions, not the mesh. The source has a
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# NPC face and silhouette gate — Quaternius pilot (2026-11-05)
|
||||
|
||||
This page is the owner decision point for
|
||||
[ADR-0017](../../adr/0017-quaternius-npc-base.md) + [ADR-0018](../../adr/0018-quaternius-stage2-runtime-rig.md)
|
||||
stage 2. It answers one question: **which head ships?**
|
||||
|
||||
`npc_casual` is now the CC0 Quaternius `Casual_Hoodie` base baked to the project contract
|
||||
(arm-down rest, five tint slots, 62-joint rig at source proportions). The procedural library with
|
||||
the MakeHuman head is unchanged and still ships for the other eleven characters.
|
||||
|
||||
## The sheet
|
||||
|
||||

|
||||
|
||||
Rows: the Quaternius base, the procedural library with the MakeHuman head, and a procedural crowd
|
||||
variant. Columns: front, three-quarter, profile, and the crowd-distance silhouette the player
|
||||
actually sees from 8 m.
|
||||
|
||||
Source frames: `godot --path . --script res://src/test/capture_npc_baseline.gd -- --out=res://tmp/npc-gate`.
|
||||
|
||||
## What the sheet shows
|
||||
|
||||
| | Head height / body | Face read at portrait | Crowd-distance read |
|
||||
| --- | --- | --- | --- |
|
||||
| Quaternius base | 0.134 | clean low-poly brows, nose, jaw, layered hair | slim, upright, clearly a person |
|
||||
| procedural + MakeHuman head | ~0.20 | oversized head, soft nose, beard slab reads as a bar | bobblehead proportions |
|
||||
| procedural crowd variant | ~0.20 | oversized head, flat cheeks | bobblehead proportions |
|
||||
|
||||
Measured (`python3 -m pytest tools/tests/test_npc_proportions.py -q`,
|
||||
`python3 tools/npc_proportions.py`):
|
||||
|
||||
- `npc_casual` (Quaternius): head 13.4 % of height, arm-down rest, 6,206 tris, 12,988 glTF
|
||||
vertices, texel density 73–74 px/m on the 256² maps.
|
||||
- The procedural library: head 16–22 % of height (the stylized band in
|
||||
`tools/npc_proportions.GUIDE`).
|
||||
|
||||
The Quaternius head is *smaller and more realistic* than the stylized library head, which is
|
||||
exactly the trade the owner has to accept or reject. The vision judge that reviewed the earlier
|
||||
head rework reads the Quaternius face as the cleaner silhouette of the three at both distances.
|
||||
|
||||
## The three options
|
||||
|
||||
1. **Full swap.** Every character rebaked onto the Quaternius base in `0.9.10`; the procedural
|
||||
library and the MakeHuman head retire to the tested fallback. Consistent look, real anatomy,
|
||||
no further head work.
|
||||
2. **Crowd only.** Quaternius for the anonymous crowd roles, procedural + MakeHuman for the named
|
||||
roles the player talks to. Keeps two art styles in one frame.
|
||||
3. **Add head detail.** Keep the Quaternius body and author a higher-detail head (subdivision and
|
||||
hair cards) inside the 24,000-triangle / 24,000-vertex budget. Most work; the current head
|
||||
already reads well at gameplay distance.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Option 1. The Quaternius head beats the MakeHuman head at portrait distance in this sheet, and it
|
||||
costs no further authoring; the budget headroom (6.2k of 24k triangles) stays available for the
|
||||
hands and gear that the review still flags as weak.
|
||||
|
||||
## Recorded decision
|
||||
|
||||
Status: **option 1 (full swap) accepted by the owner, 2026-11-05.** Every roster character moves
|
||||
onto the Quaternius base; the procedural library and the MakeHuman head become the tested
|
||||
fallback. See the follow-up decision section in
|
||||
[ADR-0018](../../adr/0018-quaternius-stage2-runtime-rig.md#follow-up-decision-full-roster-swap).
|
||||
|
||||
Roster silhouettes are less varied than the procedural library until the modular part library
|
||||
(hair, outfits) is added in the `0.9.10` roster workstream — recorded rather than hidden.
|
||||
|
After Width: | Height: | Size: 130 KiB |
@@ -8,7 +8,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
exclude_filter="tmp/*, tmp/**, build/*, build/**, docs/*, docs/**, tools/*, tools/**, src/test/*, src/test/**, .agents/*, .agents/**, packaging/*, packaging/**"
|
||||
export_path="build/gig-line-linux.x86_64"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
@@ -39,7 +39,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
exclude_filter="tmp/*, tmp/**, build/*, build/**, docs/*, docs/**, tools/*, tools/**, src/test/*, src/test/**, .agents/*, .agents/**, packaging/*, packaging/**"
|
||||
export_path="build/gig-line-windows.exe"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
@@ -75,7 +75,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
exclude_filter="tmp/*, tmp/**, build/*, build/**, docs/*, docs/**, tools/*, tools/**, src/test/*, src/test/**, .agents/*, .agents/**, packaging/*, packaging/**"
|
||||
export_path="build/gig-line-macos.zip"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
@@ -114,7 +114,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
exclude_filter="tmp/*, tmp/**, build/*, build/**, docs/*, docs/**, tools/*, tools/**, src/test/*, src/test/**, .agents/*, .agents/**, packaging/*, packaging/**"
|
||||
export_path="build/web/index.html"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
|
||||
@@ -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.8"
|
||||
config/version="0.9.9"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
|
||||
|
||||
@@ -413,38 +413,45 @@ func _build_skinned_body(npc_type: int) -> bool:
|
||||
|
||||
# Animation proxies: the existing _animate()/idle code drives these Node3D
|
||||
# pivots; _sync_skeleton() copies their rotations onto the armature bones.
|
||||
_bind_bones()
|
||||
_head = Node3D.new()
|
||||
_head.name = "Head"
|
||||
_head.position = Vector3(0, 1.62, 0)
|
||||
_head.position = _rest_origin(["Head"])
|
||||
_rig.add_child(_head)
|
||||
for side in [-1.0, 1.0]:
|
||||
var suffix := "L" if side < 0.0 else "R"
|
||||
for i in 2:
|
||||
var suffix := "L" if i == 0 else "R"
|
||||
# The proxy pivots sit on the resolved rig bones (see RIG_BONES), so the
|
||||
# proxy chirality and the exported joint naming never have to agree.
|
||||
var hip_bone := bone_name("hip", i)
|
||||
var knee_bone := bone_name("knee", i)
|
||||
var shoulder_bone := bone_name("shoulder", i)
|
||||
var elbow_bone := bone_name("elbow", i)
|
||||
var hand_bone := bone_name("hand", i)
|
||||
var hip := Node3D.new()
|
||||
hip.name = "Hip" + suffix
|
||||
hip.position = Vector3(0.13 * side, 0.64, 0)
|
||||
hip.position = _rest_origin([hip_bone])
|
||||
_rig.add_child(hip)
|
||||
var knee := Node3D.new()
|
||||
knee.name = "Knee"
|
||||
knee.position = Vector3(0, -0.34, 0)
|
||||
knee.position = _rest_origin([knee_bone]) - hip.position
|
||||
hip.add_child(knee)
|
||||
_legs.append(hip)
|
||||
_knees.append(knee)
|
||||
var shoulder := Node3D.new()
|
||||
shoulder.name = "Shoulder" + suffix
|
||||
shoulder.position = Vector3(0.27 * side, 1.38, 0)
|
||||
shoulder.position = _rest_origin([shoulder_bone])
|
||||
_rig.add_child(shoulder)
|
||||
var elbow := Node3D.new()
|
||||
elbow.name = "Elbow"
|
||||
elbow.position = Vector3(0, -0.30, 0)
|
||||
elbow.position = _rest_origin([elbow_bone]) - shoulder.position
|
||||
shoulder.add_child(elbow)
|
||||
var hand := Node3D.new()
|
||||
hand.name = "Hand"
|
||||
hand.position = Vector3(0, -0.30, 0.02)
|
||||
hand.position = _rest_origin([hand_bone]) - _rest_origin([elbow_bone])
|
||||
elbow.add_child(hand)
|
||||
_arms.append(shoulder)
|
||||
_elbows.append(elbow)
|
||||
_hands.append(hand)
|
||||
_bind_bones()
|
||||
|
||||
_add_accessory(str(outfit["accessory"]), cloth)
|
||||
_add_character_gear()
|
||||
@@ -495,35 +502,84 @@ func _bind_bones() -> void:
|
||||
for index in _skeleton.get_bone_count():
|
||||
_bone_indices[_skeleton.get_bone_name(index)] = index
|
||||
|
||||
## First-found rest origin for a list of candidate bone names, in the skeleton's
|
||||
## space (global rest), so proxy pivots sit on the actual joints.
|
||||
func _rest_origin(names: Array) -> Vector3:
|
||||
if _skeleton == null:
|
||||
return Vector3.ZERO
|
||||
for bone_name in names:
|
||||
if _bone_indices.has(bone_name):
|
||||
return _skeleton.get_bone_global_rest(_bone_indices[bone_name]).origin
|
||||
return Vector3.ZERO
|
||||
|
||||
## Bone-name candidates per animation joint, in proxy order: index 0 is the
|
||||
## avatar's left (-x) and index 1 its right (+x). The Quaternius rig labels its
|
||||
## dot-suffixed joints with the opposite side (its `L` sits at +x), so the pairs
|
||||
## carry the rig-specific mapping and nothing else has to know about it.
|
||||
const RIG_BONES := {
|
||||
"head": [["Head"], ["Head"]],
|
||||
"hip": [["UpperLeg.R", "HipL"], ["UpperLeg.L", "HipR"]],
|
||||
"knee": [["LowerLeg.R", "KneeL"], ["LowerLeg.L", "KneeR"]],
|
||||
"shoulder": [["UpperArm.R", "ShoulderL"], ["UpperArm.L", "ShoulderR"]],
|
||||
"elbow": [["LowerArm.R", "ElbowL"], ["LowerArm.L", "ElbowR"]],
|
||||
"hand": [["Wrist.R", "HandL"], ["Wrist.L", "HandR"]],
|
||||
}
|
||||
|
||||
## Resolved skeleton bone name for an animation joint on one side. Returns an
|
||||
## empty string when the rig has neither candidate, so both the Quaternius
|
||||
## 62-joint rig and the 13-core fallback resolve through the same call.
|
||||
func bone_name(joint: String, side: int = 0) -> String:
|
||||
if not RIG_BONES.has(joint):
|
||||
return ""
|
||||
var pair: Array = RIG_BONES[joint]
|
||||
return _bone_of(pair[0], pair[1], side)
|
||||
|
||||
## Copy the proxy pivot rotations onto armature bones. Limb bones point down, so
|
||||
## an X rotation swings them forward/back; the Head bone points up, so a Y
|
||||
## rotation is the look yaw.
|
||||
func _sync_skeleton() -> void:
|
||||
if _skeleton == null or _bone_indices.is_empty():
|
||||
return
|
||||
var names := ["HipL", "HipR"]
|
||||
for i in 2:
|
||||
_pose_bone(names[i], _legs[i].rotation.x, Vector3.RIGHT)
|
||||
names = ["KneeL", "KneeR"]
|
||||
for i in 2:
|
||||
_pose_bone(names[i], _knees[i].rotation.x, Vector3.RIGHT)
|
||||
names = ["ShoulderL", "ShoulderR"]
|
||||
for i in 2:
|
||||
_pose_bone(names[i], _arms[i].rotation.x, Vector3.RIGHT)
|
||||
names = ["ElbowL", "ElbowR"]
|
||||
for i in 2:
|
||||
_pose_bone(names[i], _elbows[i].rotation.x, Vector3.RIGHT)
|
||||
var hip_swing := Quaternion(Vector3.RIGHT, _legs[i].rotation.x)
|
||||
_swing_bone(bone_name("hip", i), hip_swing)
|
||||
# `_swing_bone` writes the absolute global rotation, so a child bone has to
|
||||
# receive the swing it inherits from its parent as well as its own.
|
||||
_swing_bone(bone_name("knee", i), hip_swing * Quaternion(Vector3.RIGHT, _knees[i].rotation.x))
|
||||
var shoulder_swing := Quaternion(Vector3.LEFT, _arms[i].rotation.x)
|
||||
_swing_bone(bone_name("shoulder", i), shoulder_swing)
|
||||
_swing_bone(bone_name("elbow", i), shoulder_swing * Quaternion(Vector3.LEFT, _elbows[i].rotation.x))
|
||||
if _head != null:
|
||||
_pose_bone("Head", _head.rotation.y, Vector3.UP)
|
||||
var head := bone_name("head")
|
||||
if head != "":
|
||||
_swing_bone(head, Quaternion(Vector3.UP, _head.rotation.y))
|
||||
|
||||
func _pose_bone(bone_name: String, angle: float, axis: Vector3) -> void:
|
||||
if not _bone_indices.has(bone_name):
|
||||
## First bone present from the left list (index 0) or the right list (index 1).
|
||||
func _bone_of(left: Array, right: Array, index: int) -> String:
|
||||
for bone_name in (left if index == 0 else right):
|
||||
if _bone_indices.has(bone_name):
|
||||
return str(bone_name)
|
||||
return ""
|
||||
|
||||
## Swing a bone about a world axis through its head, keeping its rest orientation
|
||||
## as the neutral. Writing the *global* pose is rig-agnostic: it does not depend
|
||||
## on how the armature pack exported its rest rolls or on the pose/rest split the
|
||||
## glTF importer chose, both of which differ between the 62-joint Quaternius rig
|
||||
## and the 13-core fallback. Measuring `get_bone_global_pose()` after the call is
|
||||
## the only reliable check (see the pose-capture gate).
|
||||
##
|
||||
## `rest` must be the **global** rest. `get_bone_rest()` is parent-relative and
|
||||
## already carries the importer's axis conversion, so conjugating with it swung
|
||||
## the limbs about the wrong axis and splayed the arms.
|
||||
func _swing_bone(bone_name: String, swing: Quaternion) -> void:
|
||||
if bone_name == "":
|
||||
return
|
||||
var index: int = _bone_indices[bone_name]
|
||||
# Bone pose rotation is the absolute local orientation, not an offset from
|
||||
# rest; compose with the rest rotation so identity preserves the bind pose.
|
||||
var rest := _skeleton.get_bone_rest(index).basis.get_rotation_quaternion()
|
||||
_skeleton.set_bone_pose_rotation(index, rest * Quaternion(axis, angle))
|
||||
_skeleton.reset_bone_pose(index)
|
||||
var rest := _skeleton.get_bone_global_rest(index).basis.get_rotation_quaternion()
|
||||
var current := _skeleton.get_bone_global_pose(index)
|
||||
var target := Basis((swing * rest).normalized()).scaled(current.basis.get_scale())
|
||||
_skeleton.set_bone_global_pose(index, Transform3D(target, current.origin))
|
||||
|
||||
func _build_body(npc_type: int) -> void:
|
||||
_rig = Node3D.new()
|
||||
|
||||
@@ -9,6 +9,17 @@ const NPC_BASE := "res://assets/models/generated/npc_base.glb"
|
||||
const SLOTS := ["skin", "cloth", "pants", "accessory", "hair"]
|
||||
const BONES := ["Head", "HipL", "HipR", "KneeL", "KneeR", "ShoulderL", "ShoulderR",
|
||||
"ElbowL", "ElbowR", "HandL", "HandR"]
|
||||
## The Quaternius rig (ADR-0018) is a 62-joint armature. `NpcAvatar` maps these
|
||||
## joints onto the animation proxies, so they are the drop-in requirement for a
|
||||
## Quaternius-derived character instead of the 13-core names.
|
||||
const QUATERNIUS_BONES := ["Head", "UpperLeg.L", "UpperLeg.R", "LowerLeg.L",
|
||||
"LowerLeg.R", "UpperArm.L", "UpperArm.R", "LowerArm.L", "LowerArm.R",
|
||||
"Wrist.L", "Wrist.R"]
|
||||
|
||||
## Quaternius-derived characters are detected by a dot-suffixed UpperArm joint;
|
||||
## the procedural library uses ShoulderL/ShoulderR instead.
|
||||
func _is_quaternius(skeleton: Skeleton3D) -> bool:
|
||||
return skeleton != null and skeleton.find_bone("UpperArm.L") >= 0
|
||||
|
||||
func _manifest() -> Dictionary:
|
||||
if not FileAccess.file_exists(MANIFEST):
|
||||
@@ -76,7 +87,8 @@ func test_characters_match_the_npc_base_contract() -> bool:
|
||||
problems.append("%s lacks Skeleton3D/npc_base" % name)
|
||||
node.free()
|
||||
continue
|
||||
for bone in BONES:
|
||||
var required: Array = QUATERNIUS_BONES if _is_quaternius(skeleton) else BONES
|
||||
for bone in required:
|
||||
if skeleton.find_bone(bone) < 0:
|
||||
problems.append("%s missing bone %s" % [name, bone])
|
||||
var slots := {}
|
||||
@@ -105,6 +117,11 @@ func test_character_joints_line_up_with_npc_base() -> bool:
|
||||
var skeleton := node.find_child("Skeleton3D", true, false) as Skeleton3D if node else null
|
||||
if skeleton == null:
|
||||
problems.append("%s has no skeleton" % name)
|
||||
elif _is_quaternius(skeleton):
|
||||
# ADR-0018: a Quaternius character keeps its own 62-joint skeleton and
|
||||
# NpcAvatar re-derives the proxy pivots from it, so the joints are not
|
||||
# expected to sit on npc_base's 13-core positions.
|
||||
pass
|
||||
else:
|
||||
for bone in BONES:
|
||||
var a := _global_rest(base_skel, base_skel.find_bone(bone)).origin
|
||||
@@ -127,10 +144,18 @@ func test_character_bones_accept_the_avatar_pose_math() -> bool:
|
||||
if skeleton == null:
|
||||
problems.append(name)
|
||||
else:
|
||||
for bone in BONES:
|
||||
var bones: Array = QUATERNIUS_BONES if _is_quaternius(skeleton) else BONES
|
||||
for bone in bones:
|
||||
var index := skeleton.find_bone(bone)
|
||||
var rest := skeleton.get_bone_rest(index).basis.get_rotation_quaternion()
|
||||
skeleton.set_bone_pose_rotation(index, rest * Quaternion(Vector3.RIGHT, 0.4))
|
||||
if index < 0:
|
||||
problems.append("%s missing %s" % [name, bone])
|
||||
continue
|
||||
# NpcAvatar._swing_bone(): pose = rest^-1 * swing * rest, applied
|
||||
# through the global pose. The composition must stay a valid
|
||||
# rotation on every limb bone of either rig.
|
||||
var rest := skeleton.get_bone_global_rest(index).basis.get_rotation_quaternion()
|
||||
var swing := Quaternion(Vector3.RIGHT, 0.4)
|
||||
skeleton.set_bone_pose_rotation(index, rest.inverse() * swing * rest)
|
||||
if not skeleton.get_bone_pose_rotation(index).is_normalized():
|
||||
problems.append("%s.%s" % [name, bone])
|
||||
if node:
|
||||
|
||||
@@ -141,8 +141,8 @@ func test_skinned_model_loads_and_keeps_the_head_contract() -> bool:
|
||||
"a skinned library character is used")
|
||||
var b := TestUtils.check(avatar.get_node_or_null("Rig/Head") != null,
|
||||
"Rig/Head survives the skinned path")
|
||||
var c := TestUtils.check(avatar._skeleton != null and avatar._bone_indices.has("Head")
|
||||
and avatar._bone_indices.has("HipL") and avatar._bone_indices.has("HandR"),
|
||||
var c := TestUtils.check(avatar._skeleton != null and avatar.bone_name("head") != ""
|
||||
and avatar.bone_name("hip", 0) != "" and avatar.bone_name("hand", 1) != "",
|
||||
"armature bones are bound")
|
||||
avatar.free()
|
||||
return a and b and c
|
||||
@@ -152,10 +152,10 @@ func test_skinned_model_loads_and_keeps_the_head_contract() -> bool:
|
||||
func test_pose_reaches_the_skeleton_bones() -> bool:
|
||||
var avatar := _avatar(Rules.NpcType.TOURIST, 55)
|
||||
var sk := avatar._skeleton
|
||||
var hip: int = int(avatar._bone_indices["HipL"])
|
||||
var knee: int = int(avatar._bone_indices["KneeL"])
|
||||
var rest_hip := sk.get_bone_rest(hip).basis.get_rotation_quaternion()
|
||||
var rest_knee := sk.get_bone_rest(knee).basis.get_rotation_quaternion()
|
||||
var hip: int = int(avatar._bone_indices[avatar.bone_name("hip", 0)])
|
||||
var knee: int = int(avatar._bone_indices[avatar.bone_name("knee", 0)])
|
||||
var rest_hip := sk.get_bone_pose_rotation(hip)
|
||||
var rest_knee := sk.get_bone_pose_rotation(knee)
|
||||
avatar.arrived = true
|
||||
avatar.npc.intent = "sit"
|
||||
for _i in 45:
|
||||
@@ -179,6 +179,49 @@ func test_pose_reaches_the_skeleton_bones() -> bool:
|
||||
avatar.free()
|
||||
return a and b and c and d
|
||||
|
||||
## Regression (ADR-0018): the shipped base must resolve every animation joint on
|
||||
## the rig it actually uses, keep the baked arm-down rest, and put each proxy on
|
||||
## the matching body side. The Quaternius rig labels its dot-suffixed joints with
|
||||
## the opposite side (its `L` sits at +x), so `RIG_BONES` is the single mapping and
|
||||
## a silent side swap would splay the arms across the torso.
|
||||
##
|
||||
## The swing itself is proven by the pose-capture gate; `_swing_bone()` writes the
|
||||
## global pose, which Godot only computes for a skeleton inside the scene tree and
|
||||
## the headless runner executes tests before the tree is ready.
|
||||
func test_shipped_rig_resolves_the_animation_joints() -> bool:
|
||||
var avatar := _avatar(Rules.NpcType.TOURIST, 91)
|
||||
var sk := avatar._skeleton
|
||||
var problems: Array[String] = []
|
||||
for joint in NpcAvatar.RIG_BONES:
|
||||
for side in 2:
|
||||
var bone := avatar.bone_name(joint, side)
|
||||
if bone == "" or not avatar._bone_indices.has(bone):
|
||||
problems.append("%s/%d unresolved" % [joint, side])
|
||||
var a := TestUtils.check(problems.is_empty(),
|
||||
"the shipped rig resolves every animation joint (%s)" % str(problems))
|
||||
var wrist := avatar.bone_name("hand", 0)
|
||||
var shoulder := avatar.bone_name("shoulder", 0)
|
||||
var b := TestUtils.check(wrist != "" and shoulder != "", "shoulder/hand joints resolve")
|
||||
if not b:
|
||||
avatar.free()
|
||||
return false
|
||||
var shoulder_rest := sk.get_bone_global_rest(int(avatar._bone_indices[shoulder]))
|
||||
var wrist_rest := sk.get_bone_global_rest(int(avatar._bone_indices[wrist]))
|
||||
var c := TestUtils.check(shoulder_rest.origin.y - wrist_rest.origin.y > 0.3,
|
||||
"the shipped base is baked arm-down (%.3f below the shoulder)"
|
||||
% (shoulder_rest.origin.y - wrist_rest.origin.y))
|
||||
# A straight-down arm axis is what makes an X swing read as forward/back.
|
||||
var arm_down := shoulder_rest.basis.y.normalized().dot(Vector3.DOWN)
|
||||
var d := TestUtils.check(arm_down > 0.95,
|
||||
"the arm rest axis points down (dot %.3f)" % arm_down)
|
||||
# Proxy side 0 must drive the -x arm and side 1 the +x arm on either rig.
|
||||
var left := sk.get_bone_global_rest(int(avatar._bone_indices[avatar.bone_name("shoulder", 0)]))
|
||||
var right := sk.get_bone_global_rest(int(avatar._bone_indices[avatar.bone_name("shoulder", 1)]))
|
||||
var e := TestUtils.check(left.origin.x < right.origin.x,
|
||||
"proxy side 0 drives the -x arm (%.3f vs %.3f)" % [left.origin.x, right.origin.x])
|
||||
avatar.free()
|
||||
return a and b and c and d and e
|
||||
|
||||
func test_character_selection_is_deterministic_and_complete() -> bool:
|
||||
# Seed 5 resolves each role to a fixed pool member; the routine crowd roles
|
||||
# now pick from the crowd-variety pools instead of a single npc_casual.
|
||||
|
||||
@@ -53,6 +53,12 @@ MANIFEST = OUT_DIR / "manifest.json"
|
||||
SLOTS = ["skin", "cloth", "pants", "accessory", "hair"]
|
||||
BONES = ["Head", "HipL", "HipR", "KneeL", "KneeR", "ShoulderL", "ShoulderR",
|
||||
"ElbowL", "ElbowR", "HandL", "HandR"]
|
||||
# ADR-0018: a Quaternius-derived character keeps the upstream 62-joint armature
|
||||
# instead of the 13-core skeleton, so these are the joints `NpcAvatar` maps onto
|
||||
# the animation proxies and the ones the audit requires.
|
||||
QUATERNIUS_BONES = ["Head", "UpperLeg.L", "UpperLeg.R", "LowerLeg.L", "LowerLeg.R",
|
||||
"UpperArm.L", "UpperArm.R", "LowerArm.L", "LowerArm.R",
|
||||
"Wrist.L", "Wrist.R"]
|
||||
# Character cap raised to 24000/24000 on 2026-11-05 for the MPFB body, the
|
||||
# accepted MakeHuman head and a seamless neck/shoulder junction. The required
|
||||
# 13 core bones stay, but optional extra bones (fingers, face, toes) are
|
||||
@@ -79,37 +85,47 @@ except ImportError: # pragma: no cover - audit/sheet/list run outside Blender
|
||||
# Characters map onto NpcAvatar.layer_for() / ACCESSORIES so the runtime can
|
||||
# pick one per avatar later. `tint` lists the slots the runtime should tint;
|
||||
# every other material keeps its authored colour.
|
||||
# `base` selects the CC0 Quaternius source the character is baked from
|
||||
# (ADR-0017/ADR-0018, owner chose the full roster swap); `drop` removes upstream
|
||||
# materials that do not belong to the role (the Worker's hivis vest and hard hat).
|
||||
# Rebuild one procedurally with `build --source procedural --head mh`.
|
||||
CHARACTERS = {
|
||||
"npc_casual": {"layer": "none", "hair": "short", "gear": ["backpack", "camera"],
|
||||
"base": "hoodie",
|
||||
"roles": ["tourist", "confused_visitor", "intoxicated", "guest"]},
|
||||
"npc_jacket": {"layer": "jacket", "hair": "short", "gear": ["lanyard", "clipboard"],
|
||||
"base": "casual",
|
||||
"roles": ["authorized_staff", "protester", "guest"]},
|
||||
"npc_coat": {"layer": "coat", "hair": "long",
|
||||
"npc_coat": {"layer": "coat", "hair": "long", "base": "hoodie",
|
||||
"gear": ["sunglasses", "shoulder_bag", "phone"],
|
||||
"roles": ["influencer", "trespasser", "violent_threat"]},
|
||||
"npc_vest": {"layer": "vest", "hair": "buzz", "gear": ["cap", "clipboard"],
|
||||
"base": "worker",
|
||||
"roles": ["delivery"]},
|
||||
"npc_dress": {"layer": "dress", "hair": "bun", "build": "slim",
|
||||
"npc_dress": {"layer": "dress", "hair": "bun", "build": "slim", "base": "casual",
|
||||
"gear": ["shoulder_bag"],
|
||||
"roles": ["performer"]},
|
||||
"npc_hoodie": {"layer": "hood", "hair": "short", "gear": ["cup"],
|
||||
"npc_hoodie": {"layer": "hood", "hair": "short", "gear": ["cup"], "base": "hoodie",
|
||||
"roles": ["trespasser", "mental_crisis", "violent_threat"]},
|
||||
"npc_firefighter": {"layer": "responder", "hair": "buzz", "build": "broad",
|
||||
"base": "worker", "drop": ["Worker_Vest", "Worker_Yellow"],
|
||||
"gear": ["firefighter_helmet", "scba_tank", "flashlight"],
|
||||
"roles": ["responder"]},
|
||||
"npc_guard": {"layer": "guard", "hair": "buzz", "build": "broad",
|
||||
"base": "worker", "drop": ["Worker_Vest", "Worker_Yellow"],
|
||||
"gear": ["cap", "wand"],
|
||||
"roles": ["player_guard", "venue_security"]},
|
||||
# Crowd-variety prototypes (character development): vary build, head
|
||||
# silhouette, hair/beard and outer layer without labelling behaviour.
|
||||
"npc_crowd_a": {"layer": "none", "hair": "bun", "build": "slim",
|
||||
# Crowd-variety prototypes (character development): the three Quaternius
|
||||
# sources are spread across the crowd pools so a crowd never shows one mesh.
|
||||
"npc_crowd_a": {"layer": "none", "hair": "bun", "build": "slim", "base": "casual",
|
||||
"gear": ["sunglasses"], "roles": ["tourist", "guest"]},
|
||||
"npc_crowd_b": {"layer": "none", "hair": "bald", "build": "broad",
|
||||
"npc_crowd_b": {"layer": "none", "hair": "bald", "build": "broad", "base": "casual",
|
||||
"beard": True, "gear": ["shoulder_bag"],
|
||||
"roles": ["confused_visitor", "guest"]},
|
||||
"npc_crowd_c": {"layer": "coat", "hair": "short",
|
||||
"npc_crowd_c": {"layer": "coat", "hair": "short", "base": "hoodie",
|
||||
"gear": ["umbrella"], "roles": ["intoxicated", "guest"]},
|
||||
"npc_crowd_d": {"layer": "hood", "hair": "short", "build": "slim",
|
||||
"base": "worker", "drop": ["Worker_Vest", "Worker_Yellow"],
|
||||
"gear": ["cup"], "roles": ["tourist", "guest"]},
|
||||
}
|
||||
|
||||
@@ -127,6 +143,11 @@ ATTACH_BLENDER = {"head": (0.0, 0.0, 1.62), "hand": (0.27, -0.02, 0.78),
|
||||
ATTACH_GODOT = {"head": (0.0, 1.62, 0.0), "hand": (0.27, 0.78, 0.02),
|
||||
"back": (0.0, 1.10, 0.0), "chest": (0.0, 1.30, 0.0)}
|
||||
HEAD_BAND = (1.55, 1.81) # face/headgear height band in Godot metres
|
||||
# Head gear is authored at the procedural library's head size; the shipped
|
||||
# Quaternius roster has a 0.20 m head whose centre sits higher (see
|
||||
# `build_gear`). 0.64 = 0.201 / 0.315.
|
||||
HEAD_GEAR_SCALE = 0.64
|
||||
HEAD_GEAR_LIFT = 0.07
|
||||
FIT_HEAD_MATERIALS = ("skin",)
|
||||
FIT_CLOTHING_MATERIAL = "cloth"
|
||||
FIT_CLOTHING_BAND = (0.30, 0.52) # hip/skirt band; captures coat/dress flare
|
||||
@@ -1433,6 +1454,312 @@ def build_character(name, head="face"):
|
||||
return [arm, mesh]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Quaternius roster (ADR-0017 / ADR-0018)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# The CC0 sources live under `blender_src/quaternius/` (gitignored for Godot via
|
||||
# the parent `.gdignore`) and are build inputs, not runtime assets. Every source
|
||||
# is 4 skinned meshes on the same 62-joint armature; the build joins them
|
||||
# **without welding** (a weld corrupts the skin weights), renames the upstream
|
||||
# materials onto the five tint slots, unwraps the degenerate source UVs and bakes
|
||||
# the arm-down rest. See docs/npc-model-sourcing.md for the measurements.
|
||||
|
||||
QUATERNIUS_SRC = SRC_DIR / "quaternius"
|
||||
|
||||
# Material -> contract slot per source. `"keep"` leaves the authored colour
|
||||
# (face, hair detail and hivis fabrics); anything not listed is kept too, so a
|
||||
# new upstream material shows up as an extra fixed-colour slot instead of
|
||||
# silently disappearing. The maps were derived from the per-material height bands
|
||||
# measured in docs/npc-model-sourcing.md.
|
||||
QUATERNIUS_SOURCES = {
|
||||
"casual": {
|
||||
"file": "Casual.glb",
|
||||
"slots": {"Skin": "skin", "White": "cloth", "Orange": "pants",
|
||||
"Grey": "accessory", "Hair_Blond": "hair",
|
||||
"Brown": "keep", "Hair_Brown": "keep"},
|
||||
"notes": "white tee + orange trousers + grey shoes; blond hair",
|
||||
},
|
||||
"hoodie": {
|
||||
"file": "Casual_Hoodie.glb",
|
||||
"slots": {"Skin": "skin", "Purple": "cloth", "LightBlue": "pants",
|
||||
"Hair": "hair", "White": "accessory",
|
||||
"Eye": "keep", "Eyebrows": "keep"},
|
||||
"notes": "hoodie + shorts + white shoes (the hoodie covers the shoe uppers)",
|
||||
},
|
||||
"worker": {
|
||||
"file": "Worker.glb",
|
||||
"slots": {"Skin": "skin", "LightBrown": "cloth", "Brown": "pants",
|
||||
"Brown2": "pants", "Grey": "accessory", "Black": "accessory",
|
||||
# The Worker's head is bare apart from a hard hat, so the
|
||||
# brows carry the hair tint and the slot keeps a surface.
|
||||
"Eyebrows": "hair",
|
||||
"Eye": "keep", "Moustache": "keep",
|
||||
"Worker_Vest": "keep", "Worker_Yellow": "keep"},
|
||||
"notes": "work shirt + trousers + hivis vest + hard hat",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _quaternius_slot_map(obj, slot_map, drop):
|
||||
"""Rename the upstream materials onto the contract slots.
|
||||
|
||||
Several upstream materials can share a slot (trousers + patches), so the
|
||||
polygons are repointed at one Blender material instead of relying on a
|
||||
rename that Blender would de-duplicate to `pants.001`.
|
||||
"""
|
||||
materials = [m for m in obj.data.materials if m is not None]
|
||||
dropped = {i for i, m in enumerate(materials) if m.name in drop}
|
||||
kept = []
|
||||
target = {}
|
||||
remap = {}
|
||||
for index, m in enumerate(materials):
|
||||
slot = slot_map.get(m.name, "keep")
|
||||
if slot == "keep":
|
||||
if m.name not in target:
|
||||
target[m.name] = len(kept)
|
||||
kept.append(m)
|
||||
remap[index] = target[m.name]
|
||||
continue
|
||||
if slot in target:
|
||||
remap[index] = target[slot]
|
||||
continue
|
||||
m.name = slot
|
||||
target[slot] = len(kept)
|
||||
kept.append(m)
|
||||
remap[index] = target[slot]
|
||||
# Faces that only exist for a dropped material go away with it.
|
||||
if dropped:
|
||||
doomed = [p.index for p in obj.data.polygons
|
||||
if p.material_index in dropped]
|
||||
if doomed:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bm.faces.ensure_lookup_table()
|
||||
bmesh.ops.delete(bm, geom=[bm.faces[i] for i in doomed],
|
||||
context="FACES")
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
# Loose vertices left behind by the helmet/vest would export as an
|
||||
# unreferenced blob, so drop them before the UV pass.
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
loose = [v for v in bm.verts if not v.link_faces]
|
||||
if loose:
|
||||
bmesh.ops.delete(bm, geom=loose, context="VERTS")
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
# `materials.clear()` resets every polygon to slot 0, so record the source
|
||||
# indices first or the whole mesh collapses onto the first slot.
|
||||
source_index = [poly.material_index for poly in obj.data.polygons]
|
||||
obj.data.materials.clear()
|
||||
for m in kept:
|
||||
obj.data.materials.append(m)
|
||||
for poly, index in zip(obj.data.polygons, source_index):
|
||||
poly.material_index = remap.get(index, 0)
|
||||
|
||||
|
||||
def _bake_quaternius_rest(arm, mesh):
|
||||
"""Bake the T-pose into an arm-down rest pose.
|
||||
|
||||
This runs on the re-imported T-pose export, not on the raw upstream file.
|
||||
The order matters: pose, read the posed matrices, apply the Armature
|
||||
modifier, **reset the pose to identity**, write the rest matrices, re-add the
|
||||
modifier. Skipping the reset double-applies the arm-down (mesh width ±0.53 m
|
||||
instead of ±0.23 m); `bpy.ops.object.armature_apply()` in Blender 5.2
|
||||
deforms the mesh without baking the rest and is not usable here.
|
||||
"""
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
for side in ("L", "R"):
|
||||
pose_bone = arm.pose.bones.get("UpperArm." + side)
|
||||
if pose_bone is None:
|
||||
continue
|
||||
pose_bone.rotation_mode = "XYZ"
|
||||
# Local X swings the arm in the frontal plane. This is only symmetric on
|
||||
# *our* exported rest: the raw upstream file mirrors the bone rolls, so
|
||||
# the build always round-trips through `_quaternius_tpose_glb()` first.
|
||||
pose_bone.rotation_euler = (-math.pi / 2, 0, 0)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.context.view_layer.update()
|
||||
for side in ("L", "R"):
|
||||
wrist = arm.pose.bones.get("Wrist." + side)
|
||||
if wrist is not None:
|
||||
head = arm.matrix_world @ wrist.head
|
||||
print(f" wrist.{side} at x={head.x:+.3f} y={head.y:+.3f} z={head.z:+.3f}")
|
||||
|
||||
fingers = ("Thumb", "Index", "Middle", "Ring", "Pinky")
|
||||
posed = {}
|
||||
for side in ("L", "R"):
|
||||
for bone in arm.data.bones:
|
||||
name = bone.name
|
||||
if not name.endswith(side):
|
||||
continue
|
||||
if name.startswith(("UpperArm", "LowerArm", "Wrist")) or name.startswith(fingers):
|
||||
posed[name] = arm.pose.bones[name].matrix.copy()
|
||||
|
||||
arm_modifier = next(m for m in mesh.modifiers if m.type == "ARMATURE")
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
bpy.ops.object.modifier_apply(modifier=arm_modifier.name)
|
||||
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
for pose_bone in arm.pose.bones:
|
||||
pose_bone.matrix_basis.identity()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
for name, matrix in posed.items():
|
||||
arm.data.edit_bones[name].matrix = matrix
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
modifier = mesh.modifiers.new("Armature", "ARMATURE")
|
||||
modifier.object = arm
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = mesh.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
xs = [v.co.x for v in evaluated.data.vertices]
|
||||
print(f" arm-down rest baked, deformed x {min(xs):.3f}..{max(xs):.3f}")
|
||||
|
||||
|
||||
def _export_preserving_skin(objs, category, name):
|
||||
"""Export a skinned GLB without touching the rest pose.
|
||||
|
||||
`export_apply=True` (what `export()` uses for the procedural library) bakes
|
||||
the evaluated deformation a second time and writes wrong inverse bind
|
||||
matrices, so Godot then renders the A-pose.
|
||||
"""
|
||||
path = asset_path(category, name)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
for o in bpy.context.selected_objects:
|
||||
o.select_set(False)
|
||||
for o in objs:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = objs[0]
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=str(path), export_format="GLB", use_selection=True,
|
||||
export_apply=False, export_animations=False, export_skins=True,
|
||||
export_yup=True, export_materials="EXPORT", export_image_format="AUTO")
|
||||
SRC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(SRC_DIR / f"{name}.blend"),
|
||||
compress=True)
|
||||
return path
|
||||
|
||||
|
||||
def _quaternius_work_dir():
|
||||
path = ROOT / "tmp" / "npc-build"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def _quaternius_tpose_glb(base, drop):
|
||||
"""Export (and cache) the joined, slot-mapped, unwrapped T-pose base.
|
||||
|
||||
The arm-down bake only behaves symmetrically on our own export, because the
|
||||
upstream file mirrors the bone rolls; this round trip normalises them. The
|
||||
result depends only on the source and the dropped materials, so it is cached
|
||||
for the run.
|
||||
"""
|
||||
key = base + ("-" + "-".join(sorted(drop)) if drop else "")
|
||||
path = _quaternius_work_dir() / f"tpose_{key}.glb"
|
||||
if path.is_file():
|
||||
return path
|
||||
source = QUATERNIUS_SOURCES[base]
|
||||
source_path = QUATERNIUS_SRC / source["file"]
|
||||
if not source_path.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"{source_path} is missing; see {QUATERNIUS_SRC / 'README.md'}")
|
||||
reset_scene()
|
||||
bpy.ops.import_scene.gltf(filepath=str(source_path))
|
||||
# The importer brings an Icosphere helper along; it is not part of the body
|
||||
# and has no UVs or weights.
|
||||
for o in list(bpy.context.scene.objects):
|
||||
if o.name.startswith("Icosphere"):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
meshes = [o for o in bpy.context.scene.objects if o.type == "MESH"]
|
||||
if not meshes:
|
||||
raise RuntimeError(f"{source_path} imported no skinned meshes")
|
||||
arm = next(o for o in bpy.context.scene.objects if o.type == "ARMATURE")
|
||||
|
||||
# Join without welding: remove_doubles merges vertices across weight groups
|
||||
# and the resulting skinning is wrong (see MEMORY.md).
|
||||
for o in bpy.context.scene.objects:
|
||||
o.select_set(False)
|
||||
for o in meshes:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = meshes[0]
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.object
|
||||
obj.name = "npc_base"
|
||||
obj.data.name = "npc_base"
|
||||
|
||||
_quaternius_slot_map(obj, source["slots"], drop)
|
||||
|
||||
# Upstream UVs are degenerate (every loop at 0,0), so an AO bake would write
|
||||
# an all-black map. Repack a real layout at a tight island margin (the
|
||||
# default 0.01 wastes most of the square on a dense body), then scale it to
|
||||
# fill the square so the 256-square maps keep a usable texel density.
|
||||
for o in bpy.context.selected_objects:
|
||||
o.select_set(False)
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.mesh.select_all(action="SELECT")
|
||||
bpy.ops.uv.smart_project(angle_limit=math.radians(66.0), island_margin=0.0005)
|
||||
bpy.ops.uv.average_islands_scale()
|
||||
bpy.ops.uv.pack_islands(margin=0.0015, rotate=True)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
uv_layer = obj.data.uv_layers.active
|
||||
loops = uv_layer.data
|
||||
us = [d.uv[0] for d in loops]
|
||||
vs = [d.uv[1] for d in loops]
|
||||
span = max(max(us) - min(us), max(vs) - min(vs))
|
||||
if span > 1e-6:
|
||||
scale = 0.98 / span
|
||||
low_u, low_v = min(us), min(vs)
|
||||
for d in loops:
|
||||
d.uv = ((d.uv[0] - low_u) * scale + 0.01,
|
||||
(d.uv[1] - low_v) * scale + 0.01)
|
||||
|
||||
# Only the five contract slots are tinted at runtime; make sure a source that
|
||||
# does not paint with one of them still declares it.
|
||||
present = {m.name for m in obj.data.materials if m}
|
||||
for slot in SLOTS:
|
||||
if slot not in present:
|
||||
obj.data.materials.append(bpy.data.materials.get(slot)
|
||||
or material(slot))
|
||||
|
||||
for o in bpy.context.scene.objects:
|
||||
o.select_set(False)
|
||||
arm.select_set(True)
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=str(path), export_format="GLB", use_selection=True,
|
||||
export_apply=False, export_animations=False, export_skins=True,
|
||||
export_yup=True, export_materials="EXPORT", export_image_format="AUTO")
|
||||
print(f" T-pose base {path.relative_to(ROOT)}")
|
||||
return path
|
||||
|
||||
|
||||
def build_quaternius_character(name):
|
||||
"""Bake one roster character from its CC0 Quaternius source."""
|
||||
spec = CHARACTERS[name]
|
||||
tpose = _quaternius_tpose_glb(spec["base"], spec.get("drop", []))
|
||||
reset_scene()
|
||||
bpy.ops.import_scene.gltf(filepath=str(tpose))
|
||||
arm = next(o for o in bpy.context.scene.objects if o.type == "ARMATURE")
|
||||
mesh = next(o for o in bpy.context.scene.objects if o.type == "MESH")
|
||||
mesh.name = "npc_base"
|
||||
mesh.data.name = "npc_base"
|
||||
_bake_quaternius_rest(arm, mesh)
|
||||
return [arm, mesh]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gear (static, attach-point space; see GEAR_ATTACH)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1486,13 +1813,14 @@ def build_gear(name):
|
||||
bevel=0.02, segs=2))
|
||||
p.append(box("Zip", (0.24, 0.005, 0.01), (0, 0.317, 0.05), M("steel")))
|
||||
for s in (-1.0, 1.0):
|
||||
# Strap path: over the shoulder, then down the chest outside the
|
||||
# torso front (Godot z≈0.21). The old chest/waist points sat at
|
||||
# z≈0.13 inside the body, so only thin tube fragments read as two
|
||||
# rigid black bars from the front.
|
||||
p.append(tube("Strap", [(0.09 * s, 0.13, 0.27), (0.11 * s, -0.02, 0.29),
|
||||
(0.12 * s, -0.06, 0.25),
|
||||
(0.14 * s, -0.19, 0.18), (0.145 * s, -0.19, -0.08)],
|
||||
# Strap path: over the shoulder, then down the chest. The chest points
|
||||
# are pulled in to x 0.12 / Godot z 0.15 so the straps hug the slimmer
|
||||
# Quaternius torso (ADR-0018) as well as the wider procedural one;
|
||||
# the earlier x 0.145 / z 0.19 path floated ~5 cm proud of the
|
||||
# Quaternius chest and read as two detached blue bars.
|
||||
p.append(tube("Strap", [(0.075 * s, 0.13, 0.27), (0.095 * s, -0.02, 0.29),
|
||||
(0.105 * s, -0.06, 0.25),
|
||||
(0.12 * s, -0.15, 0.18), (0.125 * s, -0.15, -0.08)],
|
||||
0.016, M("denim_bag"), seg=8, width_scale=1.35,
|
||||
thickness_scale=0.42))
|
||||
elif name == "shoulder_bag":
|
||||
@@ -1555,9 +1883,13 @@ def build_gear(name):
|
||||
rot=(math.radians(90), 0, 0)))
|
||||
p.append(box("Prism", (0.05, 0.05, 0.03), (0, -0.04, 0.095), M("plastic_black"),
|
||||
bevel=0.006))
|
||||
p.append(tube("Strap", [(-0.065, -0.04, 0.06), (-0.10, 0.0, 0.25), (0.0, 0.05, 0.42),
|
||||
(0.10, 0.0, 0.25), (0.065, -0.04, 0.06)], 0.006,
|
||||
M("plastic_black"), seg=5))
|
||||
# The hand attach point hangs off the wrist, so a full neck strap would
|
||||
# float beside the head instead of resting on the neck. A short wrist
|
||||
# loop keeps every hand item self-contained and follows the hand.
|
||||
p.append(tube("WristStrap", [(-0.045, -0.02, 0.02), (-0.05, -0.035, 0.075),
|
||||
(0.0, -0.045, 0.10), (0.05, -0.035, 0.075),
|
||||
(0.045, -0.02, 0.02)], 0.005, M("plastic_black"),
|
||||
seg=5, caps=False))
|
||||
elif name == "protest_sign":
|
||||
p.append(box("Pole", (0.03, 0.03, 1.1), (0, 0, 0.35), M("wood")))
|
||||
p.append(box("Board", (0.62, 0.012, 0.42), (0, -0.025, 0.82), M("cardboard"),
|
||||
@@ -1591,7 +1923,19 @@ def build_gear(name):
|
||||
else:
|
||||
raise KeyError(name)
|
||||
obj = join(p, name)
|
||||
if GEAR[name] == "hand": # origin = Hand proxy (wrist); grip 5 cm lower
|
||||
if GEAR[name] == "head":
|
||||
# Head gear is authored around the head *centre*. The shipped roster is
|
||||
# the Quaternius base (ADR-0018): its head is 0.20 m wide against the
|
||||
# procedural library's 0.315 m, and its centre sits 0.07 m higher, so the
|
||||
# head gear is scaled and lifted to match. The procedural fallback then
|
||||
# wears slightly small hats — cosmetic, and recorded in
|
||||
# docs/blender-assets.md.
|
||||
centre = Vector((0.0, 0.0, HEAD_GEAR_LIFT))
|
||||
obj.data.transform(
|
||||
Matrix.Translation(centre)
|
||||
@ Matrix.Diagonal((HEAD_GEAR_SCALE, HEAD_GEAR_SCALE,
|
||||
HEAD_GEAR_SCALE, 1.0)))
|
||||
elif GEAR[name] == "hand": # origin = Hand proxy (wrist); grip 5 cm lower
|
||||
obj.data.transform(Matrix.Translation((0, 0, -0.05)))
|
||||
uv_unwrap(obj)
|
||||
return [obj]
|
||||
@@ -1919,15 +2263,18 @@ def build_prop(name):
|
||||
# Build + render entry points (inside Blender)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_one(category, name, head="face"):
|
||||
def _build_one(category, name, head="face", source="quaternius"):
|
||||
reset_scene()
|
||||
quaternius = (category == "npc" and source == "quaternius"
|
||||
and "base" in CHARACTERS[name])
|
||||
if category == "npc":
|
||||
objs = build_character(name, head=head)
|
||||
objs = (build_quaternius_character(name) if quaternius
|
||||
else build_character(name, head=head))
|
||||
elif category == "npc_gear":
|
||||
objs = build_gear(name)
|
||||
else:
|
||||
objs = build_prop(name)
|
||||
path = export(objs, category, name)
|
||||
path = (_export_preserving_skin if quaternius else export)(objs, category, name)
|
||||
print(f"wrote {path.relative_to(ROOT)}")
|
||||
return path
|
||||
|
||||
@@ -2164,7 +2511,8 @@ def audit_asset(category, name):
|
||||
nodes = gltf.get("nodes", [])
|
||||
bones = [nodes[j].get("name", "") for s in gltf.get("skins", [])
|
||||
for j in s.get("joints", [])]
|
||||
for b in BONES:
|
||||
required = QUATERNIUS_BONES if "UpperArm.L" in bones else BONES
|
||||
for b in required:
|
||||
if b not in bones:
|
||||
res["issues"].append(f"missing bone {b}")
|
||||
if not any(n.get("name") == "npc_base" and "mesh" in n for n in nodes):
|
||||
@@ -2326,7 +2674,13 @@ def analyse_inspection(stats: dict) -> dict:
|
||||
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"}
|
||||
# ADR-0018: a Quaternius-derived character keeps the 62-joint upstream
|
||||
# armature, so the required set is the joints `NpcAvatar` maps instead of the
|
||||
# 13-core skeleton.
|
||||
if "UpperArm.L" in bones:
|
||||
needed = set(QUATERNIUS_BONES)
|
||||
else:
|
||||
needed = set(BONES) | {"Hips", "Spine", "Neck"}
|
||||
|
||||
if not meshes:
|
||||
issues.append("no mesh objects")
|
||||
@@ -2822,7 +3176,8 @@ def write_manifest(results):
|
||||
if r["category"] == "npc":
|
||||
spec = CHARACTERS[r["name"]]
|
||||
entry.update({"layer": spec["layer"], "roles": spec["roles"],
|
||||
"tint_slots": SLOTS, "mesh_node": "npc_base"})
|
||||
"tint_slots": SLOTS, "mesh_node": "npc_base",
|
||||
"base": spec.get("base", "procedural")})
|
||||
elif r["category"] == "npc_gear":
|
||||
entry["attach"] = GEAR[r["name"]]
|
||||
else:
|
||||
@@ -2874,7 +3229,10 @@ def main(argv=None):
|
||||
s.add_argument("--only", default="")
|
||||
if cmd == "build":
|
||||
s.add_argument("--head", default="face", choices=("face", "proto", "mh"),
|
||||
help="character head source (default: face)")
|
||||
help="procedural character head source (default: face)")
|
||||
s.add_argument("--source", default="quaternius",
|
||||
choices=("quaternius", "procedural"),
|
||||
help="character mesh source (default: the Quaternius roster)")
|
||||
a = sub.add_parser("audit")
|
||||
a.add_argument("--json", action="store_true")
|
||||
fit = sub.add_parser("fit")
|
||||
@@ -2966,7 +3324,7 @@ def main(argv=None):
|
||||
return 2
|
||||
for cat, name in _selected(args.only):
|
||||
if args.cmd == "build":
|
||||
_build_one(cat, name, head=args.head)
|
||||
_build_one(cat, name, head=args.head, source=args.source)
|
||||
else:
|
||||
_render_one(cat, name)
|
||||
if args.cmd == "build":
|
||||
|
||||
@@ -90,5 +90,9 @@ echo ""
|
||||
echo "Artifacts:"
|
||||
ls -la build/*"${VERSION}"*
|
||||
echo ""
|
||||
echo "Checksums:"
|
||||
shasum -a 256 build/*"${VERSION}"* > "build/SHA256SUMS-${VERSION}.txt"
|
||||
cat "build/SHA256SUMS-${VERSION}.txt"
|
||||
echo ""
|
||||
echo "Upload to: https://git.allucanget.biz/zwitschi/security-game/releases"
|
||||
echo "See docs/RELEASING.md for the full release checklist."
|
||||
@@ -78,8 +78,18 @@ def joint_positions(gltf):
|
||||
for child in node.get("children", []):
|
||||
walk(child, world, child_q)
|
||||
|
||||
# The Blender library exports the armature root as "Armature"; the Quaternius
|
||||
# sources call it "CharacterArmature". Resolve it from the skin instead of the
|
||||
# name so a rebuilt rig cannot silently return an empty joint map.
|
||||
skin_joints = set(gltf["skins"][0]["joints"]) if gltf.get("skins") else set()
|
||||
|
||||
def holds_joints(index):
|
||||
if index in skin_joints:
|
||||
return True
|
||||
return any(holds_joints(child) for child in nodes[index].get("children", []))
|
||||
|
||||
for root in roots:
|
||||
if nodes[root].get("name") == "Armature":
|
||||
if root in skin_joints or holds_joints(root):
|
||||
walk(root)
|
||||
return out
|
||||
|
||||
@@ -108,13 +118,39 @@ def mesh_vertices(gltf, binary):
|
||||
return out
|
||||
|
||||
|
||||
## ADR-0018: a Quaternius-derived character keeps its 62-joint armature, so the
|
||||
## 13-core joint names are aliased onto the upstream ones before measuring.
|
||||
QUATERNIUS_JOINTS = {
|
||||
"ShoulderL": "UpperArm.L", "ShoulderR": "UpperArm.R",
|
||||
"ElbowL": "LowerArm.L", "ElbowR": "LowerArm.R",
|
||||
"HandL": "Wrist.L", "HandR": "Wrist.R",
|
||||
"HipL": "UpperLeg.L", "HipR": "UpperLeg.R",
|
||||
"KneeL": "LowerLeg.L", "KneeR": "LowerLeg.R",
|
||||
"Spine": "Hips",
|
||||
}
|
||||
|
||||
|
||||
def _alias_joints(joints):
|
||||
"""Return a joint map that answers both rig namings."""
|
||||
out = dict(joints)
|
||||
for core, upstream in QUATERNIUS_JOINTS.items():
|
||||
if core not in out and upstream in joints:
|
||||
out[core] = joints[upstream]
|
||||
return out
|
||||
|
||||
|
||||
def _span(values):
|
||||
return max(values) - min(values) if values else 0.0
|
||||
|
||||
|
||||
def measure(name):
|
||||
_magic, _version, _length, _size, gltf, binary = _read_glb(asset_path("npc", name))
|
||||
joints = joint_positions(gltf)
|
||||
raw_joints = joint_positions(gltf)
|
||||
# ADR-0018: the Quaternius base ships its upstream (semi-realistic)
|
||||
# proportions, so the stylized guide band does not apply to it. The row still
|
||||
# reports every measurement and ratio.
|
||||
rig = "quaternius" if "UpperArm.L" in raw_joints else "core"
|
||||
joints = _alias_joints(raw_joints)
|
||||
verts = mesh_vertices(gltf, binary)
|
||||
ys = [p[1] for p, _bone, _material in verts]
|
||||
height = _span(ys)
|
||||
@@ -123,9 +159,10 @@ def measure(name):
|
||||
shoulder = [p for p, _bone, _material in verts
|
||||
if shoulder_y - 0.08 <= p[1] <= shoulder_y + 0.06]
|
||||
hip = [p for p, bone, material in verts
|
||||
if bone in ("HipL", "HipR") and material == "pants"]
|
||||
if bone in ("HipL", "HipR", "UpperLeg.L", "UpperLeg.R")
|
||||
and material == "pants"]
|
||||
feet = [p for p, bone, _material in verts
|
||||
if bone in ("KneeL", "KneeR") and p[1] < 0.2]
|
||||
if bone in ("KneeL", "KneeR", "LowerLeg.L", "LowerLeg.R") and p[1] < 0.2]
|
||||
head_h = _span([p[1] for p in head])
|
||||
head_w = _span([p[0] for p in head])
|
||||
shoulder_w = _span([p[0] for p in shoulder])
|
||||
@@ -165,6 +202,7 @@ def measure(name):
|
||||
}
|
||||
return {
|
||||
"character": name,
|
||||
"rig": rig,
|
||||
"measurements": {key: round(value, 3) for key, value in values.items()},
|
||||
"ratios": {key: round(value, 3) for key, value in ratios.items()},
|
||||
"checks": checks,
|
||||
|
||||
@@ -56,12 +56,17 @@ class BuiltAssetTests(unittest.TestCase):
|
||||
report = {entry["character"]: entry for entry in ba.fit_report()}
|
||||
self.assertIn("npc_coat", report)
|
||||
coat = report["npc_coat"]
|
||||
self.assertAlmostEqual(coat["reference"]["head_width"], 0.315, places=2)
|
||||
self.assertAlmostEqual(coat["reference"]["clothing_radius"], 0.225, places=3)
|
||||
# Reference dimensions come from the shipped mesh, so the roster swap
|
||||
# (ADR-0018: the Quaternius base, 0.20 m head) moves them. The gear is
|
||||
# authored for the shipped roster, so the ratio checks below are what
|
||||
# must stay true; the absolute numbers are a drift guard.
|
||||
self.assertAlmostEqual(coat["reference"]["head_width"], 0.201, places=2)
|
||||
self.assertAlmostEqual(coat["reference"]["clothing_radius"], 0.19, places=3)
|
||||
sunglasses = next(row for row in coat["gear"] if row["name"] == "sunglasses")
|
||||
self.assertEqual(sunglasses["attach"], "head")
|
||||
self.assertLessEqual(sunglasses["width"],
|
||||
0.70 * coat["reference"]["head_width"])
|
||||
self.assertAlmostEqual(sunglasses["width_ratio"], 0.65, places=2)
|
||||
bag = next(row for row in coat["gear"] if row["name"] == "shoulder_bag")
|
||||
self.assertEqual(bag["attach"], "back")
|
||||
self.assertGreaterEqual(bag["x_extent"], 0.2)
|
||||
@@ -73,7 +78,7 @@ class BuiltAssetTests(unittest.TestCase):
|
||||
self.assertEqual(len(row["max"]), 3)
|
||||
self.assertIn("npc_dress", report)
|
||||
dress = report["npc_dress"]
|
||||
self.assertAlmostEqual(dress["reference"]["clothing_radius"], 0.26, places=3)
|
||||
self.assertAlmostEqual(dress["reference"]["clothing_radius"], 0.18, places=3)
|
||||
dress_bag = next(row for row in dress["gear"] if row["name"] == "shoulder_bag")
|
||||
self.assertGreater(dress_bag["x_clearance"], 0)
|
||||
|
||||
@@ -106,7 +111,9 @@ 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"})
|
||||
# The five tint slots are the contract; rebuilds may add fixed-colour
|
||||
# detail materials (the Quaternius base keeps `Eye` and `Eyebrows`).
|
||||
self.assertLessEqual(set(ba.SLOTS), set(report["slots"]))
|
||||
for slot in ba.SLOTS:
|
||||
row = report["slots"][slot]
|
||||
self.assertGreater(row["triangles"], 0, slot)
|
||||
|
||||
@@ -10,9 +10,19 @@ class NpcProportionTests(unittest.TestCase):
|
||||
self.assertEqual(len({row["character"] for row in rows}), 12)
|
||||
for row in rows:
|
||||
self.assertGreater(row["measurements"]["height"], 1.6)
|
||||
self.assertIn(row["rig"], ("core", "quaternius"))
|
||||
# Both rigs must resolve every measurement; a missing joint would
|
||||
# collapse a ratio to zero.
|
||||
for key, value in row["measurements"].items():
|
||||
self.assertGreater(value, 0.0, f"{row['character']} {key}")
|
||||
|
||||
def test_current_library_stays_inside_the_stylized_guide(self):
|
||||
for row in npc_proportions.report():
|
||||
# ADR-0018: the Quaternius base is accepted at its upstream
|
||||
# (semi-realistic) proportions, so the stylized band does not apply.
|
||||
# The measurement stays in the report for the face/silhouette gate.
|
||||
if row["rig"] != "core":
|
||||
continue
|
||||
for key, check in row["checks"].items():
|
||||
self.assertEqual(check["status"], "ok",
|
||||
f"{row['character']} {key}={check['value']}")
|
||||
|
||||