mirror of
https://github.com/isledecomp/isle.pizza.git
synced 2026-03-01 06:17:38 +00:00
Fix actor editor animation and interaction bugs
Use mood (not sound+4*move) to select walking animation, matching FUN_10063b90. Load secondary animation tier (speed 4.0 threshold) which NPCs typically use in-game, producing the independent head/hat movement. Fix switchSound wrap to 9 values, add switchColor click remapping for claws/head/body, fix g_cycles case mismatches, add morph key visibility support, and preserve root Y-translation for vertical bounce while stripping horizontal movement.
This commit is contained in:
parent
3b925adafd
commit
c2d7570d41
@ -33,13 +33,13 @@ const ACTOR_SUFFIX_INDEX = (() => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* g_cycles[11][17] — animation name table from legoanimationmanager.cpp.
|
* g_cycles[11][17] — animation name table from legoanimationmanager.cpp.
|
||||||
* Rows = character type suffix index, columns = sound + 4 * move (0-16).
|
* Rows = character type suffix index, columns = mood (0-3) for walking, higher indices for other animations.
|
||||||
*/
|
*/
|
||||||
const G_CYCLES = [
|
const G_CYCLES = [
|
||||||
// 0: xx
|
// 0: xx
|
||||||
['CNs001xx','CNs002xx','CNs003xx','CNs004xx','CNs005xx','CNs007xx','CNs006xx','CNs008xx','CNs009xx','CNs010xx','CNs011xx','CNs012xx',null,null,null,null,null],
|
['CNs001xx','CNs002xx','CNs003xx','CNs004xx','CNs005xx','CNs007xx','CNs006xx','CNs008xx','CNs009xx','CNs010xx','CNs011xx','CNs012xx',null,null,null,null,null],
|
||||||
// 1: Pe
|
// 1: Pe
|
||||||
['CNs001Pe','CNs002Pe','CNs003Pe','CNs004Pe','CNs005Pe','CNs007Pe','CNs006Pe','CNs008Pe','CNs009Pe','CNs010Pe','CNs001Sk',null,null,null,null,null,null], // CNs001Sk = skateboard
|
['CNs001Pe','CNs002Pe','CNs003Pe','CNs004Pe','CNs005Pe','CNs007Pe','CNs006Pe','CNs008Pe','CNs009Pe','CNs010Pe','CNs001sk',null,null,null,null,null,null], // CNs001sk = skateboard
|
||||||
// 2: Ma
|
// 2: Ma
|
||||||
['CNs001Ma','CNs002Ma','CNs003Ma','CNs004Ma','CNs005Ma','CNs007Ma','CNs006Ma','CNs008Ma','CNs009Ma','CNs010Ma','CNs0x4Ma',null,null,'CNs011Ma','CNs012Ma','CNs013Ma',null],
|
['CNs001Ma','CNs002Ma','CNs003Ma','CNs004Ma','CNs005Ma','CNs007Ma','CNs006Ma','CNs008Ma','CNs009Ma','CNs010Ma','CNs0x4Ma',null,null,'CNs011Ma','CNs012Ma','CNs013Ma',null],
|
||||||
// 3: Pa
|
// 3: Pa
|
||||||
@ -49,7 +49,7 @@ const G_CYCLES = [
|
|||||||
// 5: La
|
// 5: La
|
||||||
['CNs001La','CNs002La','CNs003La','CNs004La','CNs005La','CNs007La','CNs006La','CNs008La','CNs009La','CNs010La','CNs011La','CNsx11La',null,null,null,null,null],
|
['CNs001La','CNs002La','CNs003La','CNs004La','CNs005La','CNs007La','CNs006La','CNs008La','CNs009La','CNs010La','CNs011La','CNsx11La',null,null,null,null,null],
|
||||||
// 6: Br
|
// 6: Br
|
||||||
['CNs001Br','CNs002Br','CNs003Br','CNs004Br','CNs005Br','CNs007Br','CNs006Br','CNs008Br','CNs009Br','CNs010Br','CNs011Br','CNs900Br','CNs901BR','CNs011Br','CNs012Br','CNs013Br','CNs014Br'],
|
['CNs001Br','CNs002Br','CNs003Br','CNs004Br','CNs005Br','CNs007Br','CNs006Br','CNs008Br','CNs009Br','CNs010Br','CNs011Br','CNs900Br','CNs901Br','CNs011Br','CNs012Br','CNs013Br','CNs014Br'],
|
||||||
// 7: Bd
|
// 7: Bd
|
||||||
['CNs001xx','CNs002xx','CNs003xx','CNs004xx','CNs005xx','CNs007xx','CNs006xx','CNs008xx','CNs009xx','CNs010xx','CNs001Bd','CNs012xx',null,null,null,null,null],
|
['CNs001xx','CNs002xx','CNs003xx','CNs004xx','CNs005xx','CNs007xx','CNs006xx','CNs008xx','CNs009xx','CNs010xx','CNs001Bd','CNs012xx',null,null,null,null,null],
|
||||||
// 8: Pg
|
// 8: Pg
|
||||||
@ -169,10 +169,9 @@ export class ActorRenderer extends BaseRenderer {
|
|||||||
this.modelGroup.rotation.y = Math.PI;
|
this.modelGroup.rotation.y = Math.PI;
|
||||||
this.scene.add(this.modelGroup);
|
this.scene.add(this.modelGroup);
|
||||||
|
|
||||||
// Load and start animation based on move/sound
|
// Load and start walking animation based on mood
|
||||||
const move = charState?.move ?? 0;
|
const mood = charState?.mood ?? 0;
|
||||||
const sound = charState?.sound ?? 0;
|
this.loadAnimationForActor(actorIndex, mood);
|
||||||
this.loadAnimationForActor(actorIndex, move, sound);
|
|
||||||
|
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
}
|
}
|
||||||
@ -359,19 +358,36 @@ export class ActorRenderer extends BaseRenderer {
|
|||||||
// ─── Animation System ────────────────────────────────────────────
|
// ─── Animation System ────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load and start animation for the given actor using g_cycles table.
|
* Compute secondary animation column index from mood, matching FUN_10063b90.
|
||||||
* Animation index = sound + 4 * move. Pre-computes world-space transforms
|
* Primary: columns 0-3 (speed 0.7), Secondary: columns 4-6 (speed 4.0).
|
||||||
* by evaluating the animation tree hierarchically, then plays via AnimationMixer.
|
* NPCs walk at speed 0.6-2.0, so most use the secondary animation which has
|
||||||
|
* independent head/hat movement. Mood adjustment: if (mood >= 2) mood--.
|
||||||
|
*/
|
||||||
|
static getSecondaryAnimColumn(mood) {
|
||||||
|
let adjMood = mood;
|
||||||
|
if (adjMood >= 2) adjMood--;
|
||||||
|
return adjMood + 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and start walking animation for the given actor using g_cycles table.
|
||||||
|
* Loads the secondary (speed 4.0) animation which NPCs typically use in-game,
|
||||||
|
* falling back to primary (speed 0.7) if unavailable. Matches FUN_10063b90
|
||||||
|
* in legoanimationmanager.cpp. Pre-computes world-space transforms by evaluating
|
||||||
|
* the animation tree hierarchically, then plays via AnimationMixer.
|
||||||
* Falls back to Y-axis rotation if unavailable.
|
* Falls back to Y-axis rotation if unavailable.
|
||||||
*/
|
*/
|
||||||
async loadAnimationForActor(actorIndex, move = 0, sound = 0) {
|
async loadAnimationForActor(actorIndex, mood = 0) {
|
||||||
if (!this.modelGroup) return;
|
if (!this.modelGroup) return;
|
||||||
|
|
||||||
this.stopAnimation();
|
this.stopAnimation();
|
||||||
|
|
||||||
const suffixIdx = ACTOR_SUFFIX_INDEX[actorIndex] ?? 0;
|
const suffixIdx = ACTOR_SUFFIX_INDEX[actorIndex] ?? 0;
|
||||||
const animIdx = sound + 4 * move;
|
// Use secondary animation (speed 4.0 threshold) — this is what NPCs use in-game
|
||||||
const animName = G_CYCLES[suffixIdx]?.[animIdx];
|
// since their walking speed (0.6-2.0) exceeds the 0.7 primary threshold
|
||||||
|
const secondaryCol = ActorRenderer.getSecondaryAnimColumn(mood);
|
||||||
|
const primaryCol = mood;
|
||||||
|
const animName = G_CYCLES[suffixIdx]?.[secondaryCol] ?? G_CYCLES[suffixIdx]?.[primaryCol];
|
||||||
|
|
||||||
if (!animName) return; // null entry in g_cycles — no animation for this combo
|
if (!animName) return; // null entry in g_cycles — no animation for this combo
|
||||||
|
|
||||||
@ -448,6 +464,8 @@ export class ActorRenderer extends BaseRenderer {
|
|||||||
tracks.push(new THREE.VectorKeyframeTrack(name, timesSec, values));
|
tracks.push(new THREE.VectorKeyframeTrack(name, timesSec, values));
|
||||||
} else if (name.endsWith('.quaternion')) {
|
} else if (name.endsWith('.quaternion')) {
|
||||||
tracks.push(new THREE.QuaternionKeyframeTrack(name, timesSec, values));
|
tracks.push(new THREE.QuaternionKeyframeTrack(name, timesSec, values));
|
||||||
|
} else if (name.endsWith('.visible')) {
|
||||||
|
tracks.push(new THREE.BooleanKeyframeTrack(name, timesSec, values));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tracks;
|
return tracks;
|
||||||
@ -461,6 +479,7 @@ export class ActorRenderer extends BaseRenderer {
|
|||||||
for (const key of data.translationKeys) timesSet.add(key.time);
|
for (const key of data.translationKeys) timesSet.add(key.time);
|
||||||
for (const key of data.rotationKeys) timesSet.add(key.time);
|
for (const key of data.rotationKeys) timesSet.add(key.time);
|
||||||
for (const key of data.scaleKeys) timesSet.add(key.time);
|
for (const key of data.scaleKeys) timesSet.add(key.time);
|
||||||
|
for (const key of data.morphKeys) timesSet.add(key.time);
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
this.collectKeyframeTimes(child, timesSet);
|
this.collectKeyframeTimes(child, timesSet);
|
||||||
}
|
}
|
||||||
@ -489,13 +508,19 @@ export class ActorRenderer extends BaseRenderer {
|
|||||||
mat = this.evaluateRotation(data.rotationKeys, time);
|
mat = this.evaluateRotation(data.rotationKeys, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Translation (skip on root node so the actor walks in place)
|
// 2. Translation
|
||||||
if (!isRoot && data.translationKeys.length > 0) {
|
if (data.translationKeys.length > 0) {
|
||||||
const vertex = this.interpolateVertex(data.translationKeys, time, true);
|
const vertex = this.interpolateVertex(data.translationKeys, time, true);
|
||||||
if (vertex) {
|
if (vertex) {
|
||||||
mat.elements[12] += vertex.x;
|
if (isRoot) {
|
||||||
mat.elements[13] += vertex.y;
|
// Root: only apply vertical (Y) to preserve bounce,
|
||||||
mat.elements[14] += vertex.z;
|
// strip horizontal (XZ) so the actor walks in place
|
||||||
|
mat.elements[13] += vertex.y;
|
||||||
|
} else {
|
||||||
|
mat.elements[12] += vertex.x;
|
||||||
|
mat.elements[13] += vertex.y;
|
||||||
|
mat.elements[14] += vertex.z;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,6 +544,12 @@ export class ActorRenderer extends BaseRenderer {
|
|||||||
const trackName = partGroup.name;
|
const trackName = partGroup.name;
|
||||||
this.pushValues(valueMap, `${trackName}.position`, position.toArray());
|
this.pushValues(valueMap, `${trackName}.position`, position.toArray());
|
||||||
this.pushValues(valueMap, `${trackName}.quaternion`, [quaternion.x, quaternion.y, quaternion.z, quaternion.w]);
|
this.pushValues(valueMap, `${trackName}.quaternion`, [quaternion.x, quaternion.y, quaternion.z, quaternion.w]);
|
||||||
|
|
||||||
|
// Evaluate visibility from morph keys (matches game's SetVisibility(data->GetVisibility(p_time)))
|
||||||
|
if (data.morphKeys.length > 0) {
|
||||||
|
const visible = this.getVisibility(data.morphKeys, time);
|
||||||
|
this.pushValues(valueMap, `${trackName}.visible`, [visible]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,6 +641,23 @@ export class ActorRenderer extends BaseRenderer {
|
|||||||
return { before, after: keys[idx] || null };
|
return { before, after: keys[idx] || null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate visibility from morph keys at a given time.
|
||||||
|
* Matches game's GetVisibility: returns true (visible) by default,
|
||||||
|
* or the last morph key's visible flag at or before the given time.
|
||||||
|
*/
|
||||||
|
getVisibility(morphKeys, time) {
|
||||||
|
let lastKey = null;
|
||||||
|
for (const key of morphKeys) {
|
||||||
|
if (key.time <= time) {
|
||||||
|
lastKey = key;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastKey ? lastKey.visible : true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append values to a named entry in the value map.
|
* Append values to a named entry in the value map.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
charState.legrtNameIndex === actorInfo.parts[9].nameIndex;
|
charState.legrtNameIndex === actorInfo.parts[9].nameIndex;
|
||||||
|
|
||||||
function actorKey(slotNumber, idx, cs) {
|
function actorKey(slotNumber, idx, cs) {
|
||||||
return `${slotNumber}-${idx}-${cs.hatPartNameIndex}-${cs.hatNameIndex}-${cs.infogronNameIndex}-${cs.armlftNameIndex}-${cs.armrtNameIndex}-${cs.leglftNameIndex}-${cs.legrtNameIndex}-${cs.move}-${cs.sound}`;
|
return `${slotNumber}-${idx}-${cs.hatPartNameIndex}-${cs.hatNameIndex}-${cs.infogronNameIndex}-${cs.armlftNameIndex}-${cs.armrtNameIndex}-${cs.leglftNameIndex}-${cs.legrtNameIndex}-${cs.mood}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@ -141,7 +141,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function switchSound() {
|
function switchSound() {
|
||||||
const nextSound = (charState.sound + 1) % 4;
|
const nextSound = (charState.sound + 1) % 9;
|
||||||
onUpdate({
|
onUpdate({
|
||||||
character: { characterIndex: actorIndex, field: 'sound', value: nextSound }
|
character: { characterIndex: actorIndex, field: 'sound', value: nextSound }
|
||||||
});
|
});
|
||||||
@ -155,9 +155,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function switchColor(event) {
|
function switchColor(event) {
|
||||||
const partIdx = renderer.getClickedPart(event);
|
let partIdx = renderer.getClickedPart(event);
|
||||||
if (partIdx < 0) return;
|
if (partIdx < 0) return;
|
||||||
|
|
||||||
|
// Remap clicked part to the part that owns its color
|
||||||
|
// (matches SwitchColor in legocharactermanager.cpp)
|
||||||
|
if (partIdx === ActorPart.CLAWLFT) partIdx = ActorPart.ARMLFT;
|
||||||
|
else if (partIdx === ActorPart.CLAWRT) partIdx = ActorPart.ARMRT;
|
||||||
|
else if (partIdx === ActorPart.HEAD) partIdx = ActorPart.INFOHAT;
|
||||||
|
else if (partIdx === ActorPart.BODY) partIdx = ActorPart.INFOGRON;
|
||||||
|
|
||||||
// Map part index to the save field
|
// Map part index to the save field
|
||||||
const fieldMap = {
|
const fieldMap = {
|
||||||
[ActorPart.INFOHAT]: 'hatNameIndex',
|
[ActorPart.INFOHAT]: 'hatNameIndex',
|
||||||
@ -169,7 +176,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const field = fieldMap[partIdx];
|
const field = fieldMap[partIdx];
|
||||||
if (!field) return; // Body (0) and Head (3) don't have color fields
|
if (!field) return;
|
||||||
|
|
||||||
const part = actorInfo.parts[partIdx];
|
const part = actorInfo.parts[partIdx];
|
||||||
if (!part.nameIndices) return;
|
if (!part.nameIndices) return;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user