1+ import * as THREE from 'three'
2+
3+ const _isNode = typeof process !== 'undefined' && process . versions ?. node
4+ const { createMachine, createActor } = await import ( _isNode ? 'xstate' : '/spoint/node_modules/xstate/dist/xstate.esm.js' )
5+
6+ export const FADE_TIME = 0.15
7+
8+ export const STATES = {
9+ IdleLoop : { loop : true } ,
10+ WalkLoop : { loop : true } ,
11+ JogFwdLoop : { loop : true } ,
12+ SprintLoop : { loop : true } ,
13+ JumpStart : { loop : false , next : 'JumpLoop' } ,
14+ JumpLoop : { loop : true } ,
15+ JumpLand : { loop : false , next : 'IdleLoop' , duration : 0.4 } ,
16+ CrouchIdleLoop : { loop : true } ,
17+ CrouchFwdLoop : { loop : true } ,
18+ Death : { loop : false , clamp : true } ,
19+ PistolShoot : { loop : false , next : null , duration : 0.3 , upperBody : true } ,
20+ Aim : { loop : true , additive : true } ,
21+ PistolReload : { loop : false , next : 'IdleLoop' , duration : 2.0 , upperBody : true }
22+ }
23+
24+ export const LOWER_BODY_BONES = new Set ( [
25+ 'root' , 'hips' , 'pelvis' ,
26+ 'leftUpperLeg' , 'leftLowerLeg' , 'leftFoot' , 'leftToes' ,
27+ 'rightUpperLeg' , 'rightLowerLeg' , 'rightFoot' , 'rightToes' ,
28+ 'LeftUpperLeg' , 'LeftLowerLeg' , 'LeftFoot' , 'LeftToes' ,
29+ 'RightUpperLeg' , 'RightLowerLeg' , 'RightFoot' , 'RightToes' ,
30+ 'LeftUpLeg' , 'LeftLeg' , 'LeftFoot' , 'LeftToeBase' ,
31+ 'RightUpLeg' , 'RightLeg' , 'RightFoot' , 'RightToeBase' ,
32+ 'leftUpLeg' , 'leftLeg' , 'leftFoot' , 'leftToeBase' ,
33+ 'rightUpLeg' , 'rightLeg' , 'rightFoot' , 'rightToeBase' ,
34+ 'lUpLeg' , 'lLeg' , 'lFoot' , 'lToe' ,
35+ 'rUpLeg' , 'rLeg' , 'rFoot' , 'rToe' ,
36+ 'Normalized_hips' , 'Normalized_upper_legL' , 'Normalized_upper_legR' ,
37+ 'Normalized_lower_legL' , 'Normalized_lower_legR' ,
38+ 'Normalized_footL' , 'Normalized_footR' ,
39+ 'Normalized_toesL' , 'Normalized_toesR' ,
40+ 'upper_legL' , 'upper_legR' , 'lower_legL' , 'lower_legR' ,
41+ 'footL' , 'footR' , 'toesL' , 'toesR'
42+ ] )
43+
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+
60+ export function createAnimationStateMachine ( mixer , root , actions , additiveActions , animConfig = { } ) {
61+ const FADE = animConfig . fadeTime || FADE_TIME
62+ const LOCO_STATES = new Set ( [ 'IdleLoop' , 'WalkLoop' , 'JogFwdLoop' , 'SprintLoop' , 'CrouchIdleLoop' , 'CrouchFwdLoop' ] )
63+ const AIR_GRACE = 0.15
64+ const SPEED_SMOOTH = 8.0
65+ const TIMESCALE_SMOOTH = 10.0
66+ const LOCO_COOLDOWN = 0.3
67+
68+ const actor = createActor ( locoMachine )
69+ actor . start ( )
70+ let current = null
71+ let oneShot = null
72+ let oneShotTimer = 0
73+ let wasOnGround = true
74+ let airTime = 0
75+ let smoothSpeed = 0
76+ let smoothTimeScale = 1.0
77+ let locomotionCooldown = 0
78+
79+ function transitionTo ( name ) {
80+ if ( current === name ) return
81+ if ( name !== 'IdleLoop' && name !== 'CrouchIdleLoop' && LOCO_STATES . has ( name ) && LOCO_STATES . has ( current ) && locomotionCooldown > 0 ) return
82+ const prev = actions . get ( current )
83+ const next = actions . get ( name )
84+ if ( ! next ) return
85+ if ( prev ) prev . fadeOut ( FADE )
86+ next . reset ( ) . fadeIn ( FADE ) . play ( )
87+ current = name
88+ if ( LOCO_STATES . has ( name ) && name !== 'IdleLoop' && name !== 'CrouchIdleLoop' ) locomotionCooldown = LOCO_COOLDOWN
89+ }
90+
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+ }
97+ }
98+
99+ if ( actions . has ( 'IdleLoop' ) ) { actions . get ( 'IdleLoop' ) . play ( ) ; current = 'IdleLoop' }
100+
101+ mixer . addEventListener ( 'finished' , ( ) => {
102+ if ( oneShot && ! STATES [ oneShot ] ?. additive ) {
103+ const cfg = STATES [ oneShot ]
104+ if ( cfg ?. clamp ) return
105+ oneShot = null ; oneShotTimer = 0
106+ if ( cfg ?. next ) sendLoco ( cfg . next === 'IdleLoop' ? 'IDLE' : cfg . next )
107+ }
108+ } )
109+
110+ function aim ( active ) {
111+ const action = additiveActions . get ( 'Aim' )
112+ if ( ! action ) return
113+ if ( active ) { if ( ! action . isRunning ( ) ) action . fadeIn ( FADE ) . play ( ) }
114+ else { if ( action . isRunning ( ) ) action . fadeOut ( FADE ) }
115+ }
116+
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+
135+ function update ( dt , velocity , onGround , health , aiming , crouching ) {
136+ if ( locomotionCooldown > 0 ) locomotionCooldown -= dt
137+ if ( oneShotTimer > 0 ) {
138+ oneShotTimer -= dt
139+ if ( oneShotTimer <= 0 ) {
140+ const cfg = STATES [ oneShot ]
141+ oneShot = null
142+ if ( cfg ?. next ) sendLoco ( cfg . next === 'IdleLoop' ? 'IDLE' : cfg . next )
143+ }
144+ }
145+ if ( ! onGround ) airTime += dt ; else airTime = 0
146+ const effectiveOnGround = onGround || airTime < AIR_GRACE
147+
148+ if ( health <= 0 && current !== 'Death' ) {
149+ sendLoco ( 'DEATH' ) ; oneShot = 'Death'
150+ } else if ( health > 0 && ( oneShot === 'Death' || current === 'Death' ) ) {
151+ const deathAction = actions . get ( 'Death' )
152+ if ( deathAction ) { deathAction . stop ( ) ; deathAction . reset ( ) }
153+ oneShot = null ; oneShotTimer = 0 ; current = null
154+ sendLoco ( 'REVIVE' )
155+ } else if ( ! oneShot || STATES [ oneShot ] ?. additive ) {
156+ const vx = velocity ?. [ 0 ] || 0 , vz = velocity ?. [ 2 ] || 0
157+ const rawSpeed = Math . sqrt ( vx * vx + vz * vz )
158+ smoothSpeed += ( rawSpeed - smoothSpeed ) * Math . min ( 1 , SPEED_SMOOTH * dt )
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 ) )
163+ }
164+
165+ if ( current && LOCO_STATES . has ( current ) && current !== 'IdleLoop' && current !== 'CrouchIdleLoop' ) {
166+ const locoAction = actions . get ( current )
167+ if ( locoAction ) {
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
171+ const stateMin = current === 'WalkLoop' ? 0.3 : current === 'JogFwdLoop' ? 3.5 : current === 'SprintLoop' ? 12.0 : 0.3
172+ const stateMax = current === 'WalkLoop' ? 4.0 : current === 'JogFwdLoop' ? 15.5 : current === 'SprintLoop' ? 24.0 : 6.0
173+ const ratio = Math . max ( 0.5 , Math . min ( 1.5 , smoothSpeed / Math . max ( 1 , ( stateMin + stateMax ) * 0.5 ) ) )
174+ const target = baseScale * ratio
175+ smoothTimeScale += ( target - smoothTimeScale ) * Math . min ( 1 , TIMESCALE_SMOOTH * dt )
176+ locoAction . timeScale = smoothTimeScale
177+ }
178+ }
179+ aim ( aiming )
180+ wasOnGround = effectiveOnGround
181+ mixer . update ( dt )
182+ }
183+ function shoot ( ) {
184+ const action = actions . get ( 'PistolShoot' )
185+ if ( ! action ) return
186+ action . reset ( ) . fadeIn ( 0.05 ) . play ( )
187+ }
188+ function reload ( ) {
189+ const action = actions . get ( 'PistolReload' )
190+ if ( ! action ) throw new Error ( '[anim] PistolReload animation not found' )
191+ action . reset ( ) . fadeIn ( 0.1 ) . play ( )
192+ }
193+ function dispose ( ) {
194+ actor . stop ( )
195+ mixer . stopAllAction ( )
196+ mixer . uncacheRoot ( root )
197+ }
198+ function getState ( ) { return current }
199+ return { transitionTo, update, aim, shoot, reload, dispose, getState }
200+ }
0 commit comments