Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
import * as THREE from 'three';
|
2026-03-18 21:01:13 -04:00
|
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
|
|
|
|
|
|
let controls;
|
|
|
|
|
let _canvas;
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
let _camera = null;
|
|
|
|
|
let _timmyGroup = null;
|
|
|
|
|
let _applySlap = null;
|
|
|
|
|
const _raycaster = new THREE.Raycaster();
|
|
|
|
|
const _pointer = new THREE.Vector2();
|
2026-03-18 21:01:13 -04:00
|
|
|
const _noCtxMenu = e => e.preventDefault();
|
|
|
|
|
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
// ── registerSlapTarget ────────────────────────────────────────────────────────
|
|
|
|
|
// Call after initAgents() with Timmy's group and the applySlap function.
|
|
|
|
|
// The capture-phase pointerdown listener will hit-test against the group and
|
|
|
|
|
// call applySlap(hitPoint) — also suppressing the OrbitControls drag.
|
|
|
|
|
|
|
|
|
|
export function registerSlapTarget(timmyGroup, applyFn) {
|
|
|
|
|
_timmyGroup = timmyGroup;
|
|
|
|
|
_applySlap = applyFn;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 21:01:13 -04:00
|
|
|
export function initInteraction(camera, renderer) {
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
_camera = camera;
|
|
|
|
|
|
2026-03-18 21:01:13 -04:00
|
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
|
|
|
controls.enableDamping = true;
|
|
|
|
|
controls.dampingFactor = 0.05;
|
|
|
|
|
controls.screenSpacePanning = false;
|
|
|
|
|
controls.minDistance = 5;
|
|
|
|
|
controls.maxDistance = 80;
|
|
|
|
|
controls.maxPolarAngle = Math.PI / 2.1;
|
|
|
|
|
controls.target.set(0, 0, 0);
|
|
|
|
|
controls.update();
|
|
|
|
|
|
|
|
|
|
_canvas = renderer.domElement;
|
|
|
|
|
_canvas.addEventListener('contextmenu', _noCtxMenu);
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
|
|
|
|
|
// Capture phase so we intercept before OrbitControls' bubble-phase handler.
|
|
|
|
|
// If Timmy is hit we call stopImmediatePropagation() to suppress the orbit drag.
|
|
|
|
|
_canvas.addEventListener('pointerdown', _onPointerDown, { capture: true });
|
2026-03-19 03:33:48 +00:00
|
|
|
|
|
|
|
|
// touchstart fallback for older mobile browsers that lack Pointer Events
|
|
|
|
|
if (!window.PointerEvent) {
|
|
|
|
|
_canvas.addEventListener('touchstart', _onTouchStart, { capture: true, passive: false });
|
|
|
|
|
}
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 03:33:48 +00:00
|
|
|
function _hitTest(clientX, clientY) {
|
|
|
|
|
if (!_timmyGroup || !_applySlap || !_camera) return false;
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
|
|
|
|
|
const rect = _canvas.getBoundingClientRect();
|
2026-03-19 03:33:48 +00:00
|
|
|
_pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;
|
|
|
|
|
_pointer.y = ((clientY - rect.top) / rect.height) * -2 + 1;
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
|
|
|
|
|
_raycaster.setFromCamera(_pointer, _camera);
|
|
|
|
|
const hits = _raycaster.intersectObject(_timmyGroup, true);
|
|
|
|
|
|
|
|
|
|
if (hits.length > 0) {
|
|
|
|
|
_applySlap(hits[0].point);
|
2026-03-19 03:33:48 +00:00
|
|
|
// 150 ms lockout to avoid accidental orbit drag immediately after a slap
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
if (controls) {
|
|
|
|
|
controls.enabled = false;
|
2026-03-19 03:33:48 +00:00
|
|
|
setTimeout(() => { if (controls) controls.enabled = true; }, 150);
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
}
|
2026-03-19 03:33:48 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _onPointerDown(event) {
|
|
|
|
|
if (_hitTest(event.clientX, event.clientY)) {
|
|
|
|
|
event.stopImmediatePropagation(); // block OrbitControls drag
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _onTouchStart(event) {
|
|
|
|
|
if (!event.touches || event.touches.length === 0) return;
|
|
|
|
|
const t = event.touches[0];
|
|
|
|
|
if (_hitTest(t.clientX, t.clientY)) {
|
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
|
event.preventDefault(); // suppress subsequent mouse events (ghost click)
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
}
|
2026-03-18 21:01:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function updateControls() {
|
|
|
|
|
if (controls) controls.update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Dispose OrbitControls event listeners.
|
|
|
|
|
* Called before context-loss teardown.
|
|
|
|
|
*/
|
|
|
|
|
export function disposeInteraction() {
|
|
|
|
|
if (_canvas) {
|
|
|
|
|
_canvas.removeEventListener('contextmenu', _noCtxMenu);
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
_canvas.removeEventListener('pointerdown', _onPointerDown, { capture: true });
|
2026-03-19 03:33:48 +00:00
|
|
|
if (!window.PointerEvent) {
|
|
|
|
|
_canvas.removeEventListener('touchstart', _onTouchStart, { capture: true });
|
|
|
|
|
}
|
2026-03-18 21:01:13 -04:00
|
|
|
_canvas = null;
|
|
|
|
|
}
|
|
|
|
|
if (controls) {
|
|
|
|
|
controls.dispose();
|
|
|
|
|
controls = null;
|
|
|
|
|
}
|
Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction
## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
|
|
|
_camera = null;
|
|
|
|
|
_timmyGroup = null;
|
|
|
|
|
_applySlap = null;
|
2026-03-18 21:01:13 -04:00
|
|
|
}
|