From cbeaa61083628b6549d8ab562f9fabc17c5d35cd Mon Sep 17 00:00:00 2001 From: "Claude (Opus 4.6)" Date: Mon, 23 Mar 2026 20:14:36 +0000 Subject: [PATCH] =?UTF-8?q?[claude]=20Timmy=20slap=20/=20ragdoll=20physics?= =?UTF-8?q?=20=E2=80=94=20spring=20wobble=20+=20Pip=20startle=20(#43)=20(#?= =?UTF-8?q?85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude (Opus 4.6) Co-committed-by: Claude (Opus 4.6) --- the-matrix/js/agents.js | 60 ++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/the-matrix/js/agents.js b/the-matrix/js/agents.js index 92e1fbe..d5c1721 100644 --- a/the-matrix/js/agents.js +++ b/the-matrix/js/agents.js @@ -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