diff --git a/TODO.md b/TODO.md index c136ddb..88f8906 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/assets/models/blender/manifest.json b/assets/models/blender/manifest.json index 71279ad..f826199 100644 --- a/assets/models/blender/manifest.json +++ b/assets/models/blender/manifest.json @@ -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": [ diff --git a/assets/models/blender/npc/npc_crowd_a.glb b/assets/models/blender/npc/npc_crowd_a.glb new file mode 100644 index 0000000..67d257b Binary files /dev/null and b/assets/models/blender/npc/npc_crowd_a.glb differ diff --git a/assets/models/blender/npc/npc_crowd_a_ao.png b/assets/models/blender/npc/npc_crowd_a_ao.png new file mode 100644 index 0000000..23a2084 Binary files /dev/null and b/assets/models/blender/npc/npc_crowd_a_ao.png differ diff --git a/assets/models/blender/npc/npc_crowd_b.glb b/assets/models/blender/npc/npc_crowd_b.glb new file mode 100644 index 0000000..812ffae Binary files /dev/null and b/assets/models/blender/npc/npc_crowd_b.glb differ diff --git a/assets/models/blender/npc/npc_crowd_b_ao.png b/assets/models/blender/npc/npc_crowd_b_ao.png new file mode 100644 index 0000000..4ddf6d9 Binary files /dev/null and b/assets/models/blender/npc/npc_crowd_b_ao.png differ diff --git a/assets/models/blender/npc/npc_crowd_c.glb b/assets/models/blender/npc/npc_crowd_c.glb new file mode 100644 index 0000000..aeb29f4 Binary files /dev/null and b/assets/models/blender/npc/npc_crowd_c.glb differ diff --git a/assets/models/blender/npc/npc_crowd_c_ao.png b/assets/models/blender/npc/npc_crowd_c_ao.png new file mode 100644 index 0000000..01b38f6 Binary files /dev/null and b/assets/models/blender/npc/npc_crowd_c_ao.png differ diff --git a/assets/models/blender/npc/npc_crowd_d.glb b/assets/models/blender/npc/npc_crowd_d.glb new file mode 100644 index 0000000..b76cc5b Binary files /dev/null and b/assets/models/blender/npc/npc_crowd_d.glb differ diff --git a/assets/models/blender/npc/npc_crowd_d_ao.png b/assets/models/blender/npc/npc_crowd_d_ao.png new file mode 100644 index 0000000..134ca38 Binary files /dev/null and b/assets/models/blender/npc/npc_crowd_d_ao.png differ diff --git a/docs/3d-model-review.md b/docs/3d-model-review.md index daee126..8a54e89 100644 --- a/docs/3d-model-review.md +++ b/docs/3d-model-review.md @@ -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) diff --git a/docs/blender_previews/sheet_npc_baseline.jpg b/docs/blender_previews/sheet_npc_baseline.jpg index 6000ce4..27ef21e 100644 Binary files a/docs/blender_previews/sheet_npc_baseline.jpg and b/docs/blender_previews/sheet_npc_baseline.jpg differ diff --git a/src/test/capture_npc_baseline.gd b/src/test/capture_npc_baseline.gd index 78b7d22..92e21a2 100644 --- a/src/test/capture_npc_baseline.gd +++ b/src/test/capture_npc_baseline.gd @@ -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. diff --git a/tools/blender_assets.py b/tools/blender_assets.py index ae5766d..5a656cb 100644 --- a/tools/blender_assets.py +++ b/tools/blender_assets.py @@ -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. diff --git a/tools/npc_baseline.py b/tools/npc_baseline.py index eb39bb0..82bb2b7 100644 --- a/tools/npc_baseline.py +++ b/tools/npc_baseline.py @@ -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"] diff --git a/tools/tests/test_npc_baseline.py b/tools/tests/test_npc_baseline.py index 481225f..af8037d 100644 --- a/tools/tests/test_npc_baseline.py +++ b/tools/tests/test_npc_baseline.py @@ -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 diff --git a/tools/tests/test_npc_proportions.py b/tools/tests/test_npc_proportions.py index 5f645bf..7892ef0 100644 --- a/tools/tests/test_npc_proportions.py +++ b/tools/tests/test_npc_proportions.py @@ -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)