[claude] Timmy slap / ragdoll physics — spring wobble + Pip startle (#43) #85

Merged
Rockachopa merged 1 commits from claude/issue-43 into main 2026-03-23 20:14:37 +00:00

View File

@@ -36,11 +36,12 @@ const COUNTER_RETORTS = [
"YOU'LL REGRET THAT, MORTAL!",
];
// Residual mini-spring for slight trembling post-fall (STAND only)
const SPRING_STIFFNESS = 7.0;
const SPRING_DAMPING = 0.80;
const MAX_TILT_RAD = 0.12;
const SLAP_IMPULSE = 0.18;
// Spring physics for slap wobble (STAND only)
const SPRING_STIFFNESS = 18.0;
const SPRING_DAMPING = 4.5;
const MAX_TILT_RAD = 0.55;
const SLAP_IMPULSE = 2.8;
const RAGDOLL_TILT_THRESHOLD = 0.42; // if tilt exceeds this, trigger full ragdoll fall
// ── Face emotion targets per internal state ───────────────────────────────────
// lidScale: 0 = fully closed, 1 = wide open
@@ -681,31 +682,54 @@ function _updateRagdoll(dt, t, bodyBob) {
}
// ── applySlap — called by interaction.js on hit ───────────────────────────────
// Applies an additive spring impulse so repeated slaps stack. If accumulated
// tilt exceeds RAGDOLL_TILT_THRESHOLD the full ragdoll fall is triggered.
export function applySlap(hitPoint) {
if (!timmy) return;
// Ignore re-slap while already falling/down — wait until standing again
const rd = timmy.rd;
// Ignore re-slap while already in ragdoll cycle — wait until standing
if (rd.state !== RD_STAND) return;
// XZ direction from Timmy to hit point (fall away from impact)
// XZ direction from Timmy to hit point (wobble/fall away from impact)
const dx = hitPoint.x - TIMMY_POS.x;
const dz = hitPoint.z - TIMMY_POS.z;
const len = Math.sqrt(dx * dx + dz * dz) || 1;
rd.fallDirX = dx / len;
rd.fallDirZ = dz / len;
const dirX = dx / len;
const dirZ = dz / len;
// Start ragdoll fall
rd.state = RD_FALL;
rd.timer = 0;
rd.fallAngle = 0;
// Additive spring impulse — stacks with any existing wobble
rd.slapVelocity.x += dirZ * SLAP_IMPULSE; // rotation.x driven by Z direction
rd.slapVelocity.z += -dirX * SLAP_IMPULSE; // rotation.z driven by X direction
// Pip startle — maximum scatter
timmy.pipStartleTimer = 5.0;
timmy.pipStartleDir.x = (Math.random() - 0.5) * 8.0;
timmy.pipStartleDir.z = (Math.random() - 0.5) * 8.0;
// Check if accumulated tilt is large enough to trigger full ragdoll
const tiltMag = Math.sqrt(rd.slapOffset.x * rd.slapOffset.x + rd.slapOffset.z * rd.slapOffset.z);
const velMag = Math.sqrt(rd.slapVelocity.x * rd.slapVelocity.x + rd.slapVelocity.z * rd.slapVelocity.z);
// Crystal flash on impact
if (tiltMag > RAGDOLL_TILT_THRESHOLD || velMag > SLAP_IMPULSE * 2.2) {
// Enough stacked force — trigger full ragdoll fall
rd.fallDirX = dirX;
rd.fallDirZ = dirZ;
rd.state = RD_FALL;
rd.timer = 0;
rd.fallAngle = 0;
// Reset spring state so it's clean when returning to STAND
rd.slapOffset.x = 0; rd.slapOffset.z = 0;
rd.slapVelocity.x = 0; rd.slapVelocity.z = 0;
// Pip startle — maximum scatter on ragdoll
timmy.pipStartleTimer = 5.0;
timmy.pipStartleDir.x = (Math.random() - 0.5) * 8.0;
timmy.pipStartleDir.z = (Math.random() - 0.5) * 8.0;
} else {
// Pip mild startle on wobble
timmy.pipStartleTimer = Math.max(timmy.pipStartleTimer, 3.0);
timmy.pipStartleDir.x = (Math.random() - 0.5) * 4.0;
timmy.pipStartleDir.z = (Math.random() - 0.5) * 4.0;
}
// Crystal flash on every hit
timmy.hitFlashTimer = 0.5;
// Cartoonish SMACK sound