"""Build three original IWT2-inspired, rigged low-poly boss assets."""

from __future__ import annotations

import json
import math
from pathlib import Path

import bpy
from mathutils import Vector


ROOT = Path(__file__).resolve().parents[2]
OUT_ROOT = ROOT / "game_assets/models/original/bosses"
FPS = 30
PARTS: list[bpy.types.Object] = []
RUNTIME_MATERIALS: list[bpy.types.Material] = []


def reset_scene() -> None:
    global PARTS, RUNTIME_MATERIALS
    PARTS = []
    RUNTIME_MATERIALS = []
    if bpy.context.object and bpy.context.object.mode != "OBJECT":
        bpy.ops.object.mode_set(mode="OBJECT")
    bpy.ops.object.select_all(action="SELECT")
    bpy.ops.object.delete(use_global=False)
    for blocks in (bpy.data.meshes, bpy.data.armatures, bpy.data.materials, bpy.data.cameras, bpy.data.lights, bpy.data.actions):
        for block in list(blocks):
            blocks.remove(block)


def material(name, color, metallic=0.0, roughness=0.5, emission=None, strength=0.0):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    mat.diffuse_color = color
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    bsdf.inputs["Base Color"].default_value = color
    bsdf.inputs["Metallic"].default_value = metallic
    bsdf.inputs["Roughness"].default_value = roughness
    if emission:
        bsdf.inputs["Emission Color"].default_value = emission
        bsdf.inputs["Emission Strength"].default_value = strength
    return mat


def prepare_materials(specs):
    mats = {name: material(f"M_{name}", **settings) for name, settings in specs.items()}
    RUNTIME_MATERIALS.extend(mats.values())
    return mats


def finish(obj, name, mat, bone, smooth=True):
    obj.name = name
    obj.data.name = f"{name}_Mesh"
    for runtime_mat in RUNTIME_MATERIALS:
        obj.data.materials.append(runtime_mat)
    mat_index = next(index for index, candidate in enumerate(RUNTIME_MATERIALS) if candidate.name == mat.name)
    for polygon in obj.data.polygons:
        polygon.material_index = mat_index
        polygon.use_smooth = smooth
    bone_group = obj.vertex_groups.new(name=bone)
    bone_group.add(range(len(obj.data.vertices)), 1.0, "REPLACE")
    mat_group = obj.vertex_groups.new(name=f"__MAT_{mat_index}")
    mat_group.add(range(len(obj.data.vertices)), 1.0, "REPLACE")
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    PARTS.append(obj)
    return obj


def ellipsoid(name, location, scale, mat, bone, subdivisions=1, rotation=(0, 0, 0)):
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=subdivisions, radius=1, location=location, rotation=rotation)
    obj = bpy.context.object
    obj.scale = scale
    return finish(obj, name, mat, bone)


def cone(name, start, end, radius_start, radius_end, mat, bone, vertices=7):
    start_v, end_v = Vector(start), Vector(end)
    direction = end_v - start_v
    bpy.ops.mesh.primitive_cone_add(
        vertices=vertices,
        radius1=radius_start,
        radius2=radius_end,
        depth=direction.length,
        location=(start_v + end_v) * 0.5,
    )
    obj = bpy.context.object
    obj.rotation_mode = "QUATERNION"
    obj.rotation_quaternion = direction.to_track_quat("Z", "Y")
    obj.rotation_mode = "XYZ"
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
    return finish(obj, name, mat, bone, smooth=False)


def plate(name, location, scale, rotation, mat, bone, vertices=6):
    bpy.ops.mesh.primitive_cone_add(vertices=vertices, radius1=1, radius2=0.58, depth=0.42, location=location, rotation=rotation)
    obj = bpy.context.object
    obj.scale = scale
    return finish(obj, name, mat, bone, smooth=False)


def armature(name, specs):
    data = bpy.data.armatures.new(f"{name}_Rig")
    rig = bpy.data.objects.new(f"{name}_Rig", data)
    bpy.context.collection.objects.link(rig)
    bpy.context.view_layer.objects.active = rig
    rig.select_set(True)
    bpy.ops.object.mode_set(mode="EDIT")
    for bone_name, head, tail, parent in specs:
        bone = data.edit_bones.new(bone_name)
        bone.head, bone.tail = head, tail
        if parent:
            bone.parent = data.edit_bones[parent]
    bpy.ops.object.mode_set(mode="POSE")
    for bone in rig.pose.bones:
        bone.rotation_mode = "XYZ"
    bpy.ops.object.mode_set(mode="OBJECT")
    return rig


def join_parts(name, rig):
    bpy.ops.object.select_all(action="DESELECT")
    for obj in PARTS:
        obj.select_set(True)
    bpy.context.view_layer.objects.active = PARTS[0]
    bpy.ops.object.join()
    body = bpy.context.object
    body.name = f"{name}_Body"
    body.data.name = f"{name}_Body_Mesh"
    material_groups = {g.index: int(g.name.removeprefix("__MAT_")) for g in body.vertex_groups if g.name.startswith("__MAT_")}
    face_materials = []
    for polygon in body.data.polygons:
        vertex = body.data.vertices[polygon.vertices[0]]
        face_materials.append(next(material_groups[a.group] for a in vertex.groups if a.group in material_groups))
    body.data.materials.clear()
    for mat in RUNTIME_MATERIALS:
        body.data.materials.append(mat)
    for polygon, index in zip(body.data.polygons, face_materials, strict=True):
        polygon.material_index = index
    for group in [g for g in body.vertex_groups if g.name.startswith("__MAT_")]:
        body.vertex_groups.remove(group)
    modifier = body.modifiers.new(f"{name}_Armature", "ARMATURE")
    modifier.object = rig
    body.parent = rig
    return body


def reset_pose(rig):
    for bone in rig.pose.bones:
        bone.location = (0, 0, 0)
        bone.rotation_euler = (0, 0, 0)
        bone.scale = (1, 1, 1)


def key_pose(rig, frame, rotations=None, locations=None, scales=None):
    reset_pose(rig)
    for name, value in (rotations or {}).items():
        rig.pose.bones[name].rotation_euler = tuple(math.radians(component) for component in value)
    for name, value in (locations or {}).items():
        rig.pose.bones[name].location = value
    for name, value in (scales or {}).items():
        rig.pose.bones[name].scale = value
    for bone in rig.pose.bones:
        bone.keyframe_insert("location", frame=frame, group=bone.name)
        bone.keyframe_insert("rotation_euler", frame=frame, group=bone.name)
        bone.keyframe_insert("scale", frame=frame, group=bone.name)


def actions(rig, definitions):
    rig.animation_data_create()
    metadata = {}
    for name, end_frame, loop, poses in definitions:
        action = bpy.data.actions.new(name)
        action.use_fake_user = True
        action.use_frame_range = True
        action.frame_start, action.frame_end, action.use_cyclic = 1, end_frame, loop
        rig.animation_data.action = action
        for pose in poses:
            key_pose(rig, **pose)
        rig.animation_data.action = None
        metadata[name] = {"frames": end_frame, "seconds": round(end_frame / FPS, 3), "loop": loop}
    return metadata


def select_only(objects):
    bpy.ops.object.select_all(action="DESELECT")
    for obj in objects:
        obj.hide_set(False)
        obj.select_set(True)
    bpy.context.view_layer.objects.active = objects[0]


def collision(asset_id, specs, out_dir):
    objects = []
    for name, location, scale in specs:
        bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=1, location=location)
        obj = bpy.context.object
        obj.name = f"COLLISION_{name}"
        obj.scale = scale
        bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
        objects.append(obj)
    select_only(objects)
    bpy.ops.export_scene.gltf(
        filepath=str(out_dir / f"{asset_id}_collision.glb"), export_format="GLB", use_selection=True,
        export_animations=False, export_skins=False, export_materials="NONE", export_apply=True, export_yup=True,
    )
    for obj in objects:
        obj.hide_render = True
        obj.hide_set(True)
    return objects


def look_at(obj, target):
    obj.rotation_euler = (Vector(target) - obj.location).to_track_quat("-Z", "Y").to_euler()


def studio():
    scene = bpy.context.scene
    scene.render.engine = "BLENDER_EEVEE"
    scene.render.resolution_x = scene.render.resolution_y = 760
    scene.render.resolution_percentage = 100
    scene.render.image_settings.file_format = "PNG"
    scene.render.fps = FPS
    scene.world.use_nodes = True
    scene.world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.005, 0.007, 0.012, 1)
    scene.world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.3
    camera_data = bpy.data.cameras.new("PreviewCamera")
    camera = bpy.data.objects.new("PreviewCamera", camera_data)
    bpy.context.collection.objects.link(camera)
    scene.camera = camera
    for name, location, color, energy, size in [
        ("Key", (5, -7, 7), (1, 0.34, 0.1), 1200, 5),
        ("Fill", (-5, -3, 4), (0.12, 0.32, 1), 800, 4),
        ("Rim", (1, 6, 6), (1, 0.12, 0.02), 1150, 3),
    ]:
        data = bpy.data.lights.new(name, "AREA")
        data.energy, data.color, data.shape, data.size = energy, color, "DISK", size
        obj = bpy.data.objects.new(name, data)
        obj.location = location
        bpy.context.collection.objects.link(obj)
        look_at(obj, (0, 0, 1.5))
    floor_mat = material("M_PreviewFloor", color=(0.014, 0.018, 0.026, 1), roughness=0.85)
    bpy.ops.mesh.primitive_plane_add(size=28, location=(0, 0, 0))
    floor = bpy.context.object
    floor.data.materials.append(floor_mat)
    return camera


def render_previews(rig, camera, out_dir, asset_id, target, distance, special_clip, special_frame):
    preview_dir = out_dir / "previews"
    preview_dir.mkdir(parents=True, exist_ok=True)
    for filename, position, clip, frame in [
        (f"{asset_id}_three_quarter.png", (distance * 0.62, -distance, distance * 0.48), "Idle", 15),
        (f"{asset_id}_side.png", (distance, 0, distance * 0.42), "Idle", 15),
        (f"{asset_id}_{special_clip.lower()}.png", (distance * 0.62, -distance, distance * 0.48), special_clip, special_frame),
    ]:
        rig.animation_data.action = bpy.data.actions[clip]
        bpy.context.scene.frame_set(frame)
        camera.location = position
        camera.data.lens = 58
        look_at(camera, target)
        bpy.context.scene.render.filepath = str(preview_dir / filename)
        bpy.ops.render.render(write_still=True)
    rig.animation_data.action = None


def export_asset(asset_id, display_name, rig, body, clips, collision_specs, preview_target, preview_distance, special_clip, special_frame):
    out_dir = OUT_ROOT / asset_id
    out_dir.mkdir(parents=True, exist_ok=True)
    select_only([rig, body])
    bpy.ops.export_scene.gltf(
        filepath=str(out_dir / f"{asset_id}.glb"), export_format="GLB", use_selection=True,
        export_animations=True, export_animation_mode="ACTIONS", export_frame_range=True,
        export_skins=True, export_morph=False, export_yup=True, export_apply=False,
        export_cameras=False, export_lights=False,
    )
    collision(asset_id, collision_specs, out_dir)
    triangles = sum(len(poly.vertices) - 2 for poly in body.data.polygons)
    metadata = {
        "name": display_name,
        "assetId": asset_id,
        "authoringTool": bpy.app.version_string,
        "format": "glTF 2.0 binary (GLB)",
        "runtime": {
            "renderMesh": body.name,
            "armature": rig.name,
            "collisionAsset": f"{asset_id}_collision.glb",
            "materials": [mat.name for mat in body.data.materials],
            "trianglesApprox": triangles,
        },
        "animations": clips,
    }
    (out_dir / f"{asset_id}.asset.json").write_text(json.dumps(metadata, indent=2) + "\n")
    camera = studio()
    render_previews(rig, camera, out_dir, asset_id, preview_target, preview_distance, special_clip, special_frame)
    bpy.ops.wm.save_as_mainfile(filepath=str(out_dir / f"{asset_id}.blend"))
    print(f"BUILT={asset_id} TRIANGLES={triangles} CLIPS={','.join(clips)}")


def build_ram():
    reset_scene()
    mats = prepare_materials({
        "Obsidian": {"color": (0.025, 0.028, 0.034, 1), "metallic": 0.28, "roughness": 0.4},
        "Basalt": {"color": (0.10, 0.09, 0.09, 1), "metallic": 0.14, "roughness": 0.62},
        "Armor": {"color": (0.18, 0.16, 0.16, 1), "metallic": 0.22, "roughness": 0.48},
        "Lava": {"color": (1, 0.08, 0.003, 1), "roughness": 0.2, "emission": (1, 0.025, 0, 1), "strength": 9},
    })
    specs = [
        ("Root", (0, 0, 0), (0, 0, 0.5), None), ("Body", (0, 0, 1.35), (0, 0, 2.5), "Root"),
        ("Head", (0, -0.75, 2.05), (0, -1.75, 2.05), "Body"), ("Horn.L", (-0.45, -1.45, 2.35), (-1.2, -1.5, 2.55), "Head"),
        ("Horn.R", (0.45, -1.45, 2.35), (1.2, -1.5, 2.55), "Head"), ("Leg.FL", (-0.7, -0.75, 1.45), (-0.78, -0.8, 0.35), "Body"),
        ("Leg.FR", (0.7, -0.75, 1.45), (0.78, -0.8, 0.35), "Body"), ("Leg.BL", (-0.7, 0.8, 1.35), (-0.78, 0.8, 0.3), "Body"),
        ("Leg.BR", (0.7, 0.8, 1.35), (0.78, 0.8, 0.3), "Body"), ("Tail", (0, 1.2, 1.75), (0, 2.25, 1.35), "Body"),
    ]
    rig = armature("ObsidianRam", specs)
    ellipsoid("BodyCore", (0, 0.1, 1.95), (1.18, 1.48, 0.86), mats["Basalt"], "Body", 2)
    ellipsoid("ShoulderMass", (0, -0.62, 2.08), (1.28, 0.82, 1.0), mats["Obsidian"], "Body", 2)
    ellipsoid("Head", (0, -1.48, 1.93), (0.72, 0.78, 0.62), mats["Armor"], "Head", 2)
    cone("Snout", (0, -1.7, 1.92), (0, -2.25, 1.72), 0.48, 0.22, mats["Obsidian"], "Head")
    ellipsoid("Eye.L", (-0.47, -1.9, 2.08), (0.075, 0.05, 0.075), mats["Lava"], "Head")
    ellipsoid("Eye.R", (0.47, -1.9, 2.08), (0.075, 0.05, 0.075), mats["Lava"], "Head")
    for side, suffix in [(-1, "L"), (1, "R")]:
        points = [(side * 0.48, -1.55, 2.33), (side * 1.05, -1.62, 2.67), (side * 1.45, -1.44, 2.35), (side * 1.43, -1.25, 1.93), (side * 1.02, -1.45, 1.78)]
        for index in range(len(points) - 1):
            cone(f"Horn{suffix}_{index}", points[index], points[index + 1], 0.19 - index * 0.025, 0.15 - index * 0.025, mats["Obsidian"], f"Horn.{suffix}", 8)
        for front, y, bone in [(True, -0.72, f"Leg.F{suffix}"), (False, 0.82, f"Leg.B{suffix}")]:
            x = side * 0.78
            cone(f"Leg{suffix}_{'F' if front else 'B'}", (x, y, 1.45), (x * 1.05, y, 0.35), 0.34, 0.24, mats["Basalt"], bone, 7)
            ellipsoid(f"Hoof{suffix}_{'F' if front else 'B'}", (x * 1.06, y - 0.18, 0.25), (0.34, 0.46, 0.24), mats["Obsidian"], bone)
    cone("Tail", (0, 1.1, 1.72), (0, 2.22, 1.3), 0.38, 0.12, mats["Basalt"], "Tail", 7)
    for index, y in enumerate((-0.7, -0.15, 0.4, 0.95)):
        plate(f"BackPlate{index}", (0, y, 2.72 + index * 0.03), (0.92 - index * 0.08, 0.62, 0.32), (math.radians(82), 0, 0), mats["Armor"], "Body")
        cone(f"BackSpike{index}", (0, y, 2.86), (0, y + 0.05, 3.38 - index * 0.06), 0.17, 0, mats["Obsidian"], "Body", 6)
    for side in (-1, 1):
        cone(f"ShoulderSpike{side}", (side * 0.9, -0.55, 2.55), (side * 1.48, -0.5, 2.82), 0.18, 0, mats["Obsidian"], "Body", 6)
    for index, (start, end) in enumerate([((-0.12, -1.96, 2.22), (0.1, -1.98, 1.78)), ((-0.55, -0.98, 2.54), (-0.35, -0.99, 1.93)), ((0.48, 0.42, 2.55), (0.25, 0.45, 1.95))]):
        cone(f"LavaCrack{index}", start, end, 0.035, 0.02, mats["Lava"], "Head" if index == 0 else "Body", 5)
    body = join_parts("ObsidianRam", rig)
    clips = actions(rig, ram_actions())
    export_asset("obsidian-ram-golem", "Obsidian Ram Golem", rig, body, clips,
                 [("Body", (0, 0, 1.7), (1.4, 1.75, 1.35)), ("Head", (0, -1.55, 1.85), (1.45, 0.85, 0.8))],
                 (0, 0, 1.6), 8.5, "Quake", 16)


def ram_actions():
    return [
        ("Idle", 60, True, [{"frame": 1}, {"frame": 16, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Head": (3, 0, 0), "Tail": (-4, 0, 0)}}, {"frame": 31}, {"frame": 46, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Head": (3, 0, 0), "Tail": (-4, 0, 0)}}, {"frame": 60}]),
        ("Walk", 32, True, [{"frame": 1, "rotations": {"Leg.FL": (-18, 0, 0), "Leg.BR": (-18, 0, 0), "Leg.FR": (18, 0, 0), "Leg.BL": (18, 0, 0)}}, {"frame": 9, "locations": {"Root": (0, 0, 0.05)}}, {"frame": 17, "rotations": {"Leg.FL": (18, 0, 0), "Leg.BR": (18, 0, 0), "Leg.FR": (-18, 0, 0), "Leg.BL": (-18, 0, 0)}}, {"frame": 25, "locations": {"Root": (0, 0, 0.05)}}, {"frame": 32, "rotations": {"Leg.FL": (-18, 0, 0), "Leg.BR": (-18, 0, 0), "Leg.FR": (18, 0, 0), "Leg.BL": (18, 0, 0)}}]),
        ("Charge", 30, False, [{"frame": 1}, {"frame": 8, "locations": {"Root": (0, 0.08, -0.12)}, "rotations": {"Head": (28, 0, 0), "Body": (8, 0, 0)}}, {"frame": 15, "locations": {"Root": (0, -0.18, 0.03)}, "rotations": {"Head": (18, 0, 0), "Leg.FL": (-24, 0, 0), "Leg.FR": (24, 0, 0)}}, {"frame": 23, "locations": {"Root": (0, -0.12, 0.08)}, "rotations": {"Head": (12, 0, 0), "Leg.FL": (24, 0, 0), "Leg.FR": (-24, 0, 0)}}, {"frame": 30}]),
        ("Quake", 36, False, [{"frame": 1}, {"frame": 12, "locations": {"Root": (0, 0.08, 0.18)}, "rotations": {"Body": (-18, 0, 0), "Head": (-20, 0, 0)}}, {"frame": 17, "locations": {"Root": (0, -0.08, -0.18)}, "rotations": {"Body": (24, 0, 0), "Head": (34, 0, 0)}}, {"frame": 25, "locations": {"Root": (0, 0, 0.03)}, "rotations": {"Body": (-5, 0, 0)}}, {"frame": 36}]),
        ("ArmorShatter", 42, False, [{"frame": 1}, {"frame": 10, "scales": {"Body": (0.94, 0.94, 0.94)}, "rotations": {"Head": (-12, 0, 0)}}, {"frame": 17, "scales": {"Body": (1.08, 1.08, 1.08)}, "rotations": {"Body": (0, 0, 10), "Horn.L": (0, -12, 0), "Horn.R": (0, 12, 0)}}, {"frame": 23, "rotations": {"Body": (0, 0, -8)}}, {"frame": 31, "rotations": {"Body": (0, 0, 4)}}, {"frame": 42}]),
        ("Stagger", 26, False, [{"frame": 1}, {"frame": 5, "locations": {"Root": (0, 0.16, -0.08)}, "rotations": {"Body": (-15, 0, 12), "Head": (20, 0, -10)}}, {"frame": 13, "rotations": {"Body": (7, 0, -6)}}, {"frame": 26}]),
        ("Death", 70, False, [{"frame": 1}, {"frame": 18, "locations": {"Root": (0.15, 0.1, -0.35)}, "rotations": {"Root": (0, 35, 32), "Head": (26, 0, 10)}}, {"frame": 42, "locations": {"Root": (0.25, 0.1, -1.15)}, "rotations": {"Root": (0, 58, 82), "Head": (42, 0, 18), "Leg.FL": (30, 0, 0), "Leg.BL": (-24, 0, 0)}}, {"frame": 70, "locations": {"Root": (0.25, 0.1, -1.2)}, "rotations": {"Root": (0, 58, 82), "Head": (44, 0, 18), "Leg.FL": (30, 0, 0), "Leg.BL": (-24, 0, 0)}}]),
    ]


def build_cinderback():
    reset_scene()
    mats = prepare_materials({
        "Charcoal": {"color": (0.035, 0.03, 0.03, 1), "metallic": 0.18, "roughness": 0.55},
        "Plate": {"color": (0.16, 0.10, 0.075, 1), "metallic": 0.2, "roughness": 0.48},
        "Underbody": {"color": (0.18, 0.018, 0.01, 1), "roughness": 0.66},
        "Lava": {"color": (1, 0.07, 0.002, 1), "roughness": 0.18, "emission": (1, 0.02, 0, 1), "strength": 10},
        "Claw": {"color": (0.55, 0.32, 0.15, 1), "roughness": 0.4},
    })
    specs = [
        ("Root", (0, 0, 0), (0, 0, 0.45), None), ("Body", (0, 0, 1.0), (0, 0, 1.95), "Root"),
        ("Head", (0, -1.0, 1.25), (0, -1.9, 1.18), "Body"), ("Tail.1", (0, 1.05, 1.2), (0, 1.8, 1.05), "Body"),
        ("Tail.2", (0, 1.75, 1.05), (0, 2.45, 0.8), "Tail.1"),
        ("Leg.FL", (-0.65, -0.75, 0.95), (-0.82, -0.82, 0.25), "Body"), ("Leg.FR", (0.65, -0.75, 0.95), (0.82, -0.82, 0.25), "Body"),
        ("Leg.BL", (-0.65, 0.65, 0.95), (-0.82, 0.7, 0.25), "Body"), ("Leg.BR", (0.65, 0.65, 0.95), (0.82, 0.7, 0.25), "Body"),
    ]
    rig = armature("Cinderback", specs)
    ellipsoid("Underbody", (0, 0, 1.12), (1.05, 1.62, 0.65), mats["Underbody"], "Body", 2)
    ellipsoid("Head", (0, -1.42, 1.2), (0.68, 0.78, 0.54), mats["Plate"], "Head", 2)
    cone("Snout", (0, -1.58, 1.2), (0, -2.05, 1.05), 0.4, 0.18, mats["Charcoal"], "Head")
    ellipsoid("Eye.L", (-0.42, -1.72, 1.37), (0.07, 0.05, 0.07), mats["Lava"], "Head")
    ellipsoid("Eye.R", (0.42, -1.72, 1.37), (0.07, 0.05, 0.07), mats["Lava"], "Head")
    for index, y in enumerate((-1.02, -0.58, -0.12, 0.34, 0.78, 1.16)):
        width = 0.93 - abs(index - 2.5) * 0.055
        plate(f"ShellPlate{index}", (0, y, 1.73 + math.sin(index / 5 * math.pi) * 0.18), (width, 0.53, 0.28), (math.radians(82), 0, 0), mats["Plate"], "Body")
        cone(f"ShellSpike{index}", (0, y, 1.9), (0, y, 2.35 + math.sin(index / 5 * math.pi) * 0.15), 0.13, 0, mats["Charcoal"], "Body", 6)
    for side, suffix in [(-1, "L"), (1, "R")]:
        for front, y, bone in [(True, -0.68, f"Leg.F{suffix}"), (False, 0.68, f"Leg.B{suffix}")]:
            x = side * 0.7
            ellipsoid(f"LegMass{suffix}{front}", (x, y, 0.78), (0.38, 0.48, 0.42), mats["Charcoal"], bone)
            cone(f"Leg{suffix}{front}", (x, y, 0.72), (side * 0.92, y - 0.12, 0.22), 0.22, 0.14, mats["Underbody"], bone)
            for toe in (-0.12, 0, 0.12):
                cone(f"Toe{suffix}{front}{toe}", (side * 0.92 + toe, y - 0.22, 0.2), (side * 0.95 + toe, y - 0.58, 0.11), 0.05, 0, mats["Claw"], bone, 5)
    cone("Tail1", (0, 1.0, 1.12), (0, 1.85, 0.88), 0.42, 0.27, mats["Plate"], "Tail.1")
    cone("Tail2", (0, 1.78, 0.88), (0, 2.48, 0.68), 0.29, 0.06, mats["Charcoal"], "Tail.2")
    for index, (x, y, z) in enumerate([(-0.48, -0.82, 1.56), (0.45, -0.28, 1.68), (-0.42, 0.28, 1.72), (0.38, 0.8, 1.55)]):
        cone(f"LavaCrack{index}", (x, y, z), (x * 0.65, y + 0.18, z - 0.32), 0.035, 0.018, mats["Lava"], "Body", 5)
    body = join_parts("Cinderback", rig)
    clips = actions(rig, cinderback_actions())
    export_asset("cinderback-ricochet", "Cinderback Ricochet", rig, body, clips,
                 [("Body", (0, 0, 1.05), (1.25, 1.85, 0.95)), ("Roll", (0, 0, 1.2), (1.45, 1.45, 1.45))],
                 (0, 0, 1.15), 8.3, "ArmorSlam", 15)


def cinderback_actions():
    return [
        ("Idle", 60, True, [{"frame": 1}, {"frame": 16, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Head": (3, 0, 0), "Tail.2": (-5, 0, 0)}}, {"frame": 31}, {"frame": 46, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Head": (3, 0, 0), "Tail.2": (-5, 0, 0)}}, {"frame": 60}]),
        ("Walk", 32, True, [{"frame": 1, "rotations": {"Leg.FL": (-14, 0, 0), "Leg.BR": (-14, 0, 0), "Leg.FR": (14, 0, 0), "Leg.BL": (14, 0, 0)}}, {"frame": 9, "locations": {"Root": (0, 0, 0.04)}}, {"frame": 17, "rotations": {"Leg.FL": (14, 0, 0), "Leg.BR": (14, 0, 0), "Leg.FR": (-14, 0, 0), "Leg.BL": (-14, 0, 0)}}, {"frame": 25, "locations": {"Root": (0, 0, 0.04)}}, {"frame": 32, "rotations": {"Leg.FL": (-14, 0, 0), "Leg.BR": (-14, 0, 0), "Leg.FR": (14, 0, 0), "Leg.BL": (14, 0, 0)}}]),
        ("Curl", 28, False, [{"frame": 1}, {"frame": 9, "locations": {"Root": (0, 0, 0.12)}, "rotations": {"Head": (-42, 0, 0), "Tail.1": (50, 0, 0), "Tail.2": (65, 0, 0), "Leg.FL": (45, 0, 0), "Leg.FR": (45, 0, 0), "Leg.BL": (-45, 0, 0), "Leg.BR": (-45, 0, 0)}}, {"frame": 17, "scales": {"Body": (1.02, 1.02, 1.02)}, "rotations": {"Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0), "Leg.FL": (58, 0, 0), "Leg.FR": (58, 0, 0), "Leg.BL": (-58, 0, 0), "Leg.BR": (-58, 0, 0)}}, {"frame": 28, "rotations": {"Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0), "Leg.FL": (58, 0, 0), "Leg.FR": (58, 0, 0), "Leg.BL": (-58, 0, 0), "Leg.BR": (-58, 0, 0)}}]),
        ("Ricochet", 30, True, [{"frame": 1, "rotations": {"Root": (0, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}, {"frame": 8, "rotations": {"Root": (90, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}, {"frame": 16, "rotations": {"Root": (180, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}, {"frame": 23, "rotations": {"Root": (270, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}, {"frame": 30, "rotations": {"Root": (360, 0, 0), "Head": (-55, 0, 0), "Tail.1": (70, 0, 0), "Tail.2": (80, 0, 0)}}]),
        ("ArmorSlam", 38, False, [{"frame": 1}, {"frame": 11, "locations": {"Root": (0, 0.1, 0.22)}, "rotations": {"Body": (-22, 0, 0), "Head": (-18, 0, 0)}}, {"frame": 16, "locations": {"Root": (0, -0.08, -0.16)}, "rotations": {"Body": (28, 0, 0), "Head": (34, 0, 0)}}, {"frame": 24, "rotations": {"Body": (-8, 0, 0)}}, {"frame": 38}]),
        ("Recover", 24, False, [{"frame": 1, "locations": {"Root": (0, 0, -0.12)}, "rotations": {"Body": (18, 0, 0)}}, {"frame": 8, "rotations": {"Body": (-5, 0, 0)}}, {"frame": 16, "rotations": {"Body": (2, 0, 0)}}, {"frame": 24}]),
        ("Stagger", 26, False, [{"frame": 1}, {"frame": 5, "locations": {"Root": (0, 0.14, -0.08)}, "rotations": {"Body": (-12, 0, 11), "Head": (22, 0, -10)}}, {"frame": 13, "rotations": {"Body": (6, 0, -5)}}, {"frame": 26}]),
        ("Death", 66, False, [{"frame": 1}, {"frame": 18, "locations": {"Root": (0.1, 0.1, -0.25)}, "rotations": {"Root": (0, 28, 35), "Head": (24, 0, 0)}}, {"frame": 42, "locations": {"Root": (0.18, 0.1, -0.8)}, "rotations": {"Root": (0, 50, 78), "Head": (38, 0, 0), "Tail.1": (-25, 0, 0)}}, {"frame": 66, "locations": {"Root": (0.18, 0.1, -0.84)}, "rotations": {"Root": (0, 50, 78), "Head": (40, 0, 0), "Tail.1": (-28, 0, 0)}}]),
    ]


def build_scorpion():
    reset_scene()
    mats = prepare_materials({
        "Obsidian": {"color": (0.025, 0.025, 0.03, 1), "metallic": 0.2, "roughness": 0.5},
        "Ochre": {"color": (0.42, 0.19, 0.045, 1), "metallic": 0.16, "roughness": 0.44},
        "Gold": {"color": (0.85, 0.38, 0.035, 1), "metallic": 0.3, "roughness": 0.28},
        "Glass": {"color": (1, 0.38, 0.02, 1), "roughness": 0.12, "emission": (1, 0.16, 0.01, 1), "strength": 4},
        "Bone": {"color": (0.58, 0.38, 0.17, 1), "roughness": 0.38},
    })
    specs = [
        ("Root", (0, 0, 0), (0, 0, 0.4), None), ("Body", (0, 0, 0.85), (0, 0, 1.55), "Root"),
        ("Head", (0, -0.65, 1.0), (0, -1.35, 0.92), "Body"), ("Claw.L", (-0.55, -0.8, 1.0), (-1.45, -1.6, 0.9), "Body"),
        ("Claw.R", (0.55, -0.8, 1.0), (1.45, -1.6, 0.9), "Body"), ("LegFront.L", (-0.5, -0.25, 0.8), (-1.4, -0.55, 0.25), "Body"),
        ("LegFront.R", (0.5, -0.25, 0.8), (1.4, -0.55, 0.25), "Body"), ("LegBack.L", (-0.5, 0.45, 0.8), (-1.4, 0.75, 0.25), "Body"),
        ("LegBack.R", (0.5, 0.45, 0.8), (1.4, 0.75, 0.25), "Body"),
        ("Tail.1", (0, 0.65, 1.0), (0, 1.35, 1.35), "Body"), ("Tail.2", (0, 1.3, 1.35), (0, 1.75, 2.0), "Tail.1"),
        ("Tail.3", (0, 1.7, 2.0), (0, 1.5, 2.68), "Tail.2"), ("Tail.4", (0, 1.5, 2.68), (0, 0.82, 3.0), "Tail.3"),
        ("Stinger", (0, 0.82, 3.0), (0, 0.18, 2.52), "Tail.4"),
    ]
    rig = armature("SandglassScorpion", specs)
    ellipsoid("Abdomen", (0, 0.28, 1.0), (0.92, 1.12, 0.54), mats["Obsidian"], "Body", 2)
    ellipsoid("Torso", (0, -0.55, 0.98), (0.76, 0.7, 0.48), mats["Ochre"], "Body", 2)
    ellipsoid("Head", (0, -1.12, 0.9), (0.52, 0.48, 0.36), mats["Obsidian"], "Head", 1)
    ellipsoid("Hourglass", (0, 0.15, 1.48), (0.42, 0.55, 0.24), mats["Glass"], "Body", 2)
    ellipsoid("HourglassWaist", (0, 0.15, 1.5), (0.44, 0.13, 0.26), mats["Obsidian"], "Body", 1)
    for side, suffix in [(-1, "L"), (1, "R")]:
        ellipsoid(f"Eye{suffix}", (side * 0.31, -1.42, 1.02), (0.06, 0.04, 0.06), mats["Glass"], "Head")
        cone(f"ClawArm{suffix}", (side * 0.45, -0.65, 1.0), (side * 1.25, -1.3, 0.9), 0.22, 0.16, mats["Ochre"], f"Claw.{suffix}")
        ellipsoid(f"ClawBody{suffix}", (side * 1.55, -1.55, 0.9), (0.62, 0.75, 0.42), mats["Gold"], f"Claw.{suffix}", 1, rotation=(0, 0, math.radians(side * 8)))
        ellipsoid(f"ClawGlass{suffix}", (side * 1.55, -1.68, 0.95), (0.37, 0.42, 0.22), mats["Glass"], f"Claw.{suffix}")
        cone(f"ClawTipOuter{suffix}", (side * 1.62, -1.98, 0.92), (side * 1.9, -2.45, 0.8), 0.2, 0, mats["Bone"], f"Claw.{suffix}")
        cone(f"ClawTipInner{suffix}", (side * 1.42, -1.98, 0.88), (side * 1.25, -2.32, 0.76), 0.14, 0, mats["Bone"], f"Claw.{suffix}")
        for index, (y, z) in enumerate([(-0.65, 0.7), (-0.18, 0.62), (0.32, 0.62), (0.78, 0.68)]):
            bone = f"LegFront.{suffix}" if index < 2 else f"LegBack.{suffix}"
            start = (side * 0.52, y, z + 0.18)
            joint = (side * (1.08 + index * 0.08), y + (index - 1.5) * 0.12, z)
            end = (side * (1.45 + index * 0.09), y + (index - 1.5) * 0.24, 0.16)
            cone(f"Leg{suffix}{index}A", start, joint, 0.14, 0.1, mats["Ochre"], bone, 6)
            cone(f"Leg{suffix}{index}B", joint, end, 0.1, 0.035, mats["Obsidian"], bone, 6)
    tail_points = [(0, 0.62, 1.02), (0, 1.35, 1.36), (0, 1.75, 2.0), (0, 1.5, 2.68), (0, 0.82, 3.0), (0, 0.18, 2.52)]
    tail_bones = ["Tail.1", "Tail.2", "Tail.3", "Tail.4", "Stinger"]
    for index, bone in enumerate(tail_bones):
        cone(f"TailSegment{index}", tail_points[index], tail_points[index + 1], 0.32 - index * 0.045, 0.25 - index * 0.04, mats["Ochre"] if index < 4 else mats["Gold"], bone, 8)
        if index < 4:
            plate(f"TailPlate{index}", tail_points[index + 1], (0.38 - index * 0.03, 0.32, 0.2), (0, 0, 0), mats["Gold"], bone)
    cone("StingerTip", (0, 0.18, 2.52), (0, -0.15, 2.18), 0.18, 0, mats["Glass"], "Stinger", 7)
    body = join_parts("SandglassScorpion", rig)
    clips = actions(rig, scorpion_actions())
    export_asset("sandglass-scorpion", "Sandglass Scorpion", rig, body, clips,
                 [("Body", (0, -0.1, 0.85), (1.15, 1.45, 0.8)), ("Claws", (0, -1.55, 0.9), (2.15, 1.0, 0.65)), ("Tail", (0, 1.0, 2.0), (0.65, 1.4, 1.35))],
                 (0, -0.1, 1.3), 9.5, "Eruption", 16)


def scorpion_actions():
    return [
        ("Idle", 60, True, [{"frame": 1}, {"frame": 16, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Tail.3": (0, 0, 4), "Claw.L": (0, 0, -3), "Claw.R": (0, 0, 3)}}, {"frame": 31}, {"frame": 46, "locations": {"Root": (0, 0, 0.035)}, "rotations": {"Tail.3": (0, 0, -4), "Claw.L": (0, 0, 3), "Claw.R": (0, 0, -3)}}, {"frame": 60}]),
        ("Walk", 32, True, [{"frame": 1, "rotations": {"LegFront.L": (-16, 0, 5), "LegBack.R": (-16, 0, -5), "LegFront.R": (16, 0, -5), "LegBack.L": (16, 0, 5)}}, {"frame": 9, "locations": {"Root": (0, 0, 0.05)}}, {"frame": 17, "rotations": {"LegFront.L": (16, 0, -5), "LegBack.R": (16, 0, 5), "LegFront.R": (-16, 0, 5), "LegBack.L": (-16, 0, -5)}}, {"frame": 25, "locations": {"Root": (0, 0, 0.05)}}, {"frame": 32, "rotations": {"LegFront.L": (-16, 0, 5), "LegBack.R": (-16, 0, -5), "LegFront.R": (16, 0, -5), "LegBack.L": (16, 0, 5)}}]),
        ("ClawAttack", 28, False, [{"frame": 1}, {"frame": 9, "rotations": {"Claw.L": (-10, 18, 30), "Claw.R": (-10, -18, -30)}}, {"frame": 14, "locations": {"Root": (0, -0.08, 0)}, "rotations": {"Claw.L": (16, -10, -38), "Claw.R": (16, 10, 38), "Head": (-8, 0, 0)}}, {"frame": 21, "rotations": {"Claw.L": (5, 0, -12), "Claw.R": (5, 0, 12)}}, {"frame": 28}]),
        ("Burrow", 34, False, [{"frame": 1}, {"frame": 10, "locations": {"Root": (0, 0, -0.15)}, "rotations": {"Claw.L": (35, 0, 0), "Claw.R": (35, 0, 0), "Tail.1": (-20, 0, 0)}}, {"frame": 20, "locations": {"Root": (0, 0, -0.62)}, "scales": {"Body": (0.92, 0.92, 0.92)}, "rotations": {"Claw.L": (48, 0, 0), "Claw.R": (48, 0, 0), "Tail.1": (-32, 0, 0)}}, {"frame": 34, "locations": {"Root": (0, 0, -0.72)}, "rotations": {"Claw.L": (48, 0, 0), "Claw.R": (48, 0, 0), "Tail.1": (-32, 0, 0)}}]),
        ("Eruption", 38, False, [{"frame": 1}, {"frame": 11, "rotations": {"Tail.2": (-18, 0, 0), "Tail.3": (-22, 0, 0), "Tail.4": (24, 0, 0), "Stinger": (18, 0, 0)}}, {"frame": 16, "locations": {"Root": (0, -0.05, 0.1)}, "rotations": {"Tail.2": (22, 0, 0), "Tail.3": (34, 0, 0), "Tail.4": (-42, 0, 0), "Stinger": (-38, 0, 0)}}, {"frame": 24, "rotations": {"Tail.3": (10, 0, 0), "Tail.4": (-12, 0, 0)}}, {"frame": 38}]),
        ("Hourglass", 44, False, [{"frame": 1}, {"frame": 12, "locations": {"Root": (0, 0, 0.1)}, "rotations": {"Body": (-10, 0, 0), "Claw.L": (0, 0, -24), "Claw.R": (0, 0, 24), "Tail.3": (-18, 0, 0)}}, {"frame": 20, "scales": {"Body": (1.08, 1.08, 1.08)}, "rotations": {"Claw.L": (0, 0, 38), "Claw.R": (0, 0, -38), "Tail.3": (22, 0, 0)}}, {"frame": 30, "scales": {"Body": (0.96, 0.96, 0.96)}, "rotations": {"Claw.L": (0, 0, -16), "Claw.R": (0, 0, 16)}}, {"frame": 44}]),
        ("Stagger", 26, False, [{"frame": 1}, {"frame": 5, "locations": {"Root": (0, 0.12, -0.08)}, "rotations": {"Body": (-12, 0, 10), "Claw.L": (-15, 0, -18), "Claw.R": (-15, 0, 18), "Tail.2": (18, 0, 0)}}, {"frame": 13, "rotations": {"Body": (6, 0, -5)}}, {"frame": 26}]),
        ("Death", 70, False, [{"frame": 1}, {"frame": 18, "locations": {"Root": (0.1, 0.1, -0.3)}, "rotations": {"Root": (0, 30, 38), "Tail.2": (-28, 0, 0), "Claw.L": (25, 0, -30), "Claw.R": (-10, 0, 30)}}, {"frame": 44, "locations": {"Root": (0.18, 0.1, -0.78)}, "rotations": {"Root": (0, 52, 82), "Tail.2": (-48, 0, 0), "Tail.3": (-32, 0, 0), "Claw.L": (38, 0, -42), "Claw.R": (12, 0, 38)}}, {"frame": 70, "locations": {"Root": (0.18, 0.1, -0.82)}, "rotations": {"Root": (0, 52, 82), "Tail.2": (-50, 0, 0), "Tail.3": (-34, 0, 0), "Claw.L": (40, 0, -44), "Claw.R": (14, 0, 40)}}]),
    ]


def main():
    bpy.context.preferences.filepaths.save_version = 0
    OUT_ROOT.mkdir(parents=True, exist_ok=True)
    build_ram()
    build_cinderback()
    build_scorpion()


if __name__ == "__main__":
    main()
