11import * 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+
36export const FADE_TIME = 0.15
47
58export 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+
4160export 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+ }
0 commit comments