Release v0.1.1 2026-07-10

This commit is contained in:
Warren H
2026-07-10 23:47:37 -04:00
parent 537a311f52
commit 6726c600e4
40 changed files with 2987 additions and 192 deletions
+50 -23
View File
@@ -2,6 +2,7 @@ import { Canvas, createPortal, useFrame, useThree } from "@react-three/fiber";
import { useAnimations, useGLTF } from "@react-three/drei";
import { Suspense, useEffect, useMemo, useRef, type MutableRefObject } from "react";
import * as THREE from "three";
import { getControllerMovement } from "../input/controller";
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
import { useGameStore } from "../game/store";
import type { MemberId, PulseKind } from "../game/types";
@@ -9,6 +10,12 @@ import { BossMechanicIndicators } from "./boss/BossMechanicIndicators";
const BULL_URL = new URL("../../game_assets/models/claudecraft/creatures/bull.glb", import.meta.url).href;
const SPIDER_URL = new URL("../../game_assets/models/downloaded/low-poly-spider/low-poly-spider.glb", import.meta.url).href;
const SPIDER_TEXTURE_URLS: Record<string, string> = {
"Spinnen_Bein_tex_2.jpg": new URL("../../game_assets/models/downloaded/low-poly-spider/textures/Spinnen_Bein_tex_2.jpg", import.meta.url).href,
"SH3.png": new URL("../../game_assets/models/downloaded/low-poly-spider/textures/SH3.png", import.meta.url).href,
"Spinnen_Bein_tex_COLOR_.jpg": new URL("../../game_assets/models/downloaded/low-poly-spider/textures/Spinnen_Bein_tex_COLOR_.jpg", import.meta.url).href,
"haar_detail_NRM.jpg": new URL("../../game_assets/models/downloaded/low-poly-spider/textures/haar_detail_NRM.jpg", import.meta.url).href,
};
const DRAGON_URL = new URL("../../game_assets/models/claudecraft/creatures/dragonevolved.glb", import.meta.url).href;
const PARTY_MODEL_URLS: Record<MemberId, string> = {
aelia: new URL("../../game_assets/models/claudecraft/chars/players/druid.glb", import.meta.url).href,
@@ -41,6 +48,11 @@ const PARTY_ATTACK_CLIPS: Record<MemberId, string> = {
orin: "Spellcast_Shoot",
vale: "Dualwield_Melee_Attack_Chop",
};
const ARENA_COLUMNS = Array.from({ length: 10 }, (_, index) => {
const angle = (index / 10) * Math.PI * 2;
return [Math.sin(angle) * 9.3, Math.cos(angle) * 9.3] as const;
});
const ARENA_TORCH_COLORS = [new THREE.Color("#ff9a4f"), new THREE.Color("#77ddce")] as const;
type GameStoreState = ReturnType<typeof useGameStore.getState>;
function encounterBossAt(state: GameStoreState, bossIndex: number) {
@@ -59,6 +71,13 @@ function targetBossMotionByInstance(state: GameStoreState, instanceId?: string)
return state.additionalBosses.find((entry) => entry.instanceId === instanceId)?.motion ?? targetBossMotion(state);
}
const configureSpiderLoader: NonNullable<Parameters<typeof useGLTF>[3]> = (loader) => {
loader.manager.setURLModifier((url) => {
const fileName = url.slice(url.lastIndexOf("/") + 1);
return SPIDER_TEXTURE_URLS[fileName] ?? url;
});
};
type ActorAnimationState = "idle" | "walk" | "run" | "attack" | "cast" | "hit" | "death";
type WeaponGrip = "staff" | "sword" | "crossbow" | "wand" | "dagger" | "prop";
@@ -208,11 +227,24 @@ function PartyCharacterModel({
}
function Arena() {
const columns = useMemo(() => {
return Array.from({ length: 10 }, (_, index) => {
const angle = (index / 10) * Math.PI * 2;
return [Math.sin(angle) * 9.3, Math.cos(angle) * 9.3] as const;
const pillarInstances = useRef<THREE.InstancedMesh>(null);
const torchInstances = useRef<THREE.InstancedMesh>(null);
useEffect(() => {
const pillars = pillarInstances.current;
const torches = torchInstances.current;
if (!pillars || !torches) return;
const matrix = new THREE.Matrix4();
ARENA_COLUMNS.forEach(([x, z], index) => {
matrix.makeTranslation(x, 1.1, z - 1);
pillars.setMatrixAt(index, matrix);
matrix.makeTranslation(x, 2.6, z - 1);
torches.setMatrixAt(index, matrix);
torches.setColorAt(index, ARENA_TORCH_COLORS[index % ARENA_TORCH_COLORS.length]);
});
pillars.instanceMatrix.needsUpdate = true;
torches.instanceMatrix.needsUpdate = true;
if (torches.instanceColor) torches.instanceColor.needsUpdate = true;
}, []);
return (
@@ -229,19 +261,16 @@ function Arena() {
<circleGeometry args={[2.1, 48]} />
<meshStandardMaterial color="#27342d" roughness={1} />
</mesh>
{columns.map(([x, z], index) => (
<group key={index} position={[x, 0, z - 1]}>
<mesh castShadow receiveShadow position={[0, 1.1, 0]}>
<cylinderGeometry args={[0.38, 0.5, 2.4, 6]} />
<meshStandardMaterial color="#26342f" roughness={0.8} />
</mesh>
<pointLight color={index % 2 ? "#dd7b38" : "#6fc9ba"} intensity={2.2} distance={5} position={[0, 2.7, 0]} />
<mesh position={[0, 2.6, 0]}>
<octahedronGeometry args={[0.2, 0]} />
<meshBasicMaterial color={index % 2 ? "#ff9a4f" : "#77ddce"} />
</mesh>
</group>
))}
<instancedMesh ref={pillarInstances} args={[undefined, undefined, ARENA_COLUMNS.length]} castShadow receiveShadow>
<cylinderGeometry args={[0.38, 0.5, 2.4, 6]} />
<meshStandardMaterial color="#26342f" roughness={0.8} />
</instancedMesh>
<instancedMesh ref={torchInstances} args={[undefined, undefined, ARENA_COLUMNS.length]}>
<octahedronGeometry args={[0.2, 0]} />
<meshBasicMaterial />
</instancedMesh>
<pointLight color="#dd7b38" intensity={2.2} distance={7} position={[-6, 2.7, -1]} />
<pointLight color="#6fc9ba" intensity={2.2} distance={7} position={[6, 2.7, -1]} />
<gridHelper args={[22, 22, "#2c4039", "#1b2925"]} position={[0, 0.01, -1]} />
</group>
);
@@ -365,11 +394,9 @@ function PlayerCharacter() {
if (state.phase === "combat" && !state.paused && !state.activeCast && player.hp > 0 && !knocked) {
inputX = Number(keys.current.has("d")) - Number(keys.current.has("a"));
inputZ = Number(keys.current.has("s")) - Number(keys.current.has("w"));
const gamepad = navigator.getGamepads?.()[0];
if (gamepad) {
inputX += Math.abs(gamepad.axes[0] ?? 0) > 0.18 ? gamepad.axes[0] : 0;
inputZ += Math.abs(gamepad.axes[1] ?? 0) > 0.18 ? gamepad.axes[1] : 0;
}
const controller = getControllerMovement();
inputX += controller.x;
inputZ += controller.y;
}
const length = Math.hypot(inputX, inputZ);
if (length > 0.05) {
@@ -587,7 +614,7 @@ function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex
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);
const gltf = useGLTF(config.url, false, true);
const gltf = useGLTF(config.url, false, true, kind === "vexa" ? configureSpiderLoader : undefined);
const model = useMemo(() => cloneSkeleton(gltf.scene), [gltf.scene]);
const { actions } = useAnimations(gltf.animations, model);
const targetPosition = useMemo(() => new THREE.Vector3(), []);