diff --git a/package.json b/package.json index 02887d3..7a60a84 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "i-want-to-heal", "private": true, - "version": "0.1.7", + "version": "0.1.8", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", diff --git a/scripts/blender/build_replacement_creature_bosses.py b/scripts/blender/build_replacement_creature_bosses.py new file mode 100644 index 0000000..05c0a99 --- /dev/null +++ b/scripts/blender/build_replacement_creature_bosses.py @@ -0,0 +1,352 @@ +"""Build two original low-poly creature bosses as animated runtime GLBs. + +Replaces weak chicken and frog visuals while keeping stable boss IDs in game data. + +Run with: + /Applications/Blender.app/Contents/MacOS/Blender --background --factory-startup \ + --python scripts/blender/build_replacement_creature_bosses.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, +) + + +def add_metadata(asset_id: str, concept: str) -> None: + metadata_path = OUT_ROOT / asset_id / f"{asset_id}.asset.json" + metadata = json.loads(metadata_path.read_text()) + metadata["sourceConcept"] = concept + metadata["license"] = "Original project-owned asset" + metadata["runtime"]["forward"] = "-Y" + metadata["runtime"]["unit"] = "meters" + metadata_path.write_text(json.dumps(metadata, indent=2) + "\n") + + +def build_brassbeak_basilisk() -> None: + """Six-legged forge basilisk replacing Cluckhorn's chicken-cow model.""" + asset_id = "brassbeak-basilisk" + reset_scene() + mats = prepare_materials({ + "Scale": {"color": (0.035, 0.105, 0.12, 1), "metallic": 0.14, "roughness": 0.62}, + "Underbelly": {"color": (0.12, 0.19, 0.18, 1), "metallic": 0.05, "roughness": 0.72}, + "Copper": {"color": (0.45, 0.16, 0.055, 1), "metallic": 0.48, "roughness": 0.33}, + "Brass": {"color": (0.78, 0.48, 0.09, 1), "metallic": 0.62, "roughness": 0.25}, + "Blade": {"color": (0.50, 0.58, 0.55, 1), "metallic": 0.72, "roughness": 0.2}, + "Furnace": {"color": (0.02, 0.82, 0.72, 1), "roughness": 0.18, "emission": (0.01, 0.72, 0.64, 1), "strength": 5.5}, + }) + specs = [ + ("Root", (0, 0, 0), (0, 0, 0.45), None), + ("Body", (0, 0.06, 1.18), (0, 0.05, 2.05), "Root"), + ("Head", (0, -1.0, 1.48), (0, -1.82, 1.38), "Body"), + ("Jaw", (0, -1.42, 1.28), (0, -2.05, 1.12), "Head"), + ("Wing.L", (-0.62, -0.08, 1.72), (-1.55, -0.28, 1.5), "Body"), + ("Wing.R", (0.62, -0.08, 1.72), (1.55, -0.28, 1.5), "Body"), + ("Leg.FL", (-0.62, -0.72, 1.12), (-0.88, -0.84, 0.24), "Body"), + ("Leg.FR", (0.62, -0.72, 1.12), (0.88, -0.84, 0.24), "Body"), + ("Leg.ML", (-0.78, 0.03, 1.06), (-1.0, 0.02, 0.22), "Body"), + ("Leg.MR", (0.78, 0.03, 1.06), (1.0, 0.02, 0.22), "Body"), + ("Leg.BL", (-0.66, 0.76, 1.12), (-0.9, 0.88, 0.24), "Body"), + ("Leg.BR", (0.66, 0.76, 1.12), (0.9, 0.88, 0.24), "Body"), + ("Tail.1", (0, 1.0, 1.3), (0, 1.85, 1.12), "Body"), + ("Tail.2", (0, 1.8, 1.12), (0, 2.7, 0.92), "Tail.1"), + ] + rig = armature("BrassbeakBasilisk", specs) + + # Broad armored silhouette with glowing furnace seams. + ellipsoid("BasiliskBody", (0, 0.08, 1.36), (1.08, 1.43, 0.72), mats["Scale"], "Body", 2) + ellipsoid("FurnaceBelly", (0, -0.15, 1.08), (0.78, 1.08, 0.46), mats["Underbelly"], "Body", 2) + for index, y in enumerate((-0.7, -0.22, 0.28, 0.76)): + width = 0.82 + (0.12 if index in (1, 2) else 0) + plate( + f"BackPlate{index}", (0, y, 1.92 + 0.08 * math.sin(index)), + (width, 0.55, 0.27), (math.radians(84), 0, 0), + mats["Copper"] if index % 2 == 0 else mats["Brass"], "Body", + ) + cone( + f"ChimneySpine{index}", (0, y, 2.02), (0, y + 0.03, 2.52 - index * 0.04), + 0.13, 0, mats["Blade"], "Body", 5, + ) + for side in (-1, 1): + cone( + f"FurnaceSeam{side:+d}", (side * 0.58, -0.76, 1.34), (side * 0.72, 0.72, 1.34), + 0.035, 0.022, mats["Furnace"], "Body", 5, + ) + + # Hammerhead, brass beak, split jaw, and crown blades. + ellipsoid("HammerHead", (0, -1.28, 1.5), (0.78, 0.74, 0.56), mats["Copper"], "Head", 2) + ellipsoid("FaceMask", (0, -1.72, 1.5), (0.58, 0.34, 0.42), mats["Brass"], "Head", 1) + cone("UpperBeak", (0, -1.68, 1.51), (0, -2.58, 1.34), 0.42, 0.035, mats["Brass"], "Head", 6) + cone("LowerBeak", (0, -1.64, 1.3), (0, -2.32, 1.18), 0.3, 0.025, mats["Blade"], "Jaw", 6) + for side, suffix in ((-1, "L"), (1, "R")): + ellipsoid(f"Eye{suffix}", (side * 0.43, -1.72, 1.66), (0.09, 0.055, 0.09), mats["Furnace"], "Head", 1) + cone( + f"BrowHorn{suffix}", (side * 0.42, -1.38, 1.78), (side * 0.88, -1.7, 2.03), + 0.13, 0, mats["Blade"], "Head", 5, + ) + for index, (x, z) in enumerate(((-0.34, 2.0), (0, 2.14), (0.34, 2.0))): + cone(f"CrownBlade{index}", (x, -1.2, 1.8), (x * 1.35, -1.15, z + 0.54), 0.12, 0, mats["Brass"], "Head", 5) + + # Blade-like vestigial wings make lateral cleaves readable from camera. + for side, suffix in ((-1, "L"), (1, "R")): + bone = f"Wing.{suffix}" + plate( + f"WingShield{suffix}", (side * 1.05, -0.08, 1.62), (0.7, 0.56, 0.13), + (math.radians(78), math.radians(side * 18), math.radians(side * 8)), mats["Copper"], bone, + ) + cone( + f"WingBlade{suffix}", (side * 0.72, -0.24, 1.7), (side * 1.95, -0.58, 1.42), + 0.19, 0.015, mats["Blade"], bone, 6, + ) + cone( + f"WingGlow{suffix}", (side * 0.88, -0.3, 1.69), (side * 1.7, -0.52, 1.5), + 0.05, 0.008, mats["Furnace"], bone, 5, + ) + + # Six short piston legs: stable, strange, easy to read while scuttling. + leg_rows = (("F", -0.72), ("M", 0.03), ("B", 0.76)) + for row_index, (row, y) in enumerate(leg_rows): + for side, suffix in ((-1, "L"), (1, "R")): + bone = f"Leg.{row}{suffix}" + hip_x = side * (0.64 if row != "M" else 0.78) + foot_x = side * (0.98 if row != "M" else 1.1) + ellipsoid(f"Hip{row}{suffix}", (hip_x, y, 1.05), (0.3, 0.34, 0.3), mats["Copper"], bone, 1) + cone(f"Shin{row}{suffix}", (hip_x, y, 1.0), (foot_x, y - 0.04, 0.28), 0.22, 0.14, mats["Scale"], bone, 6) + ellipsoid(f"Foot{row}{suffix}", (foot_x, y - 0.22, 0.2), (0.31, 0.48, 0.19), mats["Brass"], bone, 1) + for toe_index, toe_x in enumerate((-0.13, 0.13)): + cone( + f"Toe{row}{suffix}{toe_index}", (foot_x + toe_x, y - 0.42, 0.2), + (foot_x + toe_x * 1.4, y - 0.75, 0.1), 0.055, 0.004, mats["Blade"], bone, 5, + ) + + cone("TailCore1", (0, 0.95, 1.3), (0, 1.85, 1.1), 0.48, 0.3, mats["Scale"], "Tail.1", 7) + cone("TailCore2", (0, 1.78, 1.1), (0, 2.72, 0.88), 0.31, 0.07, mats["Copper"], "Tail.2", 7) + cone("TailBladeTop", (0, 2.45, 0.9), (0, 3.18, 1.45), 0.2, 0.015, mats["Blade"], "Tail.2", 5) + cone("TailBladeBottom", (0, 2.45, 0.9), (0, 3.15, 0.48), 0.18, 0.015, mats["Brass"], "Tail.2", 5) + ellipsoid("TailCoreGlow", (0, 2.54, 0.91), (0.14, 0.17, 0.14), mats["Furnace"], "Tail.2", 1) + + body = join_parts("BrassbeakBasilisk", rig) + clips = actions(rig, brassbeak_actions()) + export_asset( + asset_id, "Brassbeak Basilisk", rig, body, clips, + [ + ("Body", (0, 0, 1.25), (1.25, 1.55, 0.9)), + ("Head", (0, -1.65, 1.42), (0.95, 1.05, 0.75)), + ("Tail", (0, 2.08, 1.0), (0.55, 1.25, 0.78)), + ], + (0, 0, 1.25), 8.8, "FurnaceBurst", 20, + ) + add_metadata(asset_id, "Original six-legged forge basilisk designed for I Want to Heal") + + +def brassbeak_actions(): + return [ + ("Idle", 60, True, [ + {"frame": 1}, + {"frame": 15, "locations": {"Root": (0, 0, 0.04)}, "rotations": {"Head": (3, 0, -3), "Jaw": (7, 0, 0), "Tail.2": (0, 0, 7), "Wing.L": (0, 0, -4), "Wing.R": (0, 0, 4)}}, + {"frame": 30, "rotations": {"Head": (0, 0, 3), "Jaw": (0, 0, 0), "Tail.2": (0, 0, -7)}}, + {"frame": 45, "locations": {"Root": (0, 0, 0.04)}, "rotations": {"Head": (3, 0, -3), "Jaw": (7, 0, 0), "Tail.2": (0, 0, 7), "Wing.L": (0, 0, -4), "Wing.R": (0, 0, 4)}}, + {"frame": 60}, + ]), + ("Scuttle", 30, True, [ + {"frame": 1, "rotations": {"Leg.FL": (-18, 0, -5), "Leg.MR": (-18, 0, 4), "Leg.BL": (-18, 0, -4), "Leg.FR": (18, 0, 5), "Leg.ML": (18, 0, -4), "Leg.BR": (18, 0, 4), "Tail.2": (0, 0, -9)}}, + {"frame": 8, "locations": {"Root": (0, 0, 0.08)}, "rotations": {"Body": (-3, 0, 0)}}, + {"frame": 16, "rotations": {"Leg.FL": (18, 0, 5), "Leg.MR": (18, 0, -4), "Leg.BL": (18, 0, 4), "Leg.FR": (-18, 0, -5), "Leg.ML": (-18, 0, 4), "Leg.BR": (-18, 0, -4), "Tail.2": (0, 0, 9)}}, + {"frame": 23, "locations": {"Root": (0, 0, 0.08)}, "rotations": {"Body": (3, 0, 0)}}, + {"frame": 30, "rotations": {"Leg.FL": (-18, 0, -5), "Leg.MR": (-18, 0, 4), "Leg.BL": (-18, 0, -4), "Leg.FR": (18, 0, 5), "Leg.ML": (18, 0, -4), "Leg.BR": (18, 0, 4), "Tail.2": (0, 0, -9)}}, + ]), + ("BeakRend", 34, False, [ + {"frame": 1}, + {"frame": 9, "locations": {"Root": (0, 0.12, -0.05)}, "rotations": {"Body": (-9, 0, 0), "Head": (-24, 0, 0), "Jaw": (24, 0, 0), "Wing.L": (0, -16, -10), "Wing.R": (0, 16, 10)}}, + {"frame": 15, "locations": {"Root": (0, -0.2, 0.05)}, "rotations": {"Body": (15, 0, 0), "Head": (28, 0, 0), "Jaw": (-6, 0, 0)}}, + {"frame": 23, "rotations": {"Head": (-8, 0, 0), "Jaw": (12, 0, 0)}}, + {"frame": 34}, + ]), + ("FurnaceBurst", 46, False, [ + {"frame": 1}, + {"frame": 12, "locations": {"Root": (0, 0, 0.1)}, "scales": {"Body": (0.94, 0.94, 0.94)}, "rotations": {"Wing.L": (-18, 18, -22), "Wing.R": (-18, -18, 22), "Head": (-12, 0, 0), "Jaw": (18, 0, 0), "Tail.1": (-12, 0, 0)}}, + {"frame": 20, "locations": {"Root": (0, -0.08, 0.22)}, "scales": {"Body": (1.1, 1.1, 1.1)}, "rotations": {"Wing.L": (18, -62, -64), "Wing.R": (18, 62, 64), "Head": (20, 0, 0), "Jaw": (30, 0, 0), "Tail.1": (18, 0, 0), "Tail.2": (-22, 0, 0)}}, + {"frame": 30, "scales": {"Body": (0.97, 0.97, 0.97)}, "rotations": {"Wing.L": (4, -18, -20), "Wing.R": (4, 18, 20), "Jaw": (4, 0, 0), "Tail.2": (8, 0, 0)}}, + {"frame": 46}, + ]), + ("Stagger", 28, False, [ + {"frame": 1}, + {"frame": 6, "locations": {"Root": (0.12, 0.12, -0.09)}, "rotations": {"Body": (-14, 0, 13), "Head": (22, 0, -12), "Wing.L": (24, 0, -18), "Wing.R": (-8, 0, 12)}}, + {"frame": 15, "rotations": {"Body": (7, 0, -6), "Head": (-8, 0, 5)}}, + {"frame": 28}, + ]), + ("Death", 72, False, [ + {"frame": 1}, + {"frame": 20, "locations": {"Root": (0.15, 0.08, -0.25)}, "rotations": {"Root": (0, 25, 32), "Body": (18, 0, 12), "Head": (24, 0, -10), "Jaw": (20, 0, 0), "Wing.L": (32, 0, -26), "Wing.R": (16, 0, 20)}}, + {"frame": 46, "locations": {"Root": (0.28, 0.08, -0.78)}, "rotations": {"Root": (0, 52, 82), "Body": (30, 0, 20), "Head": (42, 0, -20), "Leg.FL": (30, 0, 0), "Leg.ML": (-24, 0, 0), "Leg.BL": (20, 0, 0), "Tail.1": (-32, 0, 0), "Tail.2": (-25, 0, 0)}}, + {"frame": 72, "locations": {"Root": (0.28, 0.08, -0.82)}, "rotations": {"Root": (0, 52, 82), "Body": (30, 0, 20), "Head": (44, 0, -20), "Leg.FL": (30, 0, 0), "Leg.ML": (-24, 0, 0), "Leg.BL": (20, 0, 0), "Tail.1": (-32, 0, 0), "Tail.2": (-25, 0, 0)}}, + ]), + ] + + +def build_bogbell_myconid() -> None: + """Bell-capped fungal brute replacing Mirelord's frog model.""" + asset_id = "bogbell-myconid" + reset_scene() + mats = prepare_materials({ + "Bark": {"color": (0.12, 0.18, 0.095, 1), "roughness": 0.88}, + "Root": {"color": (0.25, 0.31, 0.16, 1), "roughness": 0.8}, + "Cap": {"color": (0.29, 0.055, 0.31, 1), "roughness": 0.6}, + "CapEdge": {"color": (0.52, 0.15, 0.42, 1), "roughness": 0.52}, + "Gill": {"color": (0.62, 0.55, 0.31, 1), "roughness": 0.7}, + "Spore": {"color": (0.48, 1.0, 0.32, 1), "roughness": 0.16, "emission": (0.22, 0.92, 0.16, 1), "strength": 4.8}, + }) + specs = [ + ("Root", (0, 0, 0), (0, 0, 0.5), None), + ("Body", (0, 0, 1.15), (0, 0, 2.25), "Root"), + ("Cap", (0, -0.05, 2.18), (0, -0.05, 3.18), "Body"), + ("Arm.L", (-0.58, -0.15, 1.72), (-1.28, -0.62, 0.7), "Body"), + ("Arm.R", (0.58, -0.15, 1.72), (1.28, -0.62, 0.7), "Body"), + ("Leg.L", (-0.38, 0.08, 1.08), (-0.58, -0.1, 0.2), "Body"), + ("Leg.R", (0.38, 0.08, 1.08), (0.58, -0.1, 0.2), "Body"), + ("Tendril.L", (-0.42, 0.58, 1.45), (-1.05, 1.45, 0.82), "Body"), + ("Tendril.R", (0.42, 0.58, 1.45), (1.05, 1.45, 0.82), "Body"), + ] + rig = armature("BogbellMyconid", specs) + + # Gnarled trunk and hanging bell cap. + ellipsoid("Trunk", (0, 0.05, 1.48), (0.82, 0.68, 1.05), mats["Bark"], "Body", 2) + ellipsoid("ChestKnot", (0, -0.5, 1.62), (0.58, 0.28, 0.62), mats["Root"], "Body", 1) + cone("NeckStalk", (0, -0.02, 1.95), (0, -0.04, 2.65), 0.48, 0.36, mats["Gill"], "Cap", 8) + plate("BellCap", (0, -0.04, 2.78), (1.55, 1.38, 0.55), (0, 0, 0), mats["Cap"], "Cap", 9) + ellipsoid("CapCrown", (0, 0.02, 3.04), (1.25, 1.08, 0.42), mats["CapEdge"], "Cap", 2) + plate("GillBell", (0, -0.03, 2.58), (1.28, 1.12, 0.28), (0, 0, math.radians(180)), mats["Gill"], "Cap", 9) + for index, angle in enumerate(range(0, 360, 45)): + radians = math.radians(angle) + x, y = math.cos(radians) * 1.02, math.sin(radians) * 0.87 + cone( + f"CapHorn{index}", (x * 0.9, y * 0.9, 3.12), (x * 1.38, y * 1.35, 3.34 + 0.08 * (index % 2)), + 0.11, 0, mats["CapEdge"], "Cap", 5, + ) + for side, suffix in ((-1, "L"), (1, "R")): + ellipsoid(f"Eye{suffix}", (side * 0.29, -0.65, 2.18), (0.095, 0.055, 0.11), mats["Spore"], "Cap", 1) + cone( + f"FaceRoot{suffix}", (side * 0.25, -0.52, 2.03), (side * 0.42, -0.78, 1.72), + 0.07, 0.015, mats["Root"], "Cap", 5, + ) + ellipsoid("MouthHollow", (0, -0.68, 1.94), (0.22, 0.055, 0.13), mats["Cap"], "Cap", 1) + + # Root arms end in broad knuckles for readable pummel animation. + for side, suffix in ((-1, "L"), (1, "R")): + bone = f"Arm.{suffix}" + cone(f"UpperArm{suffix}", (side * 0.55, -0.12, 1.78), (side * 1.04, -0.45, 1.05), 0.3, 0.22, mats["Bark"], bone, 7) + cone(f"Forearm{suffix}", (side * 1.02, -0.44, 1.06), (side * 1.34, -0.84, 0.58), 0.24, 0.18, mats["Root"], bone, 7) + ellipsoid(f"Knuckle{suffix}", (side * 1.38, -0.91, 0.48), (0.43, 0.38, 0.32), mats["Bark"], bone, 1) + for finger in (-0.16, 0, 0.16): + cone( + f"Finger{suffix}{finger}", (side * 1.36 + finger, -1.02, 0.43), + (side * 1.46 + finger, -1.35, 0.22), 0.065, 0.012, mats["Root"], bone, 5, + ) + + for side, suffix in ((-1, "L"), (1, "R")): + bone = f"Leg.{suffix}" + ellipsoid(f"Hip{suffix}", (side * 0.4, 0.08, 1.0), (0.42, 0.46, 0.5), mats["Bark"], bone, 1) + cone(f"RootLeg{suffix}", (side * 0.4, 0.06, 0.95), (side * 0.62, -0.12, 0.25), 0.34, 0.22, mats["Root"], bone, 7) + for toe_index, toe_x in enumerate((-0.22, 0, 0.22)): + cone( + f"RootToe{suffix}{toe_index}", (side * 0.62 + toe_x, -0.2, 0.25), + (side * 0.72 + toe_x * 1.25, -0.78 - abs(toe_x), 0.08), 0.095, 0.015, mats["Bark"], bone, 6, + ) + + # Rear tendrils drag through mire. Spore sacs pulse during eruption. + for side, suffix in ((-1, "L"), (1, "R")): + bone = f"Tendril.{suffix}" + cone(f"TendrilBase{suffix}", (side * 0.4, 0.5, 1.38), (side * 0.78, 1.22, 0.82), 0.22, 0.12, mats["Root"], bone, 7) + cone(f"TendrilTip{suffix}", (side * 0.76, 1.18, 0.84), (side * 1.28, 1.85, 0.3), 0.13, 0.018, mats["Bark"], bone, 6) + ellipsoid(f"SporeSac{suffix}", (side * 0.82, 0.72, 1.25), (0.24, 0.31, 0.3), mats["Spore"], bone, 1) + for index, (x, y, z, size) in enumerate(((-0.5, 0.45, 1.88, 0.16), (0.48, 0.5, 1.72, 0.2), (-0.28, 0.62, 1.35, 0.13))): + ellipsoid(f"BodySpore{index}", (x, y, z), (size, size * 0.82, size * 1.1), mats["Spore"], "Body", 1) + + body = join_parts("BogbellMyconid", rig) + clips = actions(rig, bogbell_actions()) + export_asset( + asset_id, "Bogbell Myconid", rig, body, clips, + [ + ("Body", (0, 0, 1.45), (1.05, 0.95, 1.35)), + ("Cap", (0, 0, 2.82), (1.68, 1.48, 0.72)), + ("Roots", (0, 0.38, 0.62), (1.58, 1.75, 0.72)), + ], + (0, 0, 1.65), 8.5, "SporeEruption", 21, + ) + add_metadata(asset_id, "Original bell-capped fungal mire creature designed for I Want to Heal") + + +def bogbell_actions(): + return [ + ("Idle", 60, True, [ + {"frame": 1}, + {"frame": 15, "locations": {"Root": (0, 0, 0.04)}, "scales": {"Cap": (1.03, 1.03, 0.98)}, "rotations": {"Cap": (2, 0, -3), "Arm.L": (0, 0, -3), "Arm.R": (0, 0, 3), "Tendril.L": (0, 0, 7), "Tendril.R": (0, 0, -7)}}, + {"frame": 30, "scales": {"Cap": (0.98, 0.98, 1.03)}, "rotations": {"Cap": (-1, 0, 3), "Tendril.L": (0, 0, -7), "Tendril.R": (0, 0, 7)}}, + {"frame": 45, "locations": {"Root": (0, 0, 0.04)}, "scales": {"Cap": (1.03, 1.03, 0.98)}, "rotations": {"Cap": (2, 0, -3), "Arm.L": (0, 0, -3), "Arm.R": (0, 0, 3), "Tendril.L": (0, 0, 7), "Tendril.R": (0, 0, -7)}}, + {"frame": 60}, + ]), + ("BurrowRush", 32, True, [ + {"frame": 1, "locations": {"Root": (0, 0, -0.12)}, "rotations": {"Body": (10, 0, 0), "Arm.L": (26, 0, -12), "Arm.R": (26, 0, 12), "Leg.L": (-16, 0, 0), "Leg.R": (16, 0, 0), "Tendril.L": (-18, 0, -12), "Tendril.R": (-18, 0, 12)}}, + {"frame": 9, "locations": {"Root": (0, 0, -0.26)}, "rotations": {"Cap": (-9, 0, 0), "Leg.L": (16, 0, 0), "Leg.R": (-16, 0, 0), "Tendril.L": (14, 0, 10), "Tendril.R": (14, 0, -10)}}, + {"frame": 17, "locations": {"Root": (0, 0, -0.12)}, "rotations": {"Body": (10, 0, 0), "Arm.L": (26, 0, -12), "Arm.R": (26, 0, 12), "Leg.L": (-16, 0, 0), "Leg.R": (16, 0, 0), "Tendril.L": (-18, 0, -12), "Tendril.R": (-18, 0, 12)}}, + {"frame": 25, "locations": {"Root": (0, 0, -0.26)}, "rotations": {"Cap": (-9, 0, 0), "Leg.L": (16, 0, 0), "Leg.R": (-16, 0, 0), "Tendril.L": (14, 0, 10), "Tendril.R": (14, 0, -10)}}, + {"frame": 32, "locations": {"Root": (0, 0, -0.12)}, "rotations": {"Body": (10, 0, 0), "Arm.L": (26, 0, -12), "Arm.R": (26, 0, 12), "Leg.L": (-16, 0, 0), "Leg.R": (16, 0, 0), "Tendril.L": (-18, 0, -12), "Tendril.R": (-18, 0, 12)}}, + ]), + ("RootPummel", 36, False, [ + {"frame": 1}, + {"frame": 10, "locations": {"Root": (0, 0.1, 0.06)}, "rotations": {"Body": (-12, 0, 0), "Cap": (-8, 0, 0), "Arm.L": (-42, 0, -30), "Arm.R": (-42, 0, 30)}}, + {"frame": 17, "locations": {"Root": (0, -0.12, -0.14)}, "rotations": {"Body": (22, 0, 0), "Cap": (18, 0, 0), "Arm.L": (58, 0, 16), "Arm.R": (58, 0, -16)}}, + {"frame": 25, "rotations": {"Body": (-5, 0, 0), "Arm.L": (12, 0, -6), "Arm.R": (12, 0, 6)}}, + {"frame": 36}, + ]), + ("SporeEruption", 48, False, [ + {"frame": 1}, + {"frame": 13, "locations": {"Root": (0, 0, -0.12)}, "scales": {"Cap": (0.88, 0.88, 1.16)}, "rotations": {"Body": (-13, 0, 0), "Cap": (-15, 0, 0), "Arm.L": (-26, 0, -28), "Arm.R": (-26, 0, 28), "Tendril.L": (-28, 0, -24), "Tendril.R": (-28, 0, 24)}}, + {"frame": 21, "locations": {"Root": (0, -0.04, 0.24)}, "scales": {"Cap": (1.18, 1.18, 0.9), "Body": (1.08, 1.08, 1.08)}, "rotations": {"Body": (18, 0, 0), "Cap": (19, 0, 0), "Arm.L": (18, 0, 62), "Arm.R": (18, 0, -62), "Tendril.L": (32, 0, 48), "Tendril.R": (32, 0, -48)}}, + {"frame": 32, "scales": {"Cap": (0.97, 0.97, 1.04), "Body": (0.97, 0.97, 0.97)}, "rotations": {"Cap": (-5, 0, 0), "Arm.L": (4, 0, 12), "Arm.R": (4, 0, -12)}}, + {"frame": 48}, + ]), + ("Stagger", 28, False, [ + {"frame": 1}, + {"frame": 6, "locations": {"Root": (0.14, 0.1, -0.08)}, "rotations": {"Body": (-15, 0, 13), "Cap": (24, 0, -18), "Arm.L": (20, 0, -18), "Arm.R": (-8, 0, 12)}}, + {"frame": 15, "rotations": {"Body": (7, 0, -6), "Cap": (-8, 0, 7)}}, + {"frame": 28}, + ]), + ("Death", 74, False, [ + {"frame": 1}, + {"frame": 20, "locations": {"Root": (0.14, 0.1, -0.3)}, "rotations": {"Root": (0, 24, 30), "Body": (20, 0, 12), "Cap": (28, 0, -18), "Arm.L": (30, 0, -26), "Arm.R": (16, 0, 20), "Tendril.L": (-24, 0, -14), "Tendril.R": (-16, 0, 18)}}, + {"frame": 48, "locations": {"Root": (0.28, 0.1, -0.86)}, "rotations": {"Root": (0, 54, 84), "Body": (34, 0, 24), "Cap": (48, 0, -30), "Arm.L": (52, 0, -42), "Arm.R": (28, 0, 34), "Leg.L": (24, 0, 0), "Leg.R": (-18, 0, 0), "Tendril.L": (-40, 0, -24), "Tendril.R": (-34, 0, 26)}}, + {"frame": 74, "locations": {"Root": (0.28, 0.1, -0.9)}, "rotations": {"Root": (0, 54, 84), "Body": (34, 0, 24), "Cap": (50, 0, -30), "Arm.L": (52, 0, -42), "Arm.R": (28, 0, 34), "Leg.L": (24, 0, 0), "Leg.R": (-18, 0, 0), "Tendril.L": (-40, 0, -24), "Tendril.R": (-34, 0, 26)}}, + ]), + ] + + +def main() -> None: + bpy.context.preferences.filepaths.save_version = 0 + OUT_ROOT.mkdir(parents=True, exist_ok=True) + build_brassbeak_basilisk() + build_bogbell_myconid() + + +if __name__ == "__main__": + main() diff --git a/src/assets/game/models/original/bosses/bogbell-myconid/bogbell-myconid.glb b/src/assets/game/models/original/bosses/bogbell-myconid/bogbell-myconid.glb new file mode 100644 index 0000000..15ce8fc Binary files /dev/null and b/src/assets/game/models/original/bosses/bogbell-myconid/bogbell-myconid.glb differ diff --git a/src/assets/game/models/original/bosses/brassbeak-basilisk/brassbeak-basilisk.glb b/src/assets/game/models/original/bosses/brassbeak-basilisk/brassbeak-basilisk.glb new file mode 100644 index 0000000..8ff69fc Binary files /dev/null and b/src/assets/game/models/original/bosses/brassbeak-basilisk/brassbeak-basilisk.glb differ diff --git a/src/components/BottomScreen.tsx b/src/components/BottomScreen.tsx index cb73246..b29936c 100644 --- a/src/components/BottomScreen.tsx +++ b/src/components/BottomScreen.tsx @@ -84,6 +84,7 @@ function AbilityButton({ abilityId }: { abilityId: (typeof ABILITY_ORDER)[number const globalCooldownUntil = useGameStore((state) => state.globalCooldownUntil); const mana = useGameStore((state) => state.mana); const phase = useGameStore((state) => state.phase); + const healerAlive = useGameStore((state) => state.party.some((member) => member.id === "aelia" && member.hp > 0)); const selected = useGameStore((state) => state.party.find((member) => member.id === state.selectedMemberId)!); const activeCast = useGameStore((state) => state.activeCast); const castAbility = useGameStore((state) => state.castAbility); @@ -95,7 +96,7 @@ function AbilityButton({ abilityId }: { abilityId: (typeof ABILITY_ORDER)[number const globalRemaining = Math.max(0, globalCooldownUntil - time); const noDispel = abilityId === "purify" && selected.debuffs.length === 0; const invalidTarget = ability.targeting === "ally" && selected.hp <= 0; - const disabled = phase !== "combat" || activeCast !== null || remaining > 0 || globalRemaining > 0 || mana < manaCost || noDispel || invalidTarget; + const disabled = phase !== "combat" || !healerAlive || activeCast !== null || remaining > 0 || globalRemaining > 0 || mana < manaCost || noDispel || invalidTarget; const resourceCopy = `${manaCost ? `${manaCost} mana` : "free"}${castTime ? ` · ${castTime.toFixed(1)}s` : ""}`; return ( diff --git a/src/components/FrontEnd.tsx b/src/components/FrontEnd.tsx index bb0bd01..4f377a2 100644 --- a/src/components/FrontEnd.tsx +++ b/src/components/FrontEnd.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react"; import { buildCollections, MAX_HUNTER_NAME_LENGTH, MODE_COPY, normalizeHunterName } from "../frontend/data"; import { formatPlayTime, formatSaveTimestamp } from "../frontend/saveRepository"; +import { resolveSaveContinuation, saveVersionsMatch } from "../frontend/saveContinuation"; import { useActiveHunter, useFrontendStore } from "../frontend/store"; import type { GameModeId, SaveSlotId, SaveSlotState } from "../frontend/types"; import { useMenuController, type MenuAction } from "../input/useMenuController"; @@ -80,11 +81,46 @@ function ControllerLegend({ back = false }: { back?: boolean }) { return
Your save always lives on this device. Sign in only when you want a second copy for PC ↔ AYN Thor handoff.
+Continue to your saved hunters. Sign in when you want online copies for PC ↔ AYN Thor handoff.