Browser reference stack for PSX-era third-person adventure: fixed cameras, inventory puzzles, Box3D physics, host-authoritative P2P co-op, and a modular character harness. Ships the Ashgrove Precinct Level 1 investigation demo with a full cast and nine linked rooms.
144 lines
3.5 KiB
TypeScript
144 lines
3.5 KiB
TypeScript
/**
|
|
* Canonical skeleton — GDD §10.2.
|
|
*
|
|
* Mixamo-compatible core set. The harness builds joints under these names and
|
|
* binds skin weights to them, so every generated character plays the same
|
|
* procedural clips (and any future shared animation library).
|
|
*/
|
|
|
|
export const CANONICAL_BONES = [
|
|
"Hips",
|
|
"Spine",
|
|
"Spine1",
|
|
"Spine2",
|
|
"Neck",
|
|
"Head",
|
|
"LeftShoulder",
|
|
"LeftArm",
|
|
"LeftForeArm",
|
|
"LeftHand",
|
|
"RightShoulder",
|
|
"RightArm",
|
|
"RightForeArm",
|
|
"RightHand",
|
|
"LeftUpLeg",
|
|
"LeftLeg",
|
|
"LeftFoot",
|
|
"LeftToeBase",
|
|
"RightUpLeg",
|
|
"RightLeg",
|
|
"RightFoot",
|
|
"RightToeBase",
|
|
] as const;
|
|
|
|
export type CanonicalBone = (typeof CANONICAL_BONES)[number];
|
|
|
|
/** Parent of each bone; `null` marks the skeleton root. */
|
|
export const CANONICAL_HIERARCHY: Record<CanonicalBone, CanonicalBone | null> = {
|
|
Hips: null,
|
|
Spine: "Hips",
|
|
Spine1: "Spine",
|
|
Spine2: "Spine1",
|
|
Neck: "Spine2",
|
|
Head: "Neck",
|
|
LeftShoulder: "Spine2",
|
|
LeftArm: "LeftShoulder",
|
|
LeftForeArm: "LeftArm",
|
|
LeftHand: "LeftForeArm",
|
|
RightShoulder: "Spine2",
|
|
RightArm: "RightShoulder",
|
|
RightForeArm: "RightArm",
|
|
RightHand: "RightForeArm",
|
|
LeftUpLeg: "Hips",
|
|
LeftLeg: "LeftUpLeg",
|
|
LeftFoot: "LeftLeg",
|
|
LeftToeBase: "LeftFoot",
|
|
RightUpLeg: "Hips",
|
|
RightLeg: "RightUpLeg",
|
|
RightFoot: "RightLeg",
|
|
RightToeBase: "RightFoot",
|
|
};
|
|
|
|
/** Bones a rigger must produce for a clip to be playable at all. */
|
|
export const REQUIRED_BONES: readonly CanonicalBone[] = [
|
|
"Hips",
|
|
"Spine",
|
|
"Head",
|
|
"LeftArm",
|
|
"RightArm",
|
|
"LeftUpLeg",
|
|
"LeftLeg",
|
|
"LeftFoot",
|
|
"RightUpLeg",
|
|
"RightLeg",
|
|
"RightFoot",
|
|
];
|
|
|
|
const CANONICAL_SET: ReadonlySet<string> = new Set(CANONICAL_BONES);
|
|
|
|
export function isCanonicalBone(name: string): name is CanonicalBone {
|
|
return CANONICAL_SET.has(name);
|
|
}
|
|
|
|
/** Depth-first walk order, parents always before children. */
|
|
export function canonicalWalkOrder(): CanonicalBone[] {
|
|
const out: CanonicalBone[] = [];
|
|
const visit = (bone: CanonicalBone) => {
|
|
out.push(bone);
|
|
for (const child of CANONICAL_BONES) {
|
|
if (CANONICAL_HIERARCHY[child] === bone) visit(child);
|
|
}
|
|
};
|
|
visit("Hips");
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Common aliases emitted by third-party riggers, normalised to canonical names.
|
|
* Mixamo's `mixamorig:` prefix is stripped before lookup.
|
|
*/
|
|
export const BONE_ALIASES: Record<string, CanonicalBone> = {
|
|
hip: "Hips",
|
|
pelvis: "Hips",
|
|
root: "Hips",
|
|
spine_01: "Spine",
|
|
spine_02: "Spine1",
|
|
spine_03: "Spine2",
|
|
chest: "Spine2",
|
|
upperchest: "Spine2",
|
|
head_end: "Head",
|
|
clavicle_l: "LeftShoulder",
|
|
clavicle_r: "RightShoulder",
|
|
upperarm_l: "LeftArm",
|
|
upperarm_r: "RightArm",
|
|
lowerarm_l: "LeftForeArm",
|
|
lowerarm_r: "RightForeArm",
|
|
hand_l: "LeftHand",
|
|
hand_r: "RightHand",
|
|
thigh_l: "LeftUpLeg",
|
|
thigh_r: "RightUpLeg",
|
|
calf_l: "LeftLeg",
|
|
calf_r: "RightLeg",
|
|
foot_l: "LeftFoot",
|
|
foot_r: "RightFoot",
|
|
ball_l: "LeftToeBase",
|
|
ball_r: "RightToeBase",
|
|
};
|
|
|
|
/** Best-effort mapping of an arbitrary rigger bone name onto the canonical set. */
|
|
export function normaliseBoneName(raw: string): CanonicalBone | null {
|
|
const stripped = raw.replace(/^mixamorig[:_]?/i, "").trim();
|
|
if (isCanonicalBone(stripped)) return stripped;
|
|
|
|
const key = stripped.toLowerCase().replace(/[\s.-]+/g, "_");
|
|
const alias = BONE_ALIASES[key];
|
|
if (alias) return alias;
|
|
|
|
// `LeftArm` vs `leftarm` vs `Left_Arm`
|
|
const squashed = key.replace(/_/g, "");
|
|
for (const bone of CANONICAL_BONES) {
|
|
if (bone.toLowerCase() === squashed) return bone;
|
|
}
|
|
return null;
|
|
}
|