Release v0.1.5 2026-07-12
This commit is contained in:
@@ -6,6 +6,7 @@ import { getControllerMovement } from "../input/controller";
|
||||
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
import { ARENA_CENTER, clampToArena } from "../game/arena";
|
||||
import { BOSS_ARCHETYPE_BY_ID } from "../game/bossCatalog";
|
||||
import { bossAnimationCue } from "../game/bosses/mechanicPool";
|
||||
import {
|
||||
isActorAnimationOneShot,
|
||||
shouldStartActorAnimation,
|
||||
@@ -598,7 +599,9 @@ function BossFallback({ bossIndex }: { bossIndex: number }) {
|
||||
|
||||
function BullBoss({ bossIndex }: { bossIndex: number }) {
|
||||
const phase = useGameStore((state) => state.phase);
|
||||
const motionMode = useGameStore((state) => (bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion)?.mode ?? "holding");
|
||||
const motion = useGameStore((state) => bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion);
|
||||
const motionMode = motion?.mode ?? "holding";
|
||||
const animationCue = motion ? bossAnimationCue(motion) : "idle";
|
||||
const bossHp = useGameStore((state) => (bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss)?.hp ?? 0);
|
||||
const defeated = bossHp <= 0;
|
||||
const group = useRef<THREE.Group>(null);
|
||||
@@ -618,15 +621,13 @@ function BullBoss({ bossIndex }: { bossIndex: number }) {
|
||||
|
||||
const clipName = phase === "victory" || defeated
|
||||
? "Death"
|
||||
: motionMode === "telegraph"
|
||||
: animationCue === "attack"
|
||||
? "Idle_Headlow"
|
||||
: motionMode === "pouncing"
|
||||
: animationCue === "special"
|
||||
? "Gallop_Jump"
|
||||
: motionMode === "charging" || motionMode === "returning"
|
||||
: animationCue === "move"
|
||||
? "Gallop"
|
||||
: motionMode === "stacking"
|
||||
? "Idle_Headlow"
|
||||
: "Idle";
|
||||
: "Idle";
|
||||
|
||||
useEffect(() => {
|
||||
const next = actions[clipName];
|
||||
@@ -661,9 +662,6 @@ function BullBoss({ bossIndex }: { bossIndex: number }) {
|
||||
if (motion.mode === "telegraph" || motion.mode === "charging" || motion.mode === "pouncing") {
|
||||
facingX = motion.chargeEnd[0] - motion.chargeStart[0];
|
||||
facingZ = motion.chargeEnd[1] - motion.chargeStart[1];
|
||||
} else if (motion.mode === "returning") {
|
||||
facingX = ARENA_CENTER[0] + motion.formationOffsetX - motion.position[0];
|
||||
facingZ = ARENA_CENTER[1] - motion.position[1];
|
||||
}
|
||||
if (Math.hypot(facingX, facingZ) > 0.01) {
|
||||
const targetAngle = Math.atan2(facingX, facingZ);
|
||||
@@ -745,7 +743,7 @@ const ALTERNATE_BOSS_CONFIG: Record<AlternateBossKind, AlternateBossConfig> = {
|
||||
},
|
||||
"crystal-bat-matriarch": {
|
||||
url: CRYSTAL_BAT_MATRIARCH_URL,
|
||||
scale: 1.04,
|
||||
scale: 0.828,
|
||||
idle: "Idle",
|
||||
move: "Swoop",
|
||||
attack: "SonicPulse",
|
||||
@@ -820,84 +818,17 @@ const ALTERNATE_BOSS_CONFIG: Record<AlternateBossKind, AlternateBossConfig> = {
|
||||
},
|
||||
};
|
||||
|
||||
const PROTOTYPE_MOVE_MODES = [
|
||||
"skyfall",
|
||||
"mantis_sidestep",
|
||||
"ram_charging",
|
||||
"cinderback_ricochet",
|
||||
"charging",
|
||||
"returning",
|
||||
"sandglass_burrowing",
|
||||
"crab_scuttling",
|
||||
] as const;
|
||||
|
||||
const PROTOTYPE_ATTACK_MODES = [
|
||||
"tethering",
|
||||
"venom_cast",
|
||||
"breath_telegraph",
|
||||
"breath_sweeping",
|
||||
"mantis_line_telegraph",
|
||||
"mantis_cross_telegraph",
|
||||
"ram_charge_telegraph",
|
||||
"ram_quake",
|
||||
"ram_shatter",
|
||||
"cinderback_curl",
|
||||
"cinderback_slam",
|
||||
"ghost_soul_cross",
|
||||
"ghost_soul_cross_followup",
|
||||
"ghost_haunting",
|
||||
"golem_shockwave",
|
||||
"golem_crownfall",
|
||||
"telegraph",
|
||||
"stacking",
|
||||
"pouncing",
|
||||
"sandglass_burrow_telegraph",
|
||||
"sandglass_eruption",
|
||||
"sandglass_hourglass",
|
||||
"crab_scuttle_telegraph",
|
||||
"crab_tidal_burst",
|
||||
] as const;
|
||||
|
||||
function alternateBossClip(kind: AlternateBossKind, motionMode: ReturnType<typeof useGameStore.getState>["bossMotion"]["mode"]) {
|
||||
function alternateBossClip(kind: AlternateBossKind, motion: ReturnType<typeof useGameStore.getState>["bossMotion"]) {
|
||||
const config = ALTERNATE_BOSS_CONFIG[kind];
|
||||
if (config.prototype) {
|
||||
if ((PROTOTYPE_MOVE_MODES as readonly string[]).includes(motionMode)) return config.move;
|
||||
if ((PROTOTYPE_ATTACK_MODES as readonly string[]).includes(motionMode)) return config.attack;
|
||||
return config.idle;
|
||||
}
|
||||
if (kind === "sandglass-scorpion") {
|
||||
if (motionMode === "sandglass_burrow_telegraph" || motionMode === "sandglass_burrowing") return config.move;
|
||||
if (motionMode === "sandglass_eruption") return config.attack;
|
||||
if (motionMode === "sandglass_hourglass") return config.special;
|
||||
if (motionMode === "sandglass_recover") return "Stagger";
|
||||
}
|
||||
if (kind === "cragclaw-crab") {
|
||||
if (motionMode === "crab_scuttling") return config.move;
|
||||
if (motionMode === "crab_scuttle_telegraph") return config.attack;
|
||||
if (motionMode === "crab_tidal_burst") return config.special;
|
||||
}
|
||||
if (kind === "mournveil-ghost") {
|
||||
if (motionMode === "ghost_soul_cross" || motionMode === "ghost_soul_cross_followup") return config.attack;
|
||||
if (motionMode === "ghost_haunting") return config.special;
|
||||
}
|
||||
if (kind === "crownshard-golem") {
|
||||
if (motionMode === "golem_shockwave") return config.attack;
|
||||
if (motionMode === "golem_crownfall") return config.special;
|
||||
}
|
||||
if (kind === "crystal-bat-matriarch") {
|
||||
if (motionMode === "golem_shockwave") return config.attack;
|
||||
if (motionMode === "golem_crownfall") return config.special;
|
||||
if (motionMode === "golem_recover") return "Stagger";
|
||||
}
|
||||
if (BOSS_ARCHETYPE_BY_ID[kind] === "web-caster" && (motionMode === "tethering" || motionMode === "venom_cast")) return config.attack;
|
||||
return config.idle;
|
||||
return config[bossAnimationCue(motion)];
|
||||
}
|
||||
|
||||
function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex: number }) {
|
||||
const config = ALTERNATE_BOSS_CONFIG[kind];
|
||||
const archetype = BOSS_ARCHETYPE_BY_ID[kind];
|
||||
const phase = useGameStore((state) => state.phase);
|
||||
const motionMode = useGameStore((state) => (bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion)?.mode ?? "holding");
|
||||
const motion = useGameStore((state) => bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion);
|
||||
const motionMode = motion?.mode ?? "holding";
|
||||
const bossHp = useGameStore((state) => (bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss)?.hp ?? 0);
|
||||
const defeated = bossHp <= 0;
|
||||
const group = useRef<THREE.Group>(null);
|
||||
@@ -915,7 +846,7 @@ function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex
|
||||
});
|
||||
}, [kind, model]);
|
||||
|
||||
const clipName = phase === "victory" || defeated ? config.death : alternateBossClip(kind, motionMode);
|
||||
const clipName = phase === "victory" || defeated ? config.death : alternateBossClip(kind, motion ?? useGameStore.getState().bossMotion);
|
||||
|
||||
useEffect(() => {
|
||||
const next = actions[clipName];
|
||||
@@ -944,7 +875,7 @@ 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.mode === "sandglass_burrowing";
|
||||
const burrowed = archetype === "burrower" && motion.activeMechanicId === "burrow-rush" && motion.mode === "charging";
|
||||
const floatingHeight = config.floating ? 0.2 : 0.03;
|
||||
targetPosition.set(motion.position[0], airborne ? 3.2 : burrowed ? -0.58 : floatingHeight, motion.position[1]);
|
||||
group.current.position.lerp(targetPosition, 1 - Math.pow(0.00001, delta));
|
||||
@@ -955,14 +886,13 @@ function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex
|
||||
);
|
||||
if (archetype === "sky-sweeper" && (motion.mode === "breath_telegraph" || motion.mode === "breath_sweeping")) {
|
||||
targetAngle = motion.breathAngle;
|
||||
} else if (archetype === "duelist" && (
|
||||
motion.mode === "mantis_sidestep"
|
||||
|| motion.mode === "mantis_line_telegraph"
|
||||
} else if (
|
||||
motion.mode === "mantis_line_telegraph"
|
||||
|| motion.mode === "mantis_cross_telegraph"
|
||||
)) {
|
||||
) {
|
||||
const target = state.partyPositions[motion.chargeTargetId];
|
||||
targetAngle = Math.atan2(target[0] - motion.position[0], target[1] - motion.position[1]);
|
||||
} else if (["ram_charge_telegraph", "ram_charging", "cinderback_curl", "cinderback_ricochet", "sandglass_burrow_telegraph", "sandglass_burrowing", "crab_scuttle_telegraph", "crab_scuttling"].includes(motion.mode)) {
|
||||
} else if (motion.mode === "telegraph" || motion.mode === "charging") {
|
||||
targetAngle = Math.atan2(motion.chargeEnd[0] - motion.position[0], motion.chargeEnd[1] - motion.position[1]);
|
||||
}
|
||||
const difference = Math.atan2(
|
||||
|
||||
Reference in New Issue
Block a user