From fb41147197e12e6a045b70e5a810c9a62d06dfc2 Mon Sep 17 00:00:00 2001 From: Warren H Date: Mon, 13 Jul 2026 20:47:56 -0400 Subject: [PATCH] Release v0.1.10 2026-07-13 --- package.json | 2 +- src/components/GameScene.tsx | 11 ++++++++--- src/components/boss/bossBurrowVisuals.test.ts | 19 +++++++++++++++++++ src/components/boss/bossBurrowVisuals.ts | 18 ++++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 src/components/boss/bossBurrowVisuals.test.ts create mode 100644 src/components/boss/bossBurrowVisuals.ts diff --git a/package.json b/package.json index 7f76f9a..9c4cddf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "i-want-to-heal", "private": true, - "version": "0.1.9", + "version": "0.1.10", "type": "module", "scripts": { "predev": "pnpm assets:sync-basis-transcoder", diff --git a/src/components/GameScene.tsx b/src/components/GameScene.tsx index be71b9e..d019053 100644 --- a/src/components/GameScene.tsx +++ b/src/components/GameScene.tsx @@ -29,6 +29,7 @@ import { useGameStore } from "../game/store"; import type { BossId, MemberId, PulseKind } from "../game/types"; import { BossRoom } from "./BossRoom"; import { BossMechanicIndicators } from "./boss/BossMechanicIndicators"; +import { bossBurrowPositionY, bossIsBurrowing } from "./boss/bossBurrowVisuals"; import { bossCanTrackTarget, bossDeathOpacity } from "./boss/bossDeathVisuals"; import { GameAssetProvider, LEGACY_GAME_ASSETS_FORCED, selectedGameAssetUrl, useGameGLTF } from "./GameAssetProvider"; @@ -130,8 +131,11 @@ function createBossRenderModel(source: THREE.Object3D) { ? object.material.map(cloneMaterial) : cloneMaterial(object.material); }); + model.updateMatrixWorld(true); + const modelTopY = new THREE.Box3().setFromObject(model).max.y; return { model, + modelTopY, fadeMaterials: [...materialClones.values()].map((material): BossFadeMaterial => ({ material, baseOpacity: material.opacity, @@ -834,9 +838,10 @@ function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex const light = useRef(null); const assetUrl = selectedGameAssetUrl(config.url, config.optimizedUrl ?? config.url); const gltf = useGameGLTF(assetUrl); - const { model, fadeMaterials } = useMemo(() => createBossRenderModel(gltf.scene), [gltf.scene]); + const { model, modelTopY, fadeMaterials } = useMemo(() => createBossRenderModel(gltf.scene), [gltf.scene]); const { actions } = useAnimations(gltf.animations, model); const targetPosition = useMemo(() => new THREE.Vector3(), []); + const burrowPositionY = bossBurrowPositionY(modelTopY, config.scale); useEffect(() => { return () => { for (const entry of fadeMaterials) entry.material.dispose(); }; @@ -873,9 +878,9 @@ function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex if (!current) return; const motion = current.motion; const airborne = archetype === "sky-sweeper" && motion.mode === "skyfall"; - const burrowed = archetype === "burrower" && motion.activeMechanicId === "burrow-rush" && motion.mode === "charging"; + const burrowing = archetype === "burrower" && bossIsBurrowing(motion.activeMechanicId, motion.mode); const floatingHeight = config.floating ? 0.2 : 0.03; - targetPosition.set(motion.position[0], airborne ? 3.2 : burrowed ? -0.58 : floatingHeight, motion.position[1]); + targetPosition.set(motion.position[0], airborne ? 3.2 : burrowing ? burrowPositionY : floatingHeight, motion.position[1]); group.current.position.lerp(targetPosition, 1 - Math.pow(0.00001, delta)); if (!bossCanTrackTarget(current.boss.hp)) return; diff --git a/src/components/boss/bossBurrowVisuals.test.ts b/src/components/boss/bossBurrowVisuals.test.ts new file mode 100644 index 0000000..78cfa59 --- /dev/null +++ b/src/components/boss/bossBurrowVisuals.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { BOSS_BURROW_CLEARANCE, bossBurrowPositionY, bossIsBurrowing } from "./bossBurrowVisuals"; + +describe("boss burrow visuals", () => { + it("places the scaled top of the model below the arena floor", () => { + const modelTopY = 2.8371768724243784; + const modelScale = 0.82; + const positionY = bossBurrowPositionY(modelTopY, modelScale); + + expect(positionY + modelTopY * modelScale).toBeCloseTo(-BOSS_BURROW_CLEARANCE, 6); + }); + + it("starts sinking during the warning and remains submerged through the charge", () => { + expect(bossIsBurrowing("burrow-rush", "telegraph")).toBe(true); + expect(bossIsBurrowing("burrow-rush", "charging")).toBe(true); + expect(bossIsBurrowing("burrow-rush", "holding")).toBe(false); + expect(bossIsBurrowing("bull-charge", "charging")).toBe(false); + }); +}); diff --git a/src/components/boss/bossBurrowVisuals.ts b/src/components/boss/bossBurrowVisuals.ts new file mode 100644 index 0000000..023f19a --- /dev/null +++ b/src/components/boss/bossBurrowVisuals.ts @@ -0,0 +1,18 @@ +import type { BossMechanicId, BossMotionMode } from "../../game/types"; + +export const BOSS_BURROW_CLEARANCE = 0.58; + +export function bossBurrowPositionY( + modelTopY: number, + modelScale: number, + clearance = BOSS_BURROW_CLEARANCE, +): number { + return -(Math.max(0, modelTopY) * Math.max(0, modelScale) + Math.max(0, clearance)); +} + +export function bossIsBurrowing( + activeMechanicId: BossMechanicId | null, + mode: BossMotionMode, +): boolean { + return activeMechanicId === "burrow-rush" && (mode === "telegraph" || mode === "charging"); +}