Skip to content

Commit b55b8fd

Browse files
lanmowerclaude
andcommitted
refactor: xstate animation state machine, fix walk/run timescale, delete orphans
- Rewrite AnimationStateMachine.js to use xstate createMachine/createActor instead of hand-rolled state machine. All locomotion transitions now go through xstate actor with proper state validation via snap.can(). - Fix walk/run/sprint timeScale values (24.0→1.0, 4.5→1.0, 7.0→1.0) — previous values made walk animation play at absurd speed. - Delete orphaned files: BotHarness.js, draco_wasm_wrapper.js, WebRTCClient.js (zero imports across entire codebase). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent abd98b7 commit b55b8fd

5 files changed

Lines changed: 65 additions & 356 deletions

File tree

apps/world/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,9 @@ export default {
162162
},
163163
animation: {
164164
mixerTimeScale: 1.3,
165-
walkTimeScale: 24.0,
166-
jogTimeScale: 4.5,
167-
sprintTimeScale: 7.0,
165+
walkTimeScale: 1.0,
166+
jogTimeScale: 1.0,
167+
sprintTimeScale: 1.0,
168168
fadeTime: 0.15
169169
},
170170
entities: [

client/AnimationStateMachine.js

Lines changed: 62 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import * as THREE from 'three'
22

3+
const _isNode = typeof process !== 'undefined' && process.versions?.node
4+
const { createMachine, createActor } = await import(_isNode ? 'xstate' : '/node_modules/xstate/dist/xstate.esm.js')
5+
36
export const FADE_TIME = 0.15
47

58
export const STATES = {
@@ -38,6 +41,22 @@ export const LOWER_BODY_BONES = new Set([
3841
'footL', 'footR', 'toesL', 'toesR'
3942
])
4043

44+
const locoMachine = createMachine({
45+
id: 'loco',
46+
initial: 'IdleLoop',
47+
states: {
48+
IdleLoop: { on: { WALK: 'WalkLoop', JOG: 'JogFwdLoop', SPRINT: 'SprintLoop', CROUCH_IDLE: 'CrouchIdleLoop', JUMP: 'JumpLoop', DEATH: 'Death' } },
49+
WalkLoop: { on: { IDLE: 'IdleLoop', JOG: 'JogFwdLoop', SPRINT: 'SprintLoop', CROUCH_FWD: 'CrouchFwdLoop', JUMP: 'JumpLoop', DEATH: 'Death' } },
50+
JogFwdLoop: { on: { IDLE: 'IdleLoop', WALK: 'WalkLoop', SPRINT: 'SprintLoop', JUMP: 'JumpLoop', DEATH: 'Death' } },
51+
SprintLoop: { on: { IDLE: 'IdleLoop', WALK: 'WalkLoop', JOG: 'JogFwdLoop', JUMP: 'JumpLoop', DEATH: 'Death' } },
52+
CrouchIdleLoop: { on: { IDLE: 'IdleLoop', CROUCH_FWD: 'CrouchFwdLoop', JUMP: 'JumpLoop', DEATH: 'Death' } },
53+
CrouchFwdLoop: { on: { IDLE: 'IdleLoop', CROUCH_IDLE: 'CrouchIdleLoop', JUMP: 'JumpLoop', DEATH: 'Death' } },
54+
JumpLoop: { on: { IDLE: 'IdleLoop', LAND: 'JumpLand', DEATH: 'Death' } },
55+
JumpLand: { on: { IDLE: 'IdleLoop', WALK: 'WalkLoop', JOG: 'JogFwdLoop', DEATH: 'Death' } },
56+
Death: { on: { REVIVE: 'IdleLoop' } }
57+
}
58+
})
59+
4160
export function createAnimationStateMachine(mixer, root, actions, additiveActions, animConfig = {}) {
4261
const FADE = animConfig.fadeTime || FADE_TIME
4362
const LOCO_STATES = new Set(['IdleLoop', 'WalkLoop', 'JogFwdLoop', 'SprintLoop', 'CrouchIdleLoop', 'CrouchFwdLoop'])
@@ -46,6 +65,8 @@ export function createAnimationStateMachine(mixer, root, actions, additiveAction
4665
const TIMESCALE_SMOOTH = 10.0
4766
const LOCO_COOLDOWN = 0.3
4867

68+
const actor = createActor(locoMachine)
69+
actor.start()
4970
let current = null
5071
let oneShot = null
5172
let oneShotTimer = 0
@@ -67,18 +88,22 @@ export function createAnimationStateMachine(mixer, root, actions, additiveAction
6788
if (LOCO_STATES.has(name) && name !== 'IdleLoop' && name !== 'CrouchIdleLoop') locomotionCooldown = LOCO_COOLDOWN
6889
}
6990

70-
if (actions.has('IdleLoop')) {
71-
actions.get('IdleLoop').play()
72-
current = 'IdleLoop'
91+
function sendLoco(event) {
92+
const snap = actor.getSnapshot()
93+
if (snap.can({ type: event })) {
94+
actor.send({ type: event })
95+
transitionTo(actor.getSnapshot().value)
96+
}
7397
}
7498

99+
if (actions.has('IdleLoop')) { actions.get('IdleLoop').play(); current = 'IdleLoop' }
100+
75101
mixer.addEventListener('finished', () => {
76102
if (oneShot && !STATES[oneShot]?.additive) {
77103
const cfg = STATES[oneShot]
78104
if (cfg?.clamp) return
79-
oneShot = null
80-
oneShotTimer = 0
81-
if (cfg?.next) transitionTo(cfg.next)
105+
oneShot = null; oneShotTimer = 0
106+
if (cfg?.next) sendLoco(cfg.next === 'IdleLoop' ? 'IDLE' : cfg.next)
82107
}
83108
})
84109

@@ -89,65 +114,60 @@ export function createAnimationStateMachine(mixer, root, actions, additiveAction
89114
else { if (action.isRunning()) action.fadeOut(FADE) }
90115
}
91116

117+
function resolveLocoEvent(smoothSpeed, crouching, skipWalk) {
118+
if (crouching) return smoothSpeed < 0.8 ? 'CROUCH_IDLE' : 'CROUCH_FWD'
119+
if (skipWalk) {
120+
const idle2jog = current === 'IdleLoop' ? 2.0 : 0.8
121+
const jog2sprint = current === 'JogFwdLoop' ? 15.5 : 15.0
122+
if (smoothSpeed < idle2jog) return 'IDLE'
123+
if (smoothSpeed < jog2sprint) return 'JOG'
124+
return 'SPRINT'
125+
}
126+
const idle2walk = current === 'IdleLoop' ? 0.5 : 0.3
127+
const walk2jog = current === 'WalkLoop' ? 4.0 : 3.5
128+
const jog2sprint = current === 'JogFwdLoop' ? 15.5 : 15.0
129+
if (smoothSpeed < idle2walk) return 'IDLE'
130+
if (smoothSpeed < walk2jog) return 'WALK'
131+
if (smoothSpeed < jog2sprint) return 'JOG'
132+
return 'SPRINT'
133+
}
134+
92135
function update(dt, velocity, onGround, health, aiming, crouching) {
93136
if (locomotionCooldown > 0) locomotionCooldown -= dt
94137
if (oneShotTimer > 0) {
95138
oneShotTimer -= dt
96139
if (oneShotTimer <= 0) {
97140
const cfg = STATES[oneShot]
98141
oneShot = null
99-
if (cfg?.next) transitionTo(cfg.next)
142+
if (cfg?.next) sendLoco(cfg.next === 'IdleLoop' ? 'IDLE' : cfg.next)
100143
}
101144
}
102145
if (!onGround) airTime += dt; else airTime = 0
103146
const effectiveOnGround = onGround || airTime < AIR_GRACE
104147

105148
if (health <= 0 && current !== 'Death') {
106-
transitionTo('Death')
107-
oneShot = 'Death'
149+
sendLoco('DEATH'); oneShot = 'Death'
108150
} else if (health > 0 && (oneShot === 'Death' || current === 'Death')) {
109151
const deathAction = actions.get('Death')
110152
if (deathAction) { deathAction.stop(); deathAction.reset() }
111153
oneShot = null; oneShotTimer = 0; current = null
112-
transitionTo('IdleLoop')
154+
sendLoco('REVIVE')
113155
} else if (!oneShot || STATES[oneShot]?.additive) {
114156
const vx = velocity?.[0] || 0, vz = velocity?.[2] || 0
115157
const rawSpeed = Math.sqrt(vx * vx + vz * vz)
116158
smoothSpeed += (rawSpeed - smoothSpeed) * Math.min(1, SPEED_SMOOTH * dt)
117-
118-
if (!effectiveOnGround && !wasOnGround) {
119-
transitionTo('JumpLoop')
120-
} else if (!wasOnGround && effectiveOnGround && smoothSpeed < 1.5) {
121-
transitionTo('JumpLand')
122-
oneShot = 'JumpLand'
123-
oneShotTimer = STATES.JumpLand.duration
124-
} else if (effectiveOnGround) {
125-
if (crouching) {
126-
if (smoothSpeed < 0.8) transitionTo('CrouchIdleLoop'); else transitionTo('CrouchFwdLoop')
127-
} else if (animConfig.skipWalk) {
128-
const idle2jog = current === 'IdleLoop' ? 2.0 : 0.8
129-
const jog2sprint = current === 'JogFwdLoop' ? 15.5 : 15.0
130-
if (smoothSpeed < idle2jog) transitionTo('IdleLoop')
131-
else if (smoothSpeed < jog2sprint) transitionTo('JogFwdLoop')
132-
else transitionTo('SprintLoop')
133-
} else {
134-
const idle2walk = current === 'IdleLoop' ? 0.5 : 0.3
135-
const walk2jog = current === 'WalkLoop' ? 4.0 : 3.5
136-
const jog2sprint = current === 'JogFwdLoop' ? 15.5 : 15.0
137-
if (smoothSpeed < idle2walk) transitionTo('IdleLoop')
138-
else if (smoothSpeed < walk2jog) transitionTo('WalkLoop')
139-
else if (smoothSpeed < jog2sprint) transitionTo('JogFwdLoop')
140-
else transitionTo('SprintLoop')
141-
}
142-
}
159+
if (!effectiveOnGround && !wasOnGround) sendLoco('JUMP')
160+
else if (!wasOnGround && effectiveOnGround && smoothSpeed < 1.5) {
161+
sendLoco('LAND'); oneShot = 'JumpLand'; oneShotTimer = STATES.JumpLand.duration
162+
} else if (effectiveOnGround) sendLoco(resolveLocoEvent(smoothSpeed, crouching, animConfig.skipWalk))
143163
}
144164

145165
if (current && LOCO_STATES.has(current) && current !== 'IdleLoop' && current !== 'CrouchIdleLoop') {
146166
const locoAction = actions.get(current)
147167
if (locoAction) {
148-
const baseScale = current === 'WalkLoop' ? (animConfig.walkTimeScale || 16.0)
149-
: current === 'JogFwdLoop' ? (animConfig.jogTimeScale || 0.667)
150-
: current === 'SprintLoop' ? (animConfig.sprintTimeScale || 0.56) : 1.0
168+
const baseScale = current === 'WalkLoop' ? (animConfig.walkTimeScale || 1.0)
169+
: current === 'JogFwdLoop' ? (animConfig.jogTimeScale || 1.0)
170+
: current === 'SprintLoop' ? (animConfig.sprintTimeScale || 1.0) : 1.0
151171
const stateMin = current === 'WalkLoop' ? 0.3 : current === 'JogFwdLoop' ? 3.5 : current === 'SprintLoop' ? 12.0 : 0.3
152172
const stateMax = current === 'WalkLoop' ? 4.0 : current === 'JogFwdLoop' ? 15.5 : current === 'SprintLoop' ? 24.0 : 6.0
153173
const ratio = Math.max(0.5, Math.min(1.5, smoothSpeed / Math.max(1, (stateMin + stateMax) * 0.5)))
@@ -156,31 +176,25 @@ export function createAnimationStateMachine(mixer, root, actions, additiveAction
156176
locoAction.timeScale = smoothTimeScale
157177
}
158178
}
159-
160179
aim(aiming)
161180
wasOnGround = effectiveOnGround
162181
mixer.update(dt)
163182
}
164-
165183
function shoot() {
166184
const action = actions.get('PistolShoot')
167185
if (!action) return
168186
action.reset().fadeIn(0.05).play()
169187
}
170-
171188
function reload() {
172189
const action = actions.get('PistolReload')
173-
if (!action) { console.log('[anim] PistolReload animation not found'); return }
174-
console.log('[anim] Playing reload animation')
190+
if (!action) throw new Error('[anim] PistolReload animation not found')
175191
action.reset().fadeIn(0.1).play()
176192
}
177-
178193
function dispose() {
194+
actor.stop()
179195
mixer.stopAllAction()
180196
mixer.uncacheRoot(root)
181197
}
182-
183198
function getState() { return current }
184-
185199
return { transitionTo, update, aim, shoot, reload, dispose, getState }
186-
}
200+
}

client/WebRTCClient.js

Lines changed: 0 additions & 96 deletions
This file was deleted.

0 commit comments

Comments
 (0)