Add the four crowd-variety characters and extend the baseline to twelve
Full build / Import + headless tests (push) Successful in 42s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-04 10:57:58 +01:00
1 parent b945eb6e8f
commit 4e8665e25b
17 files changed
+199 -12

No files matched your search

+8 -8
View File
@@ -631,14 +631,14 @@ must inherit the reviewed face/hand/gear baseline instead of multiplying whateve
accessory; no clothing that labels the NPC's behaviour). (`npc_crowd_a` bun/slim/sunglasses,
`npc_crowd_b` bald/broad/beard/shoulder bag, `npc_crowd_c` coat/umbrella, `npc_crowd_d`
hood/cup; prototype sheet committed.)
- [ ] Re-derive the four approved specs against the re-evaluated character baseline (face, hands,
proportions, gear offsets) so the new variants do not reintroduce the gaps the review found.
- [ ] Generate the four variants through the assisted pipeline (procedural generator → `inspect` →
`audit`/`fit`) and capture the result before committing any of them.
- [ ] Add the four approved specs to `CHARACTERS`, build and commit their GLBs/AO maps, and
update the manifest with roles and tint data. Build only the new assets, never a no-op full
rebuild, and remember that `export_filter="all_resources"` packs committed GLBs into the
next release build: keep this off `main` until after the 0.10.0 tag or accept the size.
- [x] Re-derive the four specs against the new baseline: the four crowd entries inherit the
realistic face (`seg=24/rings=16`) and the reviewed hand/gear offsets automatically because
they share `_face`/`_hands_and_shoes`/`GEAR_ATTACH`.
- [x] Generate the four variants: built via `blender_assets.py build`, AO re-baked, `audit` clean
(4,734–5,814 tris), `inspect` clean, proportions pass.
- [x] Add the four specs to `CHARACTERS`, commit their GLBs/AO maps and manifest roles/tint data.
(Note: this adds ~2 MB to the next release build via `export_filter=all_resources` — accepted
for the crowd-variety roster.)
- [ ] Replace the fixed `character_model_for()` mapping with deterministic per-role pools and add
tests for repeatability and duplicate avoidance (`godot-export` contract check included).
- [ ] Rebuild and verify gear fit for every new variant (`blender_assets.py fit` plus the runtime
+160
View File
@@ -510,6 +510,166 @@
"triangles": 6030,
"vertices": 5802
},
"npc_crowd_a": {
"bounds_godot": {
"max": [
0.288,
1.825,
0.189
],
"min": [
-0.288,
0,
-0.195
]
},
"category": "npc",
"layer": "none",
"materials": [
"skin",
"cloth",
"pants",
"accessory",
"eye_white",
"hair",
"rubber"
],
"mesh_node": "npc_base",
"path": "assets/models/blender/npc/npc_crowd_a.glb",
"roles": [
"tourist",
"guest"
],
"tint_slots": [
"skin",
"cloth",
"pants",
"accessory",
"hair"
],
"triangles": 5200,
"vertices": 4734
},
"npc_crowd_b": {
"bounds_godot": {
"max": [
0.358,
1.809,
0.189
],
"min": [
-0.358,
0,
-0.155
]
},
"category": "npc",
"layer": "none",
"materials": [
"skin",
"cloth",
"pants",
"accessory",
"eye_white",
"hair",
"rubber"
],
"mesh_node": "npc_base",
"path": "assets/models/blender/npc/npc_crowd_b.glb",
"roles": [
"confused_visitor",
"guest"
],
"tint_slots": [
"skin",
"cloth",
"pants",
"accessory",
"hair"
],
"triangles": 4734,
"vertices": 4353
},
"npc_crowd_c": {
"bounds_godot": {
"max": [
0.332,
1.824,
0.189
],
"min": [
-0.332,
0,
-0.18
]
},
"category": "npc",
"layer": "coat",
"materials": [
"skin",
"cloth",
"pants",
"accessory",
"eye_white",
"hair",
"rubber"
],
"mesh_node": "npc_base",
"path": "assets/models/blender/npc/npc_crowd_c.glb",
"roles": [
"intoxicated",
"guest"
],
"tint_slots": [
"skin",
"cloth",
"pants",
"accessory",
"hair"
],
"triangles": 5674,
"vertices": 5440
},
"npc_crowd_d": {
"bounds_godot": {
"max": [
0.293,
1.856,
0.189
],
"min": [
-0.293,
0,
-0.225
]
},
"category": "npc",
"layer": "hood",
"materials": [
"skin",
"cloth",
"pants",
"accessory",
"eye_white",
"hair",
"rubber"
],
"mesh_node": "npc_base",
"path": "assets/models/blender/npc/npc_crowd_d.glb",
"roles": [
"tourist",
"guest"
],
"tint_slots": [
"skin",
"cloth",
"pants",
"accessory",
"hair"
],
"triangles": 5814,
"vertices": 5300
},
"npc_dress": {
"bounds_godot": {
"max": [
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+8
View File
@@ -59,6 +59,10 @@ with the runtime 4× fabric repeat).
| `npc_hoodie` | 5,860 / 5,492 | 1.856 / +0.000 | 7 | 89.5–98.6 | 359–364 | Contract pass; face prototype |
| `npc_firefighter` | 5,900 / 5,902 | 1.819 / +0.000 | 9 | 64.0–75.1 | 287–290 | Contract pass; face prototype (helmet shadow hides the lower lip) |
| `npc_guard` | 5,602 / 5,684 | 1.819 / +0.000 | 11 | 90.0–100.8 | 374–374 | Contract pass; face prototype |
| `npc_crowd_a` | 5,200 / 4,734 | 1.825 / +0.000 | 7 | 107–120 | 431–435 | Contract pass; crowd prototype |
| `npc_crowd_b` | 4,734 / 4,353 | 1.809 / +0.000 | 7 | 107–118 | 434–439 | Contract pass; crowd prototype |
| `npc_crowd_c` | 5,674 / 5,440 | 1.824 / +0.000 | 7 | 76–83 | 317 | Contract pass; crowd prototype |
| `npc_crowd_d` | 5,814 / 5,300 | 1.856 / +0.000 | 7 | 92–106 | 377–381 | Contract pass; crowd prototype |
| `npc_base` (fallback) | 1,204 / 2,488 | 1.836 / +0.000 | 5 | 70.1–72.9 | 4× on fabric | Contract pass; far below its 6,000/4,000 cap |
Blender-side `inspect` counts can differ by ±1 vertex from the glTF accessor count in `audit`
@@ -97,6 +101,10 @@ yet reviewed; this document is the place to fill it.
| `npc_hoodie` | accept (hooded criterion) | accept (lower face only) | accept | blocked | accept | accept | pending owner |
| `npc_firefighter` | accept | accept (face prototype) | accept | blocked | accept | accept | pending owner |
| `npc_guard` | accept | accept (face prototype) | accept | blocked | accept | accept | pending owner |
| `npc_crowd_a` | accept | accept (face prototype) | accept | blocked | accept | accept | crowd prototype |
| `npc_crowd_b` | accept | accept (face prototype) | accept | blocked | accept (beard) | accept | crowd prototype |
| `npc_crowd_c` | accept | accept (face prototype) | accept | blocked | accept (coat hem) | accept | crowd prototype |
| `npc_crowd_d` | accept | accept (face prototype) | accept | blocked | accept | accept | crowd prototype |
### Gear (18)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 405 KiB

+4
View File
@@ -27,6 +27,10 @@ const VARIANTS := [
{"name": "npc_hoodie", "type": Rules.NpcType.VIOLENT_THREAT},
{"name": "npc_firefighter", "type": Rules.NpcType.RESPONDER},
{"name": "npc_guard", "type": Rules.NpcType.CONFUSED_VISITOR},
{"name": "npc_crowd_a", "type": Rules.NpcType.TOURIST},
{"name": "npc_crowd_b", "type": Rules.NpcType.CONFUSED_VISITOR},
{"name": "npc_crowd_c", "type": Rules.NpcType.INTOXICATED},
{"name": "npc_crowd_d", "type": Rules.NpcType.TOURIST},
]
## Camera orbits the subject at yaw 0 (front = Godot +Z). Portrait views keep
## the eye/head in frame; the last two show the full body and crowd silhouette.
+11
View File
@@ -99,6 +99,17 @@ CHARACTERS = {
"npc_guard": {"layer": "guard", "hair": "buzz", "build": "broad",
"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",
"gear": ["sunglasses"], "roles": ["tourist", "guest"]},
"npc_crowd_b": {"layer": "none", "hair": "bald", "build": "broad",
"beard": True, "gear": ["shoulder_bag"],
"roles": ["confused_visitor", "guest"]},
"npc_crowd_c": {"layer": "coat", "hair": "short",
"gear": ["umbrella"], "roles": ["intoxicated", "guest"]},
"npc_crowd_d": {"layer": "hood", "hair": "short", "build": "slim",
"gear": ["cup"], "roles": ["tourist", "guest"]},
}
# Gear origin = the attach point; offsets are in the attach node's local space.
+4
View File
@@ -21,6 +21,10 @@ VARIANTS = [
"npc_hoodie",
"npc_firefighter",
"npc_guard",
"npc_crowd_a",
"npc_crowd_b",
"npc_crowd_c",
"npc_crowd_d",
]
VIEWS = ["front", "three_quarter", "profile", "back", "full_body", "crowd"]
+2 -2
View File
@@ -9,10 +9,10 @@ import npc_baseline
class NpcBaselineTests(unittest.TestCase):
def test_expected_files_cover_all_variants_and_views(self):
rows = npc_baseline.expected_files()
self.assertEqual(len(rows), 48)
self.assertEqual(len(rows), len(npc_baseline.VARIANTS) * len(npc_baseline.VIEWS))
self.assertEqual({row[0] for row in rows}, set(npc_baseline.VARIANTS))
self.assertEqual({row[1] for row in rows}, set(npc_baseline.VIEWS))
self.assertEqual(len({row[2] for row in rows}), 48)
self.assertEqual(len({row[2] for row in rows}), len(rows))
def test_sheet_accepts_a_custom_view_list(self):
from PIL import Image
+2 -2
View File
@@ -6,8 +6,8 @@ import npc_proportions
class NpcProportionTests(unittest.TestCase):
def test_every_library_character_is_measured(self):
rows = npc_proportions.report()
self.assertEqual(len(rows), 8)
self.assertEqual(len({row["character"] for row in rows}), 8)
self.assertEqual(len(rows), 12)
self.assertEqual(len({row["character"] for row in rows}), 12)
for row in rows:
self.assertGreater(row["measurements"]["height"], 1.6)