"""Build Crystal Bat Matriarch as a rigged, animated, runtime-ready GLB. Original 3D interpretation of IWT2's Crystal Bat Matriarch concept. Run with: /Applications/Blender.app/Contents/MacOS/Blender --background --factory-startup \ --python scripts/blender/build_crystal_bat_matriarch.py """ from __future__ import annotations import json import math import sys from pathlib import Path import bpy sys.path.insert(0, str(Path(__file__).parent)) from build_iwt2_boss_trio import ( # noqa: E402 OUT_ROOT, actions, armature, cone, ellipsoid, export_asset, finish, join_parts, plate, prepare_materials, reset_scene, ) ASSET_ID = "crystal-bat-matriarch" def wing_panel(name: str, side: int, material: bpy.types.Material, bone: str) -> bpy.types.Object: """Create a broad, faceted, low-poly wing membrane with a shallow volume.""" outline = [ (side * 0.55, 2.82), (side * 1.04, 3.85), (side * 2.08, 4.28), (side * 3.76, 4.1), (side * 3.42, 2.72), (side * 2.55, 1.75), (side * 1.52, 2.12), ] front_y, back_y = -0.04, 0.18 vertices = [(x, front_y, z) for x, z in outline] + [(x, back_y, z) for x, z in outline] count = len(outline) faces = [tuple(range(count)), tuple(range(count, count * 2))[::-1]] for index in range(count): next_index = (index + 1) % count faces.append((index, next_index, count + next_index, count + index)) mesh = bpy.data.meshes.new(f"{name}_Mesh") mesh.from_pydata(vertices, [], faces) mesh.validate() mesh.update() object = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(object) return finish(object, name, material, bone, smooth=False) def sonic_ring(name: str, location: tuple[float, float, float], radius: float, material: bpy.types.Material) -> bpy.types.Object: bpy.ops.mesh.primitive_torus_add( major_radius=radius, minor_radius=0.028, major_segments=12, minor_segments=4, location=location, rotation=(math.radians(90), 0, 0), ) return finish(bpy.context.object, name, material, "Head", smooth=False) def build_crystal_bat() -> None: reset_scene() mats = prepare_materials({ "Fur": {"color": (0.095, 0.045, 0.16, 1), "metallic": 0.06, "roughness": 0.7}, "Membrane": {"color": (0.035, 0.022, 0.07, 1), "metallic": 0.12, "roughness": 0.42}, "Amethyst": {"color": (0.42, 0.10, 0.76, 1), "metallic": 0.32, "roughness": 0.22}, "Ice": {"color": (0.25, 0.86, 1, 1), "metallic": 0.22, "roughness": 0.18, "emission": (0.04, 0.52, 1, 1), "strength": 2.8}, "Mirror": {"color": (0.6, 0.86, 1, 1), "metallic": 0.85, "roughness": 0.12}, "Mouth": {"color": (0.26, 0.015, 0.08, 1), "roughness": 0.5}, "Claw": {"color": (0.58, 0.64, 0.82, 1), "metallic": 0.3, "roughness": 0.28}, }) specs = [ ("Root", (0, 0, 0), (0, 0, 0.65), None), ("Body", (0, 0, 2.35), (0, 0, 3.12), "Root"), ("Head", (0, -0.54, 2.6), (0, -1.23, 2.74), "Body"), ("Wing.L", (-0.56, 0.04, 2.9), (-3.7, 0.2, 3.95), "Body"), ("Wing.R", (0.56, 0.04, 2.9), (3.7, 0.2, 3.95), "Body"), ("Arm.L", (-0.46, -0.38, 2.18), (-1.34, -1.58, 1.55), "Body"), ("Arm.R", (0.46, -0.38, 2.18), (1.34, -1.58, 1.55), "Body"), ("Tail.1", (0, 0.52, 2.2), (0, 1.32, 1.84), "Body"), ("Tail.2", (0, 1.28, 1.86), (0, 2.08, 2.22), "Tail.1"), ] rig = armature("CrystalBatMatriarch", specs) ellipsoid("CrystalBatMatriarch_Body", (0, 0.08, 2.34), (1.03, 0.76, 0.78), mats["Fur"], "Body", 2) ellipsoid("ChestRuff", (0, -0.42, 2.44), (0.92, 0.52, 0.7), mats["Claw"], "Body", 2) ellipsoid("NeckRuff", (0, -0.72, 2.62), (0.62, 0.36, 0.58), mats["Fur"], "Head", 1) for index, y in enumerate((-0.04, 0.28, 0.58)): plate( f"BackCrystal{index}", (0, y, 3.05 + index * 0.02), (0.64 - index * 0.07, 0.42, 0.34), (math.radians(86), 0, 0), mats["Amethyst"], "Body", ) cone(f"BackSpike{index}", (0, y, 3.22), (0, y + 0.04, 3.8 - index * 0.1), 0.15, 0, mats["Ice"], "Body", 5) ellipsoid("CrystalBatMatriarch_Head", (0, -1.04, 2.7), (0.68, 0.68, 0.56), mats["Fur"], "Head", 2) ellipsoid("Muzzle", (0, -1.58, 2.52), (0.5, 0.3, 0.28), mats["Mouth"], "Head", 1) cone("OpenMouth", (0, -1.58, 2.52), (0, -2.07, 2.42), 0.3, 0.12, mats["Mouth"], "Head", 6) for side, suffix in ((-1, "L"), (1, "R")): ellipsoid(f"Eye{suffix}", (side * 0.34, -1.5, 2.84), (0.09, 0.055, 0.09), mats["Ice"], "Head", 1) cone(f"Ear{suffix}", (side * 0.36, -0.98, 3.08), (side * 0.75, -0.84, 3.78), 0.19, 0, mats["Amethyst"], "Head", 5) cone(f"CrownCrystal{suffix}", (side * 0.22, -1.02, 3.16), (side * 0.55, -1.08, 3.82), 0.16, 0, mats["Ice"], "Head", 5) cone(f"Fang{suffix}", (side * 0.2, -1.77, 2.47), (side * 0.24, -1.88, 2.24), 0.055, 0, mats["Claw"], "Head", 5) cone("CrownCrystalCenter", (0, -1.02, 3.2), (0, -1.16, 3.96), 0.19, 0, mats["Amethyst"], "Head", 5) for index, radius in enumerate((0.34, 0.51, 0.68)): sonic_ring(f"SonicRing{index}", (0, -2.05 - index * 0.12, 2.48), radius, mats["Ice"]) for side, suffix in ((-1, "L"), (1, "R")): wing = f"Wing.{suffix}" wing_panel(f"WingPanel{suffix}", side, mats["Membrane"], wing) cone(f"WingArm{suffix}", (side * 0.5, 0.05, 2.92), (side * 1.35, 0.06, 3.88), 0.18, 0.1, mats["Fur"], wing, 6) cone(f"WingFingerA{suffix}", (side * 1.15, 0.07, 3.78), (side * 3.62, 0.08, 4.08), 0.09, 0.025, mats["Amethyst"], wing, 5) cone(f"WingFingerB{suffix}", (side * 1.1, 0.08, 3.52), (side * 3.35, 0.08, 2.72), 0.075, 0.018, mats["Amethyst"], wing, 5) for index, (x, z, scale) in enumerate(((1.54, 4.26, 0.22), (2.48, 3.95, 0.18), (3.16, 3.15, 0.15))): cone(f"WingCrystal{suffix}{index}", (side * x, -0.03, z), (side * (x + 0.2), -0.04, z + scale * 2.2), scale, 0, mats["Ice"], wing, 5) arm = f"Arm.{suffix}" cone(f"ArmUpper{suffix}", (side * 0.5, -0.38, 2.24), (side * 0.92, -1.04, 1.86), 0.18, 0.12, mats["Fur"], arm, 6) cone(f"ArmLower{suffix}", (side * 0.9, -1.02, 1.86), (side * 1.28, -1.45, 1.48), 0.12, 0.075, mats["Fur"], arm, 6) ellipsoid(f"Palm{suffix}", (side * 1.3, -1.46, 1.46), (0.18, 0.17, 0.16), mats["Amethyst"], arm, 1) for finger in (-0.14, 0, 0.14): cone(f"Claw{suffix}{finger}", (side * 1.3, -1.5, 1.45), (side * 1.55, -1.78, 1.14 + finger), 0.05, 0.007, mats["Claw"], arm, 5) cone("TailSegment1", (0, 0.48, 2.18), (0, 1.3, 1.82), 0.32, 0.2, mats["Fur"], "Tail.1", 7) cone("TailSegment2", (0, 1.27, 1.82), (0, 2.04, 2.15), 0.2, 0.08, mats["Fur"], "Tail.2", 7) cone("TailCrystal", (0, 2.0, 2.14), (0, 2.46, 2.52), 0.16, 0, mats["Amethyst"], "Tail.2", 5) for index, (x, y, z, rotation) in enumerate(((-1.55, -0.4, 3.52, -0.35), (1.52, -0.66, 3.18, 0.42), (-2.35, -0.3, 2.6, -0.5), (2.32, -0.5, 2.42, 0.38))): ellipsoid(f"MirrorShard{index}", (x, y, z), (0.2, 0.06, 0.42), mats["Mirror"], "Body", 1, rotation=(0, rotation, 0.18 * index)) body = join_parts("CrystalBatMatriarch", rig) clips = actions(rig, crystal_bat_actions()) export_asset( ASSET_ID, "Crystal Bat Matriarch", rig, body, clips, [ ("Core", (0, 0.02, 2.38), (1.22, 0.92, 0.95)), ("Head", (0, -1.08, 2.68), (0.78, 0.78, 0.65)), ("Wings", (0, 0.05, 3.06), (3.82, 0.46, 1.45)), ], (0, -0.02, 2.55), 11.5, "MirrorShatter", 22, ) metadata_path = OUT_ROOT / ASSET_ID / f"{ASSET_ID}.asset.json" metadata = json.loads(metadata_path.read_text()) metadata["sourceConcept"] = "Original 3D interpretation of IWT2 Crystal Bat Matriarch concept art" metadata["runtime"]["forward"] = "-Y" metadata["runtime"]["unit"] = "meters" metadata_path.write_text(json.dumps(metadata, indent=2) + "\n") def crystal_bat_actions(): return [ ("Idle", 60, True, [ {"frame": 1}, {"frame": 14, "locations": {"Root": (0, 0, 0.14)}, "rotations": {"Body": (3, 0, 0), "Head": (2, 4, 0), "Wing.L": (8, 13, 4), "Wing.R": (8, -13, -4), "Tail.2": (0, 0, 7)}}, {"frame": 30, "locations": {"Root": (0, 0, 0.04)}, "rotations": {"Head": (0, -4, 0), "Wing.L": (2, 5, 0), "Wing.R": (2, -5, 0), "Tail.2": (0, 0, -7)}}, {"frame": 45, "locations": {"Root": (0, 0, 0.14)}, "rotations": {"Body": (-3, 0, 0), "Head": (2, -4, 0), "Wing.L": (8, 13, 4), "Wing.R": (8, -13, -4), "Tail.2": (0, 0, 7)}}, {"frame": 60}, ]), ("Swoop", 36, True, [ {"frame": 1, "rotations": {"Wing.L": (14, 25, 6), "Wing.R": (14, -25, -6)}}, {"frame": 9, "locations": {"Root": (0, -0.06, 0.18)}, "rotations": {"Body": (-12, 0, 0), "Wing.L": (26, 46, 10), "Wing.R": (26, -46, -10)}}, {"frame": 18, "locations": {"Root": (0, -0.12, 0.04)}, "rotations": {"Body": (8, 0, 0), "Wing.L": (-8, 12, 0), "Wing.R": (-8, -12, 0)}}, {"frame": 27, "locations": {"Root": (0, -0.06, 0.18)}, "rotations": {"Body": (-12, 0, 0), "Wing.L": (26, 46, 10), "Wing.R": (26, -46, -10)}}, {"frame": 36, "rotations": {"Wing.L": (14, 25, 6), "Wing.R": (14, -25, -6)}}, ]), ("SonicPulse", 38, False, [ {"frame": 1}, {"frame": 10, "locations": {"Root": (0, 0, 0.12)}, "rotations": {"Head": (-10, 0, 0), "Body": (-7, 0, 0), "Wing.L": (10, 32, 5), "Wing.R": (10, -32, -5)}}, {"frame": 17, "locations": {"Root": (0, -0.08, 0.26)}, "scales": {"Body": (1.1, 1.1, 1.1)}, "rotations": {"Head": (23, 0, 0), "Wing.L": (24, 62, 11), "Wing.R": (24, -62, -11), "Arm.L": (-22, 0, -16), "Arm.R": (-22, 0, 16)}}, {"frame": 25, "scales": {"Body": (0.97, 0.97, 0.97)}, "rotations": {"Head": (-4, 0, 0), "Wing.L": (5, 15, 0), "Wing.R": (5, -15, 0)}}, {"frame": 38}, ]), ("MirrorShatter", 46, False, [ {"frame": 1}, {"frame": 12, "locations": {"Root": (0, 0, 0.12)}, "rotations": {"Body": (-10, 0, 0), "Wing.L": (-18, -12, 0), "Wing.R": (-18, 12, 0), "Tail.1": (-12, 0, 0)}}, {"frame": 22, "locations": {"Root": (0, -0.1, 0.25)}, "rotations": {"Body": (18, 0, 0), "Head": (18, 0, 0), "Wing.L": (32, 72, 14), "Wing.R": (32, -72, -14), "Tail.1": (18, 0, 0), "Tail.2": (-22, 0, 0)}}, {"frame": 31, "rotations": {"Wing.L": (8, 22, 0), "Wing.R": (8, -22, 0), "Tail.2": (8, 0, 0)}}, {"frame": 46}, ]), ("Stagger", 28, False, [ {"frame": 1}, {"frame": 6, "locations": {"Root": (0.14, 0.1, -0.08)}, "rotations": {"Body": (-14, 0, 15), "Head": (21, 0, -12), "Wing.L": (22, 0, -14), "Wing.R": (22, 0, 14)}}, {"frame": 15, "rotations": {"Body": (7, 0, -7), "Head": (-8, 0, 5)}}, {"frame": 28}, ]), ("Death", 70, False, [ {"frame": 1}, {"frame": 18, "locations": {"Root": (0.14, 0.1, -0.3)}, "rotations": {"Root": (0, 22, 26), "Body": (18, 0, 12), "Head": (28, 0, -12), "Wing.L": (28, 34, 8), "Wing.R": (24, -34, -8)}}, {"frame": 44, "locations": {"Root": (0.34, 0.1, -1.12)}, "rotations": {"Root": (0, 50, 78), "Body": (32, 0, 26), "Head": (48, 0, -24), "Wing.L": (62, 76, 18), "Wing.R": (56, -76, -18), "Tail.1": (-24, 0, 0)}}, {"frame": 70, "locations": {"Root": (0.34, 0.1, -1.14)}, "rotations": {"Root": (0, 50, 78), "Body": (32, 0, 26), "Head": (48, 0, -24), "Wing.L": (62, 76, 18), "Wing.R": (56, -76, -18), "Tail.1": (-24, 0, 0)}}, ]), ] if __name__ == "__main__": OUT_ROOT.mkdir(parents=True, exist_ok=True) build_crystal_bat()