new help section explaining classes and their mechanics

This commit is contained in:
Warren H
2026-07-18 12:05:34 -04:00
parent 638aef22b2
commit 40aa23be46
32 changed files with 1423 additions and 134 deletions
+77 -13
View File
@@ -1,10 +1,12 @@
import { createPortal } from "@react-three/fiber";
import { useTexture } from "@react-three/drei";
import { useEffect, useMemo } from "react";
import * as THREE from "three";
import {
CHARACTER_PART_CATALOG,
characterAppearancePartIds,
type CharacterAppearanceV1,
type CharacterMaterialVariant,
type CharacterPartId,
} from "../game/characterAppearance";
import type { MemberId } from "../game/types";
@@ -12,9 +14,14 @@ import { useGameGLTF } from "./GameAssetProvider";
interface BoundCharacterPart {
group: THREE.Group;
materials: THREE.Material[];
skeletons: THREE.Skeleton[];
}
const CHARACTER_MATERIAL_VARIANT_URLS: Record<CharacterMaterialVariant, string> = {
"priest-vestments": new URL("../assets/game/textures/claudecraft/chars/players/priest-vestments.webp", import.meta.url).href,
};
function rigBonesByName(rigScene: THREE.Object3D) {
const bones = new Map<string, THREE.Bone>();
rigScene.traverse((object) => {
@@ -27,11 +34,13 @@ function createBoundCharacterPart(
sourceScene: THREE.Object3D,
rigScene: THREE.Object3D,
partId: CharacterPartId,
materialTexture?: THREE.Texture,
): BoundCharacterPart {
const definition = CHARACTER_PART_CATALOG[partId];
const rigBones = rigBonesByName(rigScene);
const group = new THREE.Group();
group.name = `character-part:${partId}`;
const materialClones = new Map<THREE.Material, THREE.Material>();
const skeletons: THREE.Skeleton[] = [];
sourceScene.updateMatrixWorld(true);
@@ -44,6 +53,20 @@ function createBoundCharacterPart(
partNode.matrix.decompose(partNode.position, partNode.quaternion, partNode.scale);
partNode.traverse((object) => {
if (!(object instanceof THREE.SkinnedMesh)) return;
if (materialTexture) {
const cloneMaterial = (source: THREE.Material) => {
const existing = materialClones.get(source);
if (existing) return existing;
const clone = source.clone();
if ("map" in clone) clone.map = materialTexture;
clone.needsUpdate = true;
materialClones.set(source, clone);
return clone;
};
object.material = Array.isArray(object.material)
? object.material.map(cloneMaterial)
: cloneMaterial(object.material);
}
const mappedBones = object.skeleton.bones.map((sourceBone) => {
const rigBone = rigBones.get(sourceBone.name);
if (!rigBone) throw new Error(`Character part ${partId} cannot resolve rig bone ${sourceBone.name}.`);
@@ -63,7 +86,56 @@ function createBoundCharacterPart(
group.add(partNode);
}
return { group, skeletons };
return { group, materials: [...materialClones.values()], skeletons };
}
function BoundCharacterPart({
actorScene,
materialTexture,
modelUrls,
partId,
}: {
actorScene: THREE.Object3D;
materialTexture?: THREE.Texture;
modelUrls: Record<MemberId, string>;
partId: CharacterPartId;
}) {
const definition = CHARACTER_PART_CATALOG[partId];
const gltf = useGameGLTF(modelUrls[definition.sourceMemberId]);
const boundPart = useMemo(
() => createBoundCharacterPart(gltf.scene, actorScene, partId, materialTexture),
[actorScene, gltf.scene, materialTexture, partId],
);
useEffect(() => () => {
for (const skeleton of boundPart.skeletons) skeleton.dispose();
for (const material of boundPart.materials) material.dispose();
}, [boundPart]);
const rigRoot = actorScene.getObjectByName("Rig_Medium") ?? actorScene;
return createPortal(<primitive object={boundPart.group} />, rigRoot);
}
function MaterialVariantCharacterPart({
actorScene,
materialVariant,
modelUrls,
partId,
}: {
actorScene: THREE.Object3D;
materialVariant: CharacterMaterialVariant;
modelUrls: Record<MemberId, string>;
partId: CharacterPartId;
}) {
const sourceTexture = useTexture(CHARACTER_MATERIAL_VARIANT_URLS[materialVariant]);
const materialTexture = useMemo(() => {
sourceTexture.flipY = false;
sourceTexture.colorSpace = THREE.SRGBColorSpace;
sourceTexture.magFilter = THREE.NearestFilter;
sourceTexture.needsUpdate = true;
return sourceTexture;
}, [sourceTexture]);
return <BoundCharacterPart actorScene={actorScene} materialTexture={materialTexture} modelUrls={modelUrls} partId={partId} />;
}
function ModularCharacterPart({
@@ -76,18 +148,10 @@ function ModularCharacterPart({
partId: CharacterPartId;
}) {
const definition = CHARACTER_PART_CATALOG[partId];
const gltf = useGameGLTF(modelUrls[definition.sourceMemberId]);
const boundPart = useMemo(
() => createBoundCharacterPart(gltf.scene, actorScene, partId),
[actorScene, gltf.scene, partId],
);
useEffect(() => () => {
for (const skeleton of boundPart.skeletons) skeleton.dispose();
}, [boundPart]);
const rigRoot = actorScene.getObjectByName("Rig_Medium") ?? actorScene;
return createPortal(<primitive object={boundPart.group} />, rigRoot);
const materialVariant = "materialVariant" in definition ? definition.materialVariant : undefined;
return materialVariant
? <MaterialVariantCharacterPart actorScene={actorScene} materialVariant={materialVariant} modelUrls={modelUrls} partId={partId} />
: <BoundCharacterPart actorScene={actorScene} modelUrls={modelUrls} partId={partId} />;
}
export function ModularCharacterBody({