spoint 0.1.0 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +134 -209
  2. package/SKILL.md +95 -0
  3. package/apps/environment/index.js +200 -1
  4. package/apps/environment/models/decorative/.gitkeep +0 -0
  5. package/apps/environment/models/hazards/.gitkeep +0 -0
  6. package/apps/environment/models/interactive/.gitkeep +0 -0
  7. package/apps/environment/models/structures/.gitkeep +0 -0
  8. package/apps/environment/smartObjects.js +114 -0
  9. package/apps/interactable/index.js +155 -0
  10. package/apps/physics-crate/index.js +15 -9
  11. package/apps/power-crate/index.js +18 -12
  12. package/apps/tps-game/$GDUPI.vrm +0 -0
  13. package/apps/tps-game/Cleetus.vrm +0 -0
  14. package/apps/tps-game/index.js +185 -27
  15. package/apps/world/index.js +68 -22
  16. package/bin/create-app.js +337 -0
  17. package/client/ARControls.js +301 -0
  18. package/client/LoadingManager.js +117 -0
  19. package/client/MobileControls.js +1122 -0
  20. package/client/anim-lib.glb +0 -0
  21. package/client/animation.js +306 -0
  22. package/client/app.js +1341 -65
  23. package/client/camera.js +191 -33
  24. package/client/createLoadingScreen.js +69 -0
  25. package/client/editor/bridge.js +113 -0
  26. package/client/editor/css/main.css +794 -0
  27. package/client/editor/images/rotate.svg +4 -0
  28. package/client/editor/images/scale.svg +4 -0
  29. package/client/editor/images/translate.svg +4 -0
  30. package/client/editor/index.html +103 -0
  31. package/client/editor/js/Command.js +41 -0
  32. package/client/editor/js/Config.js +81 -0
  33. package/client/editor/js/Editor.js +785 -0
  34. package/client/editor/js/EditorControls.js +438 -0
  35. package/client/editor/js/History.js +321 -0
  36. package/client/editor/js/Loader.js +987 -0
  37. package/client/editor/js/LoaderUtils.js +90 -0
  38. package/client/editor/js/Menubar.Add.js +510 -0
  39. package/client/editor/js/Menubar.Edit.js +145 -0
  40. package/client/editor/js/Menubar.File.js +466 -0
  41. package/client/editor/js/Menubar.Help.js +73 -0
  42. package/client/editor/js/Menubar.Status.js +51 -0
  43. package/client/editor/js/Menubar.View.js +183 -0
  44. package/client/editor/js/Menubar.js +27 -0
  45. package/client/editor/js/Player.js +53 -0
  46. package/client/editor/js/Resizer.js +58 -0
  47. package/client/editor/js/Script.js +503 -0
  48. package/client/editor/js/Selector.js +102 -0
  49. package/client/editor/js/Sidebar.Geometry.BoxGeometry.js +121 -0
  50. package/client/editor/js/Sidebar.Geometry.BufferGeometry.js +115 -0
  51. package/client/editor/js/Sidebar.Geometry.CapsuleGeometry.js +97 -0
  52. package/client/editor/js/Sidebar.Geometry.CircleGeometry.js +97 -0
  53. package/client/editor/js/Sidebar.Geometry.CylinderGeometry.js +121 -0
  54. package/client/editor/js/Sidebar.Geometry.DodecahedronGeometry.js +73 -0
  55. package/client/editor/js/Sidebar.Geometry.ExtrudeGeometry.js +196 -0
  56. package/client/editor/js/Sidebar.Geometry.IcosahedronGeometry.js +73 -0
  57. package/client/editor/js/Sidebar.Geometry.LatheGeometry.js +98 -0
  58. package/client/editor/js/Sidebar.Geometry.Modifiers.js +73 -0
  59. package/client/editor/js/Sidebar.Geometry.OctahedronGeometry.js +74 -0
  60. package/client/editor/js/Sidebar.Geometry.PlaneGeometry.js +97 -0
  61. package/client/editor/js/Sidebar.Geometry.RingGeometry.js +121 -0
  62. package/client/editor/js/Sidebar.Geometry.ShapeGeometry.js +76 -0
  63. package/client/editor/js/Sidebar.Geometry.SphereGeometry.js +133 -0
  64. package/client/editor/js/Sidebar.Geometry.TetrahedronGeometry.js +74 -0
  65. package/client/editor/js/Sidebar.Geometry.TorusGeometry.js +109 -0
  66. package/client/editor/js/Sidebar.Geometry.TorusKnotGeometry.js +121 -0
  67. package/client/editor/js/Sidebar.Geometry.TubeGeometry.js +135 -0
  68. package/client/editor/js/Sidebar.Geometry.js +332 -0
  69. package/client/editor/js/Sidebar.Material.BooleanProperty.js +60 -0
  70. package/client/editor/js/Sidebar.Material.ColorProperty.js +87 -0
  71. package/client/editor/js/Sidebar.Material.ConstantProperty.js +62 -0
  72. package/client/editor/js/Sidebar.Material.MapProperty.js +249 -0
  73. package/client/editor/js/Sidebar.Material.NumberProperty.js +60 -0
  74. package/client/editor/js/Sidebar.Material.Program.js +73 -0
  75. package/client/editor/js/Sidebar.Material.RangeValueProperty.js +63 -0
  76. package/client/editor/js/Sidebar.Material.js +751 -0
  77. package/client/editor/js/Sidebar.Object.Animation.js +102 -0
  78. package/client/editor/js/Sidebar.Object.js +898 -0
  79. package/client/editor/js/Sidebar.Project.App.js +165 -0
  80. package/client/editor/js/Sidebar.Project.Image.js +225 -0
  81. package/client/editor/js/Sidebar.Project.Materials.js +82 -0
  82. package/client/editor/js/Sidebar.Project.Renderer.js +144 -0
  83. package/client/editor/js/Sidebar.Project.Video.js +242 -0
  84. package/client/editor/js/Sidebar.Project.js +31 -0
  85. package/client/editor/js/Sidebar.Properties.js +73 -0
  86. package/client/editor/js/Sidebar.Scene.js +585 -0
  87. package/client/editor/js/Sidebar.Script.js +129 -0
  88. package/client/editor/js/Sidebar.Settings.History.js +146 -0
  89. package/client/editor/js/Sidebar.Settings.Shortcuts.js +175 -0
  90. package/client/editor/js/Sidebar.Settings.js +60 -0
  91. package/client/editor/js/Sidebar.js +41 -0
  92. package/client/editor/js/Storage.js +98 -0
  93. package/client/editor/js/Strings.js +2028 -0
  94. package/client/editor/js/Toolbar.js +84 -0
  95. package/client/editor/js/Viewport.Controls.js +92 -0
  96. package/client/editor/js/Viewport.Info.js +136 -0
  97. package/client/editor/js/Viewport.Pathtracer.js +91 -0
  98. package/client/editor/js/Viewport.ViewHelper.js +39 -0
  99. package/client/editor/js/Viewport.XR.js +222 -0
  100. package/client/editor/js/Viewport.js +900 -0
  101. package/client/editor/js/commands/AddObjectCommand.js +68 -0
  102. package/client/editor/js/commands/AddScriptCommand.js +75 -0
  103. package/client/editor/js/commands/Commands.js +23 -0
  104. package/client/editor/js/commands/MoveObjectCommand.js +111 -0
  105. package/client/editor/js/commands/MultiCmdsCommand.js +85 -0
  106. package/client/editor/js/commands/RemoveObjectCommand.js +88 -0
  107. package/client/editor/js/commands/RemoveScriptCommand.js +81 -0
  108. package/client/editor/js/commands/SetColorCommand.js +73 -0
  109. package/client/editor/js/commands/SetGeometryCommand.js +87 -0
  110. package/client/editor/js/commands/SetGeometryValueCommand.js +70 -0
  111. package/client/editor/js/commands/SetMaterialColorCommand.js +86 -0
  112. package/client/editor/js/commands/SetMaterialCommand.js +79 -0
  113. package/client/editor/js/commands/SetMaterialMapCommand.js +143 -0
  114. package/client/editor/js/commands/SetMaterialRangeCommand.js +91 -0
  115. package/client/editor/js/commands/SetMaterialValueCommand.js +90 -0
  116. package/client/editor/js/commands/SetMaterialVectorCommand.js +79 -0
  117. package/client/editor/js/commands/SetPositionCommand.js +84 -0
  118. package/client/editor/js/commands/SetRotationCommand.js +84 -0
  119. package/client/editor/js/commands/SetScaleCommand.js +84 -0
  120. package/client/editor/js/commands/SetSceneCommand.js +103 -0
  121. package/client/editor/js/commands/SetScriptValueCommand.js +80 -0
  122. package/client/editor/js/commands/SetShadowValueCommand.js +73 -0
  123. package/client/editor/js/commands/SetUuidCommand.js +70 -0
  124. package/client/editor/js/commands/SetValueCommand.js +75 -0
  125. package/client/editor/js/libs/acorn/acorn.js +3236 -0
  126. package/client/editor/js/libs/acorn/acorn_loose.js +1299 -0
  127. package/client/editor/js/libs/acorn/walk.js +344 -0
  128. package/client/editor/js/libs/app/index.html +57 -0
  129. package/client/editor/js/libs/app.js +251 -0
  130. package/client/editor/js/libs/codemirror/addon/dialog.css +32 -0
  131. package/client/editor/js/libs/codemirror/addon/dialog.js +163 -0
  132. package/client/editor/js/libs/codemirror/addon/show-hint.css +36 -0
  133. package/client/editor/js/libs/codemirror/addon/show-hint.js +529 -0
  134. package/client/editor/js/libs/codemirror/addon/tern.css +87 -0
  135. package/client/editor/js/libs/codemirror/addon/tern.js +750 -0
  136. package/client/editor/js/libs/codemirror/codemirror.css +344 -0
  137. package/client/editor/js/libs/codemirror/codemirror.js +9849 -0
  138. package/client/editor/js/libs/codemirror/mode/glsl.js +233 -0
  139. package/client/editor/js/libs/codemirror/mode/javascript.js +959 -0
  140. package/client/editor/js/libs/codemirror/theme/monokai.css +41 -0
  141. package/client/editor/js/libs/esprima.js +6401 -0
  142. package/client/editor/js/libs/jsonlint.js +453 -0
  143. package/client/editor/js/libs/signals.min.js +14 -0
  144. package/client/editor/js/libs/tern-threejs/threejs.js +5031 -0
  145. package/client/editor/js/libs/ternjs/comment.js +87 -0
  146. package/client/editor/js/libs/ternjs/def.js +588 -0
  147. package/client/editor/js/libs/ternjs/doc_comment.js +401 -0
  148. package/client/editor/js/libs/ternjs/infer.js +1635 -0
  149. package/client/editor/js/libs/ternjs/polyfill.js +80 -0
  150. package/client/editor/js/libs/ternjs/signal.js +26 -0
  151. package/client/editor/js/libs/ternjs/tern.js +993 -0
  152. package/client/editor/js/libs/ui.js +1346 -0
  153. package/client/editor/js/libs/ui.three.js +855 -0
  154. package/client/facial-animation.js +455 -0
  155. package/client/index.html +7 -4
  156. package/client/loading.css +147 -0
  157. package/client/loading.html +25 -0
  158. package/client/style.css +251 -0
  159. package/package.json +7 -3
  160. package/server.js +9 -1
  161. package/src/apps/AppContext.js +1 -1
  162. package/src/apps/AppLoader.js +50 -37
  163. package/src/apps/AppRuntime.js +32 -8
  164. package/src/client/InputHandler.js +233 -0
  165. package/src/client/JitterBuffer.js +207 -0
  166. package/src/client/KalmanFilter.js +125 -0
  167. package/src/client/MessageHandler.js +101 -0
  168. package/src/client/PhysicsNetworkClient.js +141 -68
  169. package/src/client/ReconnectManager.js +62 -0
  170. package/src/client/SmoothInterpolation.js +127 -0
  171. package/src/client/SnapshotProcessor.js +144 -0
  172. package/src/connection/ConnectionManager.js +21 -3
  173. package/src/connection/SessionStore.js +13 -3
  174. package/src/index.client.js +4 -6
  175. package/src/netcode/EventLog.js +29 -15
  176. package/src/netcode/LagCompensator.js +25 -26
  177. package/src/netcode/NetworkState.js +4 -1
  178. package/src/netcode/PhysicsIntegration.js +20 -6
  179. package/src/netcode/PlayerManager.js +10 -2
  180. package/src/netcode/SnapshotEncoder.js +66 -19
  181. package/src/netcode/TickSystem.js +13 -4
  182. package/src/physics/World.js +66 -13
  183. package/src/protocol/msgpack.js +90 -63
  184. package/src/sdk/ReloadHandlers.js +12 -2
  185. package/src/sdk/ReloadManager.js +5 -0
  186. package/src/sdk/ServerHandlers.js +50 -11
  187. package/src/sdk/StaticHandler.js +22 -6
  188. package/src/sdk/TickHandler.js +101 -34
  189. package/src/sdk/scaffold.js +31 -0
  190. package/src/sdk/server.js +59 -33
  191. package/src/shared/movement.js +2 -1
  192. package/src/spatial/Octree.js +5 -0
  193. package/apps/interactive-door/index.js +0 -33
  194. package/apps/patrol-npc/index.js +0 -37
  195. package/src/connection/QualityMonitor.js +0 -46
  196. package/src/debug/StateInspector.js +0 -42
  197. package/src/index.js +0 -1
  198. package/src/index.server.js +0 -27
  199. package/src/protocol/Codec.js +0 -60
  200. package/src/protocol/SequenceTracker.js +0 -71
  201. package/src/sdk/ClientMessageHandler.js +0 -80
  202. package/src/sdk/client.js +0 -122
  203. package/world/kaira.glb +0 -0
  204. package/world/schwust.glb +0 -0
Binary file
@@ -0,0 +1,306 @@
1
+ import * as THREE from 'three'
2
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
3
+
4
+ const FADE_TIME = 0.15
5
+ const STATES = {
6
+ IdleLoop: { loop: true },
7
+ WalkLoop: { loop: true },
8
+ JogFwdLoop: { loop: true },
9
+ SprintLoop: { loop: true },
10
+ JumpStart: { loop: false, next: 'JumpLoop' },
11
+ JumpLoop: { loop: true },
12
+ JumpLand: { loop: false, next: 'IdleLoop', duration: 0.4 },
13
+ CrouchIdleLoop: { loop: true },
14
+ CrouchFwdLoop: { loop: true },
15
+ Death: { loop: false, clamp: true },
16
+ PistolShoot: { loop: false, next: null, duration: 0.3, upperBody: true },
17
+ Aim: { loop: true, additive: true },
18
+ PistolReload: { loop: false, next: 'IdleLoop', duration: 2.0, upperBody: true }
19
+ }
20
+
21
+ const LOWER_BODY_BONES = new Set([
22
+ 'root', 'hips', 'pelvis',
23
+ 'leftUpperLeg', 'leftLowerLeg', 'leftFoot', 'leftToes',
24
+ 'rightUpperLeg', 'rightLowerLeg', 'rightFoot', 'rightToes',
25
+ 'LeftUpperLeg', 'LeftLowerLeg', 'LeftFoot', 'LeftToes',
26
+ 'RightUpperLeg', 'RightLowerLeg', 'RightFoot', 'RightToes',
27
+ // Additional leg bones that might appear
28
+ 'LeftUpLeg', 'LeftLeg', 'LeftFoot', 'LeftToeBase',
29
+ 'RightUpLeg', 'RightLeg', 'RightFoot', 'RightToeBase',
30
+ 'leftUpLeg', 'leftLeg', 'leftFoot', 'leftToeBase',
31
+ 'rightUpLeg', 'rightLeg', 'rightFoot', 'rightToeBase',
32
+ 'lUpLeg', 'lLeg', 'lFoot', 'lToe',
33
+ 'rUpLeg', 'rLeg', 'rFoot', 'rToe'
34
+ ])
35
+
36
+ function extractBoneName(trackName) {
37
+ const m = trackName.match(/\.bones\[([^\]]+)\]/)
38
+ if (m) return m[1]
39
+ return trackName.split('.')[0]
40
+ }
41
+
42
+ function filterUpperBodyTracks(clip) {
43
+ const filteredTracks = clip.tracks.filter(track => {
44
+ return !LOWER_BODY_BONES.has(extractBoneName(track.name))
45
+ })
46
+ return new THREE.AnimationClip(clip.name, clip.duration, filteredTracks)
47
+ }
48
+
49
+ function filterValidClipTracks(clip, targetObj) {
50
+ // Get all bone/mesh names that exist in target
51
+ const validBones = new Set()
52
+ targetObj.traverse(child => {
53
+ if (child.isBone || child.isSkinnedMesh) {
54
+ validBones.add(child.name)
55
+ }
56
+ })
57
+
58
+ const validTracks = clip.tracks.filter(track => {
59
+ const boneName = extractBoneName(track.name)
60
+ if (!validBones.has(boneName)) {
61
+ console.warn(`[anim] Filtering out track for missing bone: ${boneName}`)
62
+ return false
63
+ }
64
+ return true
65
+ })
66
+
67
+ if (validTracks.length < clip.tracks.length) {
68
+ console.log(`[anim] Filtered clip ${clip.name}: ${clip.tracks.length} → ${validTracks.length} tracks`)
69
+ return new THREE.AnimationClip(clip.name, clip.duration, validTracks)
70
+ }
71
+
72
+ return clip
73
+ }
74
+
75
+ const q1 = new THREE.Quaternion()
76
+ const restInv = new THREE.Quaternion()
77
+ const parentRest = new THREE.Quaternion()
78
+
79
+ function normalizeClips(gltf, vrmVersion, vrmHumanoid) {
80
+ const scene = gltf.scene
81
+ scene.updateMatrixWorld(true)
82
+ const clips = new Map()
83
+ for (const clip of gltf.animations) {
84
+ const name = clip.name.replace(/^VRM\|/, '').replace(/@\d+$/, '')
85
+ const tracks = []
86
+ for (const track of clip.tracks) {
87
+ const [boneName, property] = track.name.split('.')
88
+ if (property === 'scale') continue
89
+ if (property === 'position') {
90
+ if (boneName !== 'root' && boneName !== 'hips') continue
91
+ if (vrmVersion === '0') {
92
+ const newTrack = track.clone()
93
+ for (let i = 0; i < newTrack.values.length; i += 3) {
94
+ newTrack.values[i] = -newTrack.values[i]
95
+ newTrack.values[i + 2] = -newTrack.values[i + 2]
96
+ }
97
+ tracks.push(newTrack)
98
+ } else {
99
+ tracks.push(track)
100
+ }
101
+ continue
102
+ }
103
+ let bone = scene.getObjectByName(boneName)
104
+ if (!bone && vrmHumanoid) bone = vrmHumanoid.getNormalizedBoneNode(boneName)
105
+ if (!bone || !bone.parent) { tracks.push(track); continue }
106
+ if (property === 'quaternion') {
107
+ bone.getWorldQuaternion(restInv).invert()
108
+ bone.parent.getWorldQuaternion(parentRest)
109
+ const newTrack = track.clone()
110
+ for (let i = 0; i < newTrack.values.length; i += 4) {
111
+ q1.fromArray(newTrack.values, i)
112
+ q1.premultiply(parentRest).multiply(restInv)
113
+ if (vrmVersion === '0') { q1.x = -q1.x; q1.z = -q1.z }
114
+ q1.toArray(newTrack.values, i)
115
+ }
116
+ tracks.push(newTrack)
117
+ } else {
118
+ tracks.push(track)
119
+ }
120
+ }
121
+ clips.set(name, new THREE.AnimationClip(clip.name, clip.duration, tracks))
122
+ }
123
+ return clips
124
+ }
125
+
126
+ export async function loadAnimationLibrary(vrmVersion, vrmHumanoid) {
127
+ const loader = new GLTFLoader()
128
+ const gltf = await loader.loadAsync('/anim-lib.glb')
129
+ const normalizedClips = normalizeClips(gltf, vrmVersion || '1', vrmHumanoid)
130
+ console.log(`[anim] Loaded animation library (${normalizedClips.size} clips):`, [...normalizedClips.keys()])
131
+ return { normalizedClips }
132
+ }
133
+
134
+ export function createPlayerAnimator(vrm, allClips, vrmVersion, animConfig = {}) {
135
+ const FADE = animConfig.fadeTime || FADE_TIME
136
+ const root = vrm.scene
137
+ const mixer = new THREE.AnimationMixer(root)
138
+ mixer.timeScale = animConfig.mixerTimeScale || 1.3
139
+ const actions = new Map()
140
+ const additiveActions = new Map()
141
+
142
+ const clips = allClips.normalizedClips || allClips.rawClips || allClips
143
+
144
+ for (const [name, clip] of clips) {
145
+ if (!STATES[name]) continue
146
+ const cfg = STATES[name]
147
+
148
+ if (cfg.upperBody) {
149
+ console.log(`[anim] ${name} tracks:`, clip.tracks.map(t => extractBoneName(t.name)))
150
+ }
151
+
152
+ let playClip = filterValidClipTracks(clip, root)
153
+
154
+ if (cfg.upperBody) {
155
+ const upperBodyClip = filterUpperBodyTracks(playClip)
156
+ const action = mixer.clipAction(upperBodyClip)
157
+ if (!cfg.loop) {
158
+ action.loop = THREE.LoopOnce
159
+ action.clampWhenFinished = cfg.clamp || false
160
+ }
161
+ actions.set(name, action)
162
+ } else if (cfg.additive) {
163
+ const upperBodyClip = filterUpperBodyTracks(playClip)
164
+ const action = mixer.clipAction(upperBodyClip)
165
+ action.blendMode = THREE.AdditiveAnimationBlendMode
166
+ if (!cfg.loop) {
167
+ action.loop = THREE.LoopOnce
168
+ action.clampWhenFinished = cfg.clamp || false
169
+ }
170
+ additiveActions.set(name, action)
171
+ } else {
172
+ const action = mixer.clipAction(playClip)
173
+ if (!cfg.loop) {
174
+ action.loop = THREE.LoopOnce
175
+ action.clampWhenFinished = cfg.clamp || false
176
+ }
177
+ if (name === 'WalkLoop') action.timeScale = animConfig.walkTimeScale || 2.0
178
+ if (name === 'SprintLoop') action.timeScale = animConfig.sprintTimeScale || 0.56
179
+ actions.set(name, action)
180
+ }
181
+ }
182
+ let current = null
183
+ let oneShot = null
184
+ let oneShotTimer = 0
185
+ let wasOnGround = true
186
+ let airTime = 0
187
+ let smoothSpeed = 0
188
+ let locomotionCooldown = 0
189
+ const AIR_GRACE = 0.15
190
+ const SPEED_SMOOTH = 8.0
191
+ const LOCO_COOLDOWN = 0.3
192
+ const LOCO_STATES = new Set(['IdleLoop', 'WalkLoop', 'JogFwdLoop', 'SprintLoop', 'CrouchIdleLoop', 'CrouchFwdLoop'])
193
+
194
+ function transitionTo(name) {
195
+ if (current === name) return
196
+ if (LOCO_STATES.has(name) && LOCO_STATES.has(current) && locomotionCooldown > 0) return
197
+ const prev = actions.get(current)
198
+ const next = actions.get(name)
199
+ if (!next) return
200
+ if (prev) prev.fadeOut(FADE)
201
+ next.reset().fadeIn(FADE).play()
202
+ current = name
203
+ if (LOCO_STATES.has(name)) locomotionCooldown = LOCO_COOLDOWN
204
+ }
205
+
206
+ if (actions.has('IdleLoop')) {
207
+ actions.get('IdleLoop').play()
208
+ current = 'IdleLoop'
209
+ }
210
+
211
+ mixer.addEventListener('finished', () => {
212
+ if (oneShot && !STATES[oneShot]?.additive) {
213
+ const cfg = STATES[oneShot]
214
+ if (cfg?.clamp) return
215
+ oneShot = null
216
+ oneShotTimer = 0
217
+ if (cfg?.next) transitionTo(cfg.next)
218
+ }
219
+ })
220
+
221
+ return {
222
+ update(dt, velocity, onGround, health, aiming, crouching) {
223
+ if (locomotionCooldown > 0) locomotionCooldown -= dt
224
+ if (oneShotTimer > 0) {
225
+ oneShotTimer -= dt
226
+ if (oneShotTimer <= 0) {
227
+ const cfg = STATES[oneShot]
228
+ oneShot = null
229
+ if (cfg?.next) transitionTo(cfg.next)
230
+ }
231
+ }
232
+
233
+ if (!onGround) airTime += dt
234
+ else airTime = 0
235
+ const effectiveOnGround = onGround || airTime < AIR_GRACE
236
+
237
+ if (health <= 0 && current !== 'Death') {
238
+ transitionTo('Death')
239
+ oneShot = 'Death'
240
+ } else if (health > 0 && (oneShot === 'Death' || current === 'Death')) {
241
+ const deathAction = actions.get('Death')
242
+ if (deathAction) { deathAction.stop(); deathAction.reset() }
243
+ oneShot = null
244
+ oneShotTimer = 0
245
+ current = null
246
+ transitionTo('IdleLoop')
247
+ } else if (!oneShot || STATES[oneShot]?.additive) {
248
+ const vx = velocity?.[0] || 0, vz = velocity?.[2] || 0
249
+ const rawSpeed = Math.sqrt(vx * vx + vz * vz)
250
+ smoothSpeed += (rawSpeed - smoothSpeed) * Math.min(1, SPEED_SMOOTH * dt)
251
+
252
+ if (!effectiveOnGround && !wasOnGround) {
253
+ transitionTo('JumpLoop')
254
+ } else if (!wasOnGround && effectiveOnGround && smoothSpeed < 1.5) {
255
+ transitionTo('JumpLand')
256
+ oneShot = 'JumpLand'
257
+ oneShotTimer = STATES.JumpLand.duration
258
+ } else if (effectiveOnGround) {
259
+ if (crouching) {
260
+ if (smoothSpeed < 0.8) transitionTo('CrouchIdleLoop')
261
+ else transitionTo('CrouchFwdLoop')
262
+ } else {
263
+ const idle2walk = current === 'IdleLoop' ? 0.8 : 0.3
264
+ const walk2jog = current === 'WalkLoop' ? 5.0 : 4.5
265
+ const jog2sprint = current === 'JogFwdLoop' ? 6.0 : 5.5
266
+ if (smoothSpeed < idle2walk) transitionTo('IdleLoop')
267
+ else if (smoothSpeed < walk2jog) transitionTo('WalkLoop')
268
+ else if (smoothSpeed < jog2sprint) transitionTo('JogFwdLoop')
269
+ else transitionTo('SprintLoop')
270
+ }
271
+ }
272
+ }
273
+
274
+ this.aim(aiming)
275
+ wasOnGround = effectiveOnGround
276
+ mixer.update(dt)
277
+ },
278
+ shoot() {
279
+ const action = actions.get('PistolShoot')
280
+ if (!action) return
281
+ action.reset().fadeIn(0.05).play()
282
+ },
283
+ aim(active) {
284
+ const action = additiveActions.get('Aim')
285
+ if (!action) return
286
+ if (active) {
287
+ if (!action.isRunning()) action.fadeIn(FADE).play()
288
+ } else {
289
+ if (action.isRunning()) action.fadeOut(FADE)
290
+ }
291
+ },
292
+ reload() {
293
+ const action = actions.get('PistolReload')
294
+ if (!action) {
295
+ console.log('[anim] PistolReload animation not found')
296
+ return
297
+ }
298
+ console.log('[anim] Playing reload animation')
299
+ action.reset().fadeIn(0.1).play()
300
+ },
301
+ dispose() {
302
+ mixer.stopAllAction()
303
+ mixer.uncacheRoot(root)
304
+ }
305
+ }
306
+ }