forked from phenom/healer-man
Release Healer Man 0.1.9
This commit is contained in:
1 parent
c245a199b6
commit
d060296ade
74 files changed
+5901
-334
No files matched your search
@@ -5,31 +5,47 @@ import {
|
||||
Suspense,
|
||||
useEffect,
|
||||
useMemo,
|
||||
type MutableRefObject,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
DoubleSide,
|
||||
Mesh,
|
||||
Object3D,
|
||||
SkinnedMesh,
|
||||
Vector3,
|
||||
type Material,
|
||||
} from "three";
|
||||
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
import type { ClassId } from "../app/characterCatalog";
|
||||
import type { AvatarManifestEntry } from "./characterManifest";
|
||||
import type {
|
||||
EquipmentAttachmentName,
|
||||
EquipmentModelVisual,
|
||||
} from "./equipmentAppearance";
|
||||
import {
|
||||
resolveAvatarAttachmentPoint,
|
||||
type AvatarAttachmentTransform,
|
||||
} from "./attachmentPoints";
|
||||
import { AvatarErrorBoundary } from "./AvatarErrorBoundary";
|
||||
import {
|
||||
avatarWeaponSocketRotation,
|
||||
findAvatarHandBone,
|
||||
StarterWeaponAttachments,
|
||||
findAvatarShieldBone,
|
||||
StarterWeaponVisual,
|
||||
type AvatarHand,
|
||||
type StarterWeaponRotation,
|
||||
} from "./starterWeapons";
|
||||
|
||||
const EQUIPMENT_ATTACHMENT_NAMES: readonly EquipmentAttachmentName[] = Object.freeze([
|
||||
"handLeft",
|
||||
"handRight",
|
||||
"shield",
|
||||
"shoulderLeft",
|
||||
"shoulderRight",
|
||||
]);
|
||||
const NO_ROTATION: StarterWeaponRotation = Object.freeze([0, 0, 0]);
|
||||
|
||||
export type EquipmentAttachmentTransforms = Readonly<
|
||||
Partial<Record<EquipmentAttachmentName, AvatarAttachmentTransform>>
|
||||
>;
|
||||
|
||||
export function equipmentAttachmentHand(
|
||||
attachment: EquipmentAttachmentName,
|
||||
): AvatarHand | null {
|
||||
@@ -58,23 +74,14 @@ function cloneEquipmentScene(source: Object3D, name: string): Object3D {
|
||||
return scene;
|
||||
}
|
||||
|
||||
function findSkinBone(root: Object3D, boneIndex: number): Object3D | null {
|
||||
let bone: Object3D | null = null;
|
||||
root.traverse((node) => {
|
||||
if (bone) return;
|
||||
const skinnedMesh = node as SkinnedMesh;
|
||||
if (!skinnedMesh.isSkinnedMesh) return;
|
||||
bone = skinnedMesh.skeleton.bones[boneIndex] ?? null;
|
||||
});
|
||||
return bone;
|
||||
}
|
||||
|
||||
function fallbackTarget(
|
||||
root: Object3D,
|
||||
attachment: EquipmentAttachmentName,
|
||||
): Object3D | null {
|
||||
if (attachment === "handLeft" || attachment === "shield") {
|
||||
return findAvatarHandBone(root, "left");
|
||||
return attachment === "shield"
|
||||
? findAvatarShieldBone(root)
|
||||
: findAvatarHandBone(root, "left");
|
||||
}
|
||||
if (attachment === "handRight") return findAvatarHandBone(root, "right");
|
||||
return root.getObjectByName(
|
||||
@@ -82,43 +89,42 @@ function fallbackTarget(
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
function attachmentTransform(
|
||||
/** Snapshots all authored attachment offsets once, before animation begins. */
|
||||
export function resolveEquipmentAttachmentTransforms(
|
||||
root: Object3D,
|
||||
entry: AvatarManifestEntry,
|
||||
attachment: EquipmentAttachmentName,
|
||||
): { target: Object3D; position: Vector3 } | null {
|
||||
const point = entry.attachmentPoints?.[attachment];
|
||||
const target = point
|
||||
? findSkinBone(root, point.bone) ?? fallbackTarget(root, attachment)
|
||||
: fallbackTarget(root, attachment);
|
||||
if (!target) return null;
|
||||
if (!point) return { target, position: new Vector3() };
|
||||
|
||||
root.updateWorldMatrix(true, true);
|
||||
const position = root.localToWorld(new Vector3(...point.position));
|
||||
target.worldToLocal(position);
|
||||
return { target, position };
|
||||
): EquipmentAttachmentTransforms {
|
||||
const transforms: Partial<Record<EquipmentAttachmentName, AvatarAttachmentTransform>> = {};
|
||||
for (const attachment of EQUIPMENT_ATTACHMENT_NAMES) {
|
||||
const transform = resolveAvatarAttachmentPoint(
|
||||
root,
|
||||
entry.attachmentPoints?.[attachment],
|
||||
fallbackTarget(root, attachment),
|
||||
{
|
||||
useTargetOrigin: attachment === "handLeft"
|
||||
|| attachment === "handRight"
|
||||
|| attachment === "shield",
|
||||
},
|
||||
);
|
||||
if (transform) transforms[attachment] = transform;
|
||||
}
|
||||
return transforms;
|
||||
}
|
||||
|
||||
function AttachedEquipmentModel({
|
||||
entry,
|
||||
itemModel,
|
||||
root,
|
||||
transform,
|
||||
}: {
|
||||
readonly entry: AvatarManifestEntry;
|
||||
readonly itemModel: EquipmentModelVisual;
|
||||
readonly root: Object3D;
|
||||
readonly transform: AvatarAttachmentTransform;
|
||||
}): ReactNode {
|
||||
const gltf = useGLTF(resolveContentUrl(itemModel.url));
|
||||
const scene = useMemo(
|
||||
() => cloneEquipmentScene(gltf.scene, `${itemModel.itemId}-${itemModel.attachment}`),
|
||||
[gltf.scene, itemModel.attachment, itemModel.itemId],
|
||||
);
|
||||
const transform = useMemo(
|
||||
() => attachmentTransform(root, entry, itemModel.attachment),
|
||||
[entry, itemModel.attachment, root],
|
||||
);
|
||||
|
||||
useEffect(() => () => {
|
||||
const materials = new Set<Material>();
|
||||
scene.traverse((node) => {
|
||||
@@ -130,52 +136,109 @@ function AttachedEquipmentModel({
|
||||
materials.forEach((material) => material.dispose());
|
||||
}, [scene]);
|
||||
|
||||
if (!transform) return null;
|
||||
const socketRotation = itemModel.fallbackKind && itemModel.weaponStance
|
||||
? avatarWeaponSocketRotation(entry, itemModel.fallbackKind, itemModel.weaponStance)
|
||||
: NO_ROTATION;
|
||||
|
||||
return createPortal(
|
||||
<primitive object={scene} position={transform.position} />,
|
||||
<group
|
||||
position={transform.position}
|
||||
rotation={socketRotation}
|
||||
scale={transform.scale}
|
||||
>
|
||||
<primitive object={scene} />
|
||||
</group>,
|
||||
transform.target,
|
||||
);
|
||||
}
|
||||
|
||||
export function EquipmentAttachments({
|
||||
attackPulseRef,
|
||||
classId,
|
||||
function EquipmentModelFallback({
|
||||
entry,
|
||||
models,
|
||||
root,
|
||||
fallbackScale,
|
||||
itemModel,
|
||||
transform,
|
||||
}: {
|
||||
readonly attackPulseRef?: MutableRefObject<number>;
|
||||
readonly classId: ClassId;
|
||||
readonly entry: AvatarManifestEntry;
|
||||
readonly fallbackScale: number;
|
||||
readonly itemModel: EquipmentModelVisual;
|
||||
readonly transform: AvatarAttachmentTransform;
|
||||
}): ReactNode {
|
||||
const hand = equipmentAttachmentHand(itemModel.attachment);
|
||||
if (!hand || !itemModel.fallbackKind || !itemModel.weaponStance) return null;
|
||||
const socketRotation = avatarWeaponSocketRotation(
|
||||
entry,
|
||||
itemModel.fallbackKind,
|
||||
itemModel.weaponStance,
|
||||
);
|
||||
return createPortal(
|
||||
<group
|
||||
position={transform.position}
|
||||
rotation={socketRotation}
|
||||
scale={transform.scale}
|
||||
>
|
||||
<StarterWeaponVisual
|
||||
hand={hand}
|
||||
kind={itemModel.fallbackKind}
|
||||
name={`fallback-equipment-${itemModel.itemId}-${itemModel.attachment}`}
|
||||
scale={fallbackScale}
|
||||
/>
|
||||
</group>,
|
||||
transform.target,
|
||||
);
|
||||
}
|
||||
|
||||
function ResolvedEquipmentAttachment({
|
||||
entry,
|
||||
fallbackScale,
|
||||
itemModel,
|
||||
transform,
|
||||
}: {
|
||||
readonly entry: AvatarManifestEntry;
|
||||
readonly fallbackScale: number;
|
||||
readonly itemModel: EquipmentModelVisual;
|
||||
readonly transform: AvatarAttachmentTransform;
|
||||
}): ReactNode {
|
||||
const fallback = (
|
||||
<EquipmentModelFallback
|
||||
entry={entry}
|
||||
fallbackScale={fallbackScale}
|
||||
itemModel={itemModel}
|
||||
transform={transform}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<AvatarErrorBoundary
|
||||
fallback={fallback}
|
||||
resetKey={`${itemModel.itemId}:${itemModel.attachment}:${itemModel.url}`}
|
||||
>
|
||||
<Suspense fallback={fallback}>
|
||||
<AttachedEquipmentModel entry={entry} itemModel={itemModel} transform={transform} />
|
||||
</Suspense>
|
||||
</AvatarErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export function EquipmentAttachments({
|
||||
entry,
|
||||
fallbackScale,
|
||||
models,
|
||||
transforms,
|
||||
}: {
|
||||
readonly entry: AvatarManifestEntry;
|
||||
readonly fallbackScale: number;
|
||||
readonly models: readonly EquipmentModelVisual[];
|
||||
readonly root: Object3D;
|
||||
readonly transforms: EquipmentAttachmentTransforms;
|
||||
}): ReactNode {
|
||||
return models.map((itemModel) => {
|
||||
const hand = equipmentAttachmentHand(itemModel.attachment);
|
||||
const fallback = hand
|
||||
? (
|
||||
<StarterWeaponAttachments
|
||||
attackPulseRef={attackPulseRef}
|
||||
classId={classId}
|
||||
onlyHands={[hand]}
|
||||
root={root}
|
||||
/>
|
||||
)
|
||||
: null;
|
||||
return (
|
||||
<AvatarErrorBoundary
|
||||
const transform = transforms[itemModel.attachment];
|
||||
return transform ? (
|
||||
<ResolvedEquipmentAttachment
|
||||
key={`${itemModel.itemId}:${itemModel.attachment}`}
|
||||
fallback={fallback}
|
||||
resetKey={`${itemModel.itemId}:${itemModel.attachment}:${itemModel.url}`}
|
||||
>
|
||||
<Suspense fallback={fallback}>
|
||||
<AttachedEquipmentModel
|
||||
entry={entry}
|
||||
itemModel={itemModel}
|
||||
root={root}
|
||||
/>
|
||||
</Suspense>
|
||||
</AvatarErrorBoundary>
|
||||
);
|
||||
entry={entry}
|
||||
fallbackScale={fallbackScale}
|
||||
itemModel={itemModel}
|
||||
transform={transform}
|
||||
/>
|
||||
) : null;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user