`blender_assets.py fit` measures each character head width (skin vertices weighted to Head in y 1.55-1.81) and clothing radius (cloth skirt in y 0.30-0.52, pants hip fallback), then prints every attached head/back/chest gear bounding box with head-band status and x/radial skirt clearance. The report confirms the glasses width and shoulder-bag coat clip, giving the next fit fixes numbers. Python tooling 87/87; asset audit clean.
93 lines
4.1 KiB
Python
93 lines
4.1 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_fit_report_measures_gear_against_body(self):
|
|
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)
|
|
sunglasses = next(row for row in coat["gear"] if row["name"] == "sunglasses")
|
|
self.assertEqual(sunglasses["attach"], "head")
|
|
self.assertLess(sunglasses["width"], coat["reference"]["head_width"])
|
|
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)
|
|
for character in report.values():
|
|
for row in character["gear"]:
|
|
self.assertEqual(len(row["min"]), 3)
|
|
self.assertEqual(len(row["max"]), 3)
|
|
self.assertIn("npc_dress", report)
|
|
self.assertAlmostEqual(report["npc_dress"]["reference"]["clothing_radius"], 0.26, places=3)
|
|
|
|
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()
|