Files
security-game/tools/test_blender_assets.py
T
zwitschi 1ee32215b0
Full build / Import + headless tests (push) Successful in 38s
Full build / Full desktop build + optional release (push) Skipped
Detail the NPC avatars and finish the realism follow-ups
Blender library
- Rebuild the face: sclera, iris and upper lid per eye, brow ridge, cheekbones,
  nose bridge and tip, ears with lobes, split upper/lower lips.
- Give each hand four fingers plus a thumb and end every sleeve in a cuff ring.
- Raise CHAR_MAX_VERTS 4000 -> 5000 (16 avatars still cost ~6.9 ms of a 50 ms
  frame), decimate the body to 26 %; characters are now 3,974-5,026 tris and
  3,744-4,807 verts. Rebuild all 8 variants and re-bake their AO maps.

Capture tooling
- Add a `portrait` stage to capture_realism.gd: freeze one NPC, park the crowd
  out of frame, stand the subject on open sidewalk, turn it into the camera and
  put the guard on the sunlit side. New --portrait-type=N picks the role.
- Add the npc_portrait_day concept and map the shot to it, so avatar detail is
  scored against a portrait instead of a wide street frame.

Flashlight
- Drop the fake additive beam (Materials.light_beam + the cone mesh): on GL
  Compatibility a first-person camera inside the cone renders it as a filled
  disc with a bright far ring. Keep the tight spot (14 deg, 20 m, energy 4.2)
  and its headless test; the visible shaft is now planned behind a Forward+
  preset.

Docs and task tracking
- Move the finished realism follow-ups (ground exposure, flashlight, NPC detail,
  judge re-check) to DONE.md and expand TODO.md with step-by-step plans for NPC
  gear fit, venue geometry plus the committed gallery, and the Forward+ preset.
- Record the decisions, gotchas and portrait recipe in MEMORY.md; update the
  Blender contract, the concept README and the capture section of README.md.
- Refresh the gallery captures affected by the avatar rebuild.

Checks: headless 228/228, Python 86/86, blender_assets audit exit 0,
scene_concepts audit all GOOD, screen_designs compare 11 good / 0 bad,
smoke_npc_budget OK, boot clean (only the known headless mesh warning).
2026-11-01 17:46:25 +01:00

74 lines
3.0 KiB
Python

import json
import unittest
import blender_assets as ba
import npc_pipeline
class CatalogueTests(unittest.TestCase):
def test_contract_matches_npc_pipeline(self):
self.assertEqual(ba.SLOTS, npc_pipeline.SLOTS)
self.assertEqual(sorted(ba.BONES), sorted(npc_pipeline.BONES))
# The base mesh keeps the frozen 6000/4000 contract; library variants may
# spend up to 5000 verts on face/hand detail (documented in blender-assets.md).
self.assertLessEqual(ba.CHAR_MAX_TRIS, npc_pipeline.MAX_TRIANGLES)
self.assertLessEqual(ba.CHAR_MAX_VERTS, 5000)
def test_gear_uses_known_attach_points(self):
for name, attach in ba.GEAR.items():
self.assertIn(attach, ba.GEAR_ATTACH, name)
self.assertIn(attach, ba.ATTACH_BLENDER, name)
def test_character_gear_exists(self):
layers = set()
for name, spec in ba.CHARACTERS.items():
layers.add(spec["layer"])
for gear in spec.get("gear", []):
self.assertIn(gear, ba.GEAR, f"{name} -> {gear}")
# Every NpcAvatar.layer_for() value has a character.
for layer in ("none", "jacket", "coat", "vest", "dress"):
self.assertIn(layer, layers)
def test_emissive_props_use_emissive_palette_entries(self):
for name, spec in ba.PROPS.items():
for mat in spec.get("emissive", []):
self.assertGreater(ba.PALETTE[mat][3], 0.0, f"{name}:{mat}")
@unittest.skipUnless(ba.MANIFEST.is_file(), "Blender assets not built")
class BuiltAssetTests(unittest.TestCase):
def test_every_asset_audits_clean(self):
bad = {}
for category, name in ba.catalogue():
result = ba.audit_asset(category, name)
if not result["ok"]:
bad[name] = result["issues"]
self.assertEqual(bad, {})
def test_manifest_covers_catalogue(self):
manifest = json.loads(ba.MANIFEST.read_text(encoding="utf-8"))
self.assertEqual(set(manifest["assets"]), {n for _, n in ba.catalogue()})
def test_manifest_paths_stay_in_the_library(self):
manifest = json.loads(ba.MANIFEST.read_text(encoding="utf-8"))
for name, entry in manifest["assets"].items():
self.assertTrue(entry["path"].startswith("assets/models/blender/"), name)
if entry["category"] == "npc_gear":
self.assertIn(entry["attach"], ba.GEAR_ATTACH, name)
def test_baked_ao_maps_exist_for_every_asset(self):
from PIL import Image
for name, entry in json.loads(
ba.MANIFEST.read_text(encoding="utf-8"))["assets"].items():
path = ba.OUT_DIR / entry["category"] / f"{name}_ao.png"
self.assertTrue(path.is_file(), path)
with Image.open(path) as image:
self.assertEqual(image.size, (256, 256), path)
def test_sources_are_hidden_from_godot(self):
self.assertTrue((ba.SRC_DIR / ".gdignore").is_file())
if __name__ == "__main__":
unittest.main()