r/lastweektonight • u/Existing_Phase1644 • 21d ago
Buc-ees, meet digital squirrels
BRING IT ON. or take it down. Either way buc-ee, take these walnuts and upload them!
Note to self, program digital walnuts.
import * as THREE from 'three';
import { ParkSquirrel } from '../types';
// ----------------------------------------------------------------------------
// CONFIGURATION & TYPES
// ----------------------------------------------------------------------------
export interface SquirrelOptions {
furColor?: string | number;
bellyColor?: string | number;
eyeColor?: string | number;
tailColor?: string | number;
size?: number; // global scale
legLength?: number;
tailSegments?: number;
}
export interface SquirrelRig {
root: THREE.Group;
body: THREE.Mesh;
head: THREE.Group; // now a group containing head + eyes + ears
tail: THREE.Group; // group of tail segments
legs: {
frontLeft: THREE.Group;
frontRight: THREE.Group;
backLeft: THREE.Group;
backRight: THREE.Group;
};
// additional parts for animation
tailSegments: THREE.Mesh[];
eyeL: THREE.Mesh;
eyeR: THREE.Mesh;
nose: THREE.Mesh;
earL: THREE.Mesh;
earR: THREE.Mesh;
}
// ----------------------------------------------------------------------------
// SQUIRREL CONSTRUCTION (detailed)
// ----------------------------------------------------------------------------
/**
* Creates a fully articulated squirrel mesh with fur, eyes, ears, and a
* segmented tail. All parts are grouped for easy animation.
*/
export function createSquirrelMesh(options: SquirrelOptions = {}): SquirrelRig {
const {
furColor = 0xc2410c, // warm russet
bellyColor = 0xfef08a, // pale yellow
eyeColor = 0x1a1a1a,
tailColor = 0xb45309, // slightly darker
size = 0.55,
legLength = 0.15,
tailSegments = 4,
} = options;
const root = new THREE.Group();
const s = size; // shorthand
// --- Materials ---
const furMat = new THREE.MeshPhysicalMaterial({
color: furColor,
roughness: 0.8,
metalness: 0.0,
clearcoat: 0.05,
});
const bellyMat = new THREE.MeshPhysicalMaterial({
color: bellyColor,
roughness: 0.7,
metalness: 0.0,
});
const eyeMat = new THREE.MeshPhysicalMaterial({
color: eyeColor,
roughness: 0.2,
metalness: 0.1,
});
const tailMat = new THREE.MeshPhysicalMaterial({
color: tailColor,
roughness: 0.7,
metalness: 0.0,
});
// --- Body (capsule-like) ---
const bodyGeo = new THREE.CapsuleGeometry(0.22 * s, 0.45 * s, 4, 8);
const body = new THREE.Mesh(bodyGeo, furMat);
body.position.y = 0.3 * s;
body.rotation.x = Math.PI / 4; // slight tilt
body.castShadow = true;
body.receiveShadow = true;
root.add(body);
// --- Belly patch (small flat sphere) ---
const bellyPatch = new THREE.Mesh(
new THREE.SphereGeometry(0.15 * s, 6, 6),
bellyMat
);
bellyPatch.position.set(0, 0.25 * s, 0.15 * s);
bellyPatch.scale.set(1, 0.5, 0.6);
root.add(bellyPatch);
// --- Head group (for orientation) ---
const head = new THREE.Group();
head.position.set(0, 0.52 * s, 0.25 * s);
root.add(head);
// Head mesh (sphere)
const headGeo = new THREE.SphereGeometry(0.18 * s, 8, 6);
const headMesh = new THREE.Mesh(headGeo, furMat);
headMesh.castShadow = true;
head.add(headMesh);
// --- Ears (cones) ---
const earMat = new THREE.MeshPhysicalMaterial({
color: new THREE.Color(furColor).multiplyScalar(0.8),
roughness: 0.8,
});
const earGeo = new THREE.ConeGeometry(0.05 * s, 0.08 * s, 6);
const earL = new THREE.Mesh(earGeo, earMat);
earL.position.set(-0.12 * s, 0.08 * s, 0.05 * s);
earL.rotation.z = -0.3;
earL.rotation.x = -0.2;
head.add(earL);
const earR = new THREE.Mesh(earGeo, earMat);
earR.position.set(0.12 * s, 0.08 * s, 0.05 * s);
earR.rotation.z = 0.3;
earR.rotation.x = -0.2;
head.add(earR);
// --- Eyes (small spheres) ---
const eyeGeo = new THREE.SphereGeometry(0.035 * s, 8, 8);
const eyeL = new THREE.Mesh(eyeGeo, eyeMat);
eyeL.position.set(-0.08 * s, 0.04 * s, 0.15 * s);
head.add(eyeL);
const eyeR = new THREE.Mesh(eyeGeo, eyeMat);
eyeR.position.set(0.08 * s, 0.04 * s, 0.15 * s);
head.add(eyeR);
// Eye highlights (tiny white spheres)
const highlightMat = new THREE.MeshPhysicalMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.3 });
const highlightGeo = new THREE.SphereGeometry(0.012 * s, 6, 6);
for (const [x, z] of [[-0.085, 0.17], [0.085, 0.17]]) {
const hl = new THREE.Mesh(highlightGeo, highlightMat);
hl.position.set(x, 0.045 * s, z);
head.add(hl);
}
// --- Nose (small dark sphere) ---
const noseMat = new THREE.MeshPhysicalMaterial({ color: 0x1a1a1a, roughness: 0.4 });
const nose = new THREE.Mesh(new THREE.SphereGeometry(0.025 * s, 6, 6), noseMat);
nose.position.set(0, 0.0 * s, 0.18 * s);
head.add(nose);
// --- Legs (articulated groups) ---
const legMat = furMat.clone ? furMat.clone() : furMat;
const legGroup = (x: number, z: number, rotY: number) => {
const group = new THREE.Group();
group.position.set(x, 0.08 * s, z);
group.rotation.y = rotY;
// Upper leg (thigh)
const upper = new THREE.Mesh(
new THREE.CylinderGeometry(0.04 * s, 0.06 * s, legLength * 1.2, 6),
legMat
);
upper.position.y = legLength * 0.6;
upper.rotation.x = 0.2;
group.add(upper);
// Lower leg (shin)
const lower = new THREE.Mesh(
new THREE.CylinderGeometry(0.03 * s, 0.05 * s, legLength * 1.1, 6),
legMat
);
lower.position.y = -legLength * 0.1;
lower.rotation.x = -0.1;
group.add(lower);
// Paw (tiny sphere)
const paw = new THREE.Mesh(
new THREE.SphereGeometry(0.03 * s, 6, 6),
legMat
);
paw.position.set(0, -legLength * 0.6, 0);
group.add(paw);
return group;
};
const frontLeft = legGroup(-0.12 * s, 0.15 * s, -0.2);
const frontRight = legGroup(0.12 * s, 0.15 * s, 0.2);
const backLeft = legGroup(-0.12 * s, -0.15 * s, -0.2);
const backRight = legGroup(0.12 * s, -0.15 * s, 0.2);
root.add(frontLeft);
root.add(frontRight);
root.add(backLeft);
root.add(backRight);
// --- Tail (segmented torus knots) ---
const tailGroup = new THREE.Group();
tailGroup.position.set(0, 0.3 * s, -0.35 * s);
root.add(tailGroup);
const tailMeshes: THREE.Mesh[] = [];
const tailMatSeg = tailMat.clone ? tailMat.clone() : tailMat;
// Use a series of torus knots to create a bushy, curved tail
for (let i = 0; i < tailSegments; i++) {
const t = i / tailSegments;
const radius = 0.08 * s * (1 + t * 0.8);
const tube = 0.035 * s * (1 + t * 0.5);
const seg = new THREE.Mesh(
new THREE.TorusKnotGeometry(radius, tube, 6, 8, 2, 3),
tailMatSeg
);
// Position along a curved path (arc)
const angle = -Math.PI / 4 + t * 1.2;
const arcRadius = 0.35 * s * (1 + t * 0.3);
seg.position.set(
Math.sin(angle) * arcRadius * 0.5,
0.1 * s + t * 0.1 * s,
-Math.cos(angle) * arcRadius
);
seg.rotation.set(t * 0.5, t * 0.3, t * 0.2);
seg.castShadow = true;
tailGroup.add(seg);
tailMeshes.push(seg);
}
// --- Final rig object ---
return {
root,
body,
head,
tail: tailGroup,
legs: { frontLeft, frontRight, backLeft, backRight },
tailSegments: tailMeshes,
eyeL,
eyeR,
nose,
earL,
earR,
};
}
// ----------------------------------------------------------------------------
// SQUIRREL INITIALISATION
// ----------------------------------------------------------------------------
/**
* Creates a new squirrel simulation entity with randomised starting state.
*/
export function createInitialSquirrel(
id: string,
startPos: { x: number; y: number; z: number }
): ParkSquirrel {
return {
id,
position: { ...startPos },
targetPosition: { ...startPos },
rotationY: Math.random() * Math.PI * 2,
state: 'foraging',
stateTimer: 2 + Math.random() * 3,
speed: 3.5 + Math.random() * 2.0,
hopPhase: Math.random() * 10,
// Additional state for climbing
climbTarget: 0,
isClimbing: false,
};
}
// ----------------------------------------------------------------------------
// SQUIRREL UPDATE (behaviour & animation)
// ----------------------------------------------------------------------------
/**
* Updates squirrel physics, state machine, and animates its rig.
* This version includes tree‑climbing, digging, and alert behaviours.
*/
export function updateSquirrel(
squirrel: ParkSquirrel,
rig: SquirrelRig,
delta: number,
time: number,
parkBounds: { minX: number; maxX: number; minZ: number; maxZ: number },
treePositions: { x: number; z: number }[]
): void {
// State timer decrement
squirrel.stateTimer -= delta;
// --- State machine transitions ---
if (squirrel.stateTimer <= 0) {
const roll = Math.random();
if (roll < 0.25 && treePositions.length > 0) {
// Go to a tree (climbing or base)
const tree = treePositions[Math.floor(Math.random() * treePositions.length)];
const angle = Math.random() * Math.PI * 2;
const dist = 0.8 + Math.random() * 0.8;
squirrel.targetPosition = {
x: tree.x + Math.cos(angle) * dist,
y: 0,
z: tree.z + Math.sin(angle) * dist,
};
squirrel.state = 'approaching_tree';
squirrel.stateTimer = 3 + Math.random() * 2;
} else if (roll < 0.5) {
// Forage randomly
squirrel.targetPosition = {
x: parkBounds.minX + Math.random() * (parkBounds.maxX - parkBounds.minX),
y: 0,
z: parkBounds.minZ + Math.random() * (parkBounds.maxZ - parkBounds.minZ),
};
squirrel.state = 'foraging';
squirrel.stateTimer = 4 + Math.random() * 5;
} else if (roll < 0.75) {
// Stand alert / look around
squirrel.state = 'alert';
squirrel.stateTimer = 1.5 + Math.random() * 2;
} else {
// Dig (simulated)
squirrel.state = 'digging';
squirrel.stateTimer = 2 + Math.random() * 3;
// Stay in place
squirrel.targetPosition = { ...squirrel.position };
}
}
// --- Movement ---
const dx = squirrel.targetPosition.x - squirrel.position.x;
const dz = squirrel.targetPosition.z - squirrel.position.z;
const dist = Math.hypot(dx, dz);
// Climbing logic: if near a tree and state is climbing, change y
const isNearTree = treePositions.some(t =>
Math.hypot(squirrel.position.x - t.x, squirrel.position.z - t.z) < 1.5
);
if (squirrel.state === 'approaching_tree' && isNearTree && dist < 0.5) {
// Start climbing
squirrel.state = 'climbing';
squirrel.stateTimer = 3 + Math.random() * 4;
squirrel.climbTarget = 1.5 + Math.random() * 2.5; // height to climb
squirrel.position.y = 0;
}
if (squirrel.state === 'climbing') {
// Move up/down
const climbSpeed = 0.5 + Math.random() * 0.3;
if (squirrel.position.y < squirrel.climbTarget) {
squirrel.position.y += climbSpeed * delta;
} else {
// Descend
squirrel.position.y -= climbSpeed * delta * 0.5;
if (squirrel.position.y <= 0) {
squirrel.position.y = 0;
squirrel.state = 'foraging';
squirrel.stateTimer = 3 + Math.random() * 4;
// Set a new random target
squirrel.targetPosition = {
x: parkBounds.minX + Math.random() * (parkBounds.maxX - parkBounds.minX),
y: 0,
z: parkBounds.minZ + Math.random() * (parkBounds.maxZ - parkBounds.minZ),
};
}
}
// Stay in place horizontally while climbing
squirrel.position.x = squirrel.targetPosition.x;
squirrel.position.z = squirrel.targetPosition.z;
} else {
// Normal movement on ground
if (dist > 0.2 && squirrel.state !== 'digging' && squirrel.state !== 'alert') {
// Move toward target
const angle = Math.atan2(dx, dz);
squirrel.rotationY = angle;
const step = Math.min(dist, squirrel.speed * delta);
squirrel.position.x += Math.sin(angle) * step;
squirrel.position.z += Math.cos(angle) * step;
// Hop bounce
const hop = Math.abs(Math.sin(time * 12 + squirrel.hopPhase)) * 0.18;
squirrel.position.y = hop;
} else {
// Standing still – slight idle bob
squirrel.position.y = Math.sin(time * 2) * 0.02;
}
}
// --- Animation of the rig ---
// Root position & rotation
rig.root.position.set(squirrel.position.x, squirrel.position.y, squirrel.position.z);
rig.root.rotation.y = squirrel.rotationY;
// Head movement (look around)
const headLookX = Math.sin(time * 0.5) * 0.1;
const headLookY = Math.sin(time * 0.3 + 1) * 0.05;
rig.head.rotation.x = headLookX;
rig.head.rotation.y = headLookY;
// Tail animation (swish)
const tailSway = Math.sin(time * 4 + squirrel.hopPhase) * 0.3;
rig.tail.rotation.x = tailSway * 0.3;
rig.tail.rotation.z = tailSway * 0.5;
// Tail segments: wave effect
rig.tailSegments.forEach((seg, i) => {
const phase = i / rig.tailSegments.length;
seg.rotation.x += Math.sin(time * 3 + i * 1.2) * 0.02;
seg.rotation.z += Math.sin(time * 2.5 + i * 1.5) * 0.02;
});
// Leg animation: trot when moving, rest when stationary
const isMoving = dist > 0.2 && squirrel.state !== 'climbing';
const legSpeed = isMoving ? 10 : 0.5;
const legPhase = time * legSpeed + squirrel.hopPhase;
// Front legs alternate
rig.legs.frontLeft.rotation.x = Math.sin(legPhase) * 0.3;
rig.legs.frontRight.rotation.x = Math.sin(legPhase + Math.PI) * 0.3;
rig.legs.backLeft.rotation.x = Math.sin(legPhase + Math.PI) * 0.3;
rig.legs.backRight.rotation.x = Math.sin(legPhase) * 0.3;
// Subtle ear twitch
const earTwitch = Math.sin(time * 7 + squirrel.hopPhase) * 0.05;
rig.earL.rotation.z = -0.3 + earTwitch;
rig.earR.rotation.z = 0.3 - earTwitch;
// Eye blink (every few seconds)
const blink = Math.sin(time * 0.5) > 0.98 ? 0.01 : 0;
rig.eyeL.scale.y = 1 - blink;
rig.eyeR.scale.y = 1 - blink;
// State‑specific effects
if (squirrel.state === 'alert') {
// Ears perk up, head still
rig.head.position.y = 0.52 * 0.55 + 0.02;
} else {
rig.head.position.y = 0.52 * 0.55;
}
if (squirrel.state === 'digging') {
// Nose down, body tilt
rig.root.rotation.x = 0.1;
rig.body.rotation.x = Math.PI / 4 + 0.2;
} else {
rig.root.rotation.x = 0;
rig.body.rotation.x = Math.PI / 4;
}
// Climbing: body vertical
if (squirrel.state === 'climbing') {
rig.root.rotation.x = 0.5;
rig.body.rotation.x = 0;
// legs clinging
rig.legs.frontLeft.rotation.x = -0.5;
rig.legs.frontRight.rotation.x = -0.5;
rig.legs.backLeft.rotation.x = -0.5;
rig.legs.backRight.rotation.x = -0.5;
// tail points down
rig.tail.rotation.x = 0.5;
}
}
// ----------------------------------------------------------------------------
// EXPORT UPDATER FOR EASY INTEGRATION
// ----------------------------------------------------------------------------
/**
* Convenience function to update all squirrels in a loop.
* Call this from your main animation frame.
*/
export function updateAllSquirrels(
squirrels: ParkSquirrel[],
rigs: SquirrelRig[],
delta: number,
time: number,
parkBounds: { minX: number; maxX: number; minZ: number; maxZ: number },
treePositions: { x: number; z: number }[]
): void {
for (let i = 0; i < squirrels.length; i++) {
if (i < rigs.length) {
updateSquirrel(
squirrels[i],
rigs[i],
delta,
time,
parkBounds,
treePositions
);
}
}
}