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.assertLessEqual(sunglasses["width"], 0.70 * 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) self.assertGreater(bag["x_clearance"], 0) self.assertGreater(bag["inner_x"], coat["reference"]["clothing_radius"]) 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) dress = report["npc_dress"] self.assertAlmostEqual(dress["reference"]["clothing_radius"], 0.26, places=3) dress_bag = next(row for row in dress["gear"] if row["name"] == "shoulder_bag") self.assertGreater(dress_bag["x_clearance"], 0) 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()