spoint 0.1.0 → 0.1.10

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 +28 -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
package/client/app.js CHANGED
@@ -1,30 +1,655 @@
1
1
  import * as THREE from 'three'
2
2
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
3
+ import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm'
3
4
  import { PhysicsNetworkClient, InputHandler, MSG } from '/src/index.client.js'
4
5
  import { createElement, applyDiff } from 'webjsx'
5
6
  import { createCameraController } from './camera.js'
7
+ import { loadAnimationLibrary, createPlayerAnimator } from './animation.js'
8
+ import { initFacialSystem } from './facial-animation.js'
9
+ import { VRButton } from 'three/addons/webxr/VRButton.js'
10
+ import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'
11
+ import { XRHandModelFactory } from 'three/addons/webxr/XRHandModelFactory.js'
12
+ import { LoadingManager } from './LoadingManager.js'
13
+ import { createLoadingScreen } from './createLoadingScreen.js'
14
+ import { MobileControls, detectDevice } from './MobileControls.js'
15
+ import { ARControls, createARButton } from './ARControls.js'
16
+
17
+ const loadingMgr = new LoadingManager()
18
+ const loadingScreen = createLoadingScreen(loadingMgr)
19
+ loadingMgr.setStage('CONNECTING')
6
20
 
7
21
  const scene = new THREE.Scene()
8
22
  scene.background = new THREE.Color(0x87ceeb)
9
23
  scene.fog = new THREE.Fog(0x87ceeb, 80, 200)
10
- const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 500)
11
- const renderer = new THREE.WebGLRenderer({ antialias: true })
24
+ const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.05, 500)
25
+ let worldConfig = {}
26
+ let inputConfig = { pointerLock: true }
27
+ const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
28
+ const renderer = new THREE.WebGLRenderer({ antialias: !isMobileDevice, powerPreference: 'high-performance' })
12
29
  renderer.setSize(window.innerWidth, window.innerHeight)
13
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
30
+ renderer.setPixelRatio(isMobileDevice ? Math.min(window.devicePixelRatio, 2) : window.devicePixelRatio)
14
31
  renderer.shadowMap.enabled = true
15
32
  renderer.shadowMap.type = THREE.PCFSoftShadowMap
33
+ renderer.xr.enabled = true
16
34
  document.body.appendChild(renderer.domElement)
17
35
 
18
- scene.add(new THREE.AmbientLight(0xffffff, 0.6))
19
- const sun = new THREE.DirectionalLight(0xffffff, 1.0)
20
- sun.position.set(30, 50, 20)
36
+ async function initVRButton() {
37
+ if (navigator.xr && await navigator.xr.isSessionSupported('immersive-vr')) {
38
+ document.body.appendChild(VRButton.createButton(renderer, { optionalFeatures: ['hand-tracking'] }))
39
+ }
40
+ }
41
+ initVRButton()
42
+
43
+ const controllerModels = new Map()
44
+ const controllerGrips = new Map()
45
+ const laserPointers = new Map()
46
+ const controllerModelFactory = new XRControllerModelFactory()
47
+
48
+ const handModels = new Map()
49
+ const handRays = new Map()
50
+ const handModelFactory = new XRHandModelFactory()
51
+ let handsDetected = false
52
+
53
+ let wristUI = null
54
+ let wristUICanvas = null
55
+ let wristUIContext = null
56
+
57
+ let vrSettingsPanel = null
58
+ let vrSettings = {
59
+ snapTurnAngle: 30,
60
+ smoothTurnSpeed: 0,
61
+ vignetteEnabled: false,
62
+ playerHeight: 1.6,
63
+ teleportEnabled: false
64
+ }
65
+
66
+ let teleportArc = null
67
+ let teleportMarker = null
68
+ let teleportTarget = null
69
+ let isTeleporting = false
70
+ let xrBaseReferenceSpace = null
71
+ const ARC_SEGMENTS = 20
72
+ const ARC_GRAVITY = -9.8
73
+ const ARC_VELOCITY = 8
74
+
75
+ let fadeQuad = null
76
+ let fadeOpacity = 0
77
+ let fadeState = 'none'
78
+ const FADE_SPEED = 5
79
+ const FADE_DELAY = 50
80
+
81
+ let vignetteMesh = null
82
+ let vignetteOpacity = 0
83
+ let vignetteTargetOpacity = 0
84
+
85
+ let mobileControls = null
86
+ let arControls = null
87
+ let arButton = null
88
+ let arEnabled = false
89
+ const deviceInfo = detectDevice()
90
+
91
+ mobileControls = new MobileControls({
92
+ joystickRadius: 45,
93
+ rotationSensitivity: 0.003,
94
+ zoomSensitivity: 0.008
95
+ })
96
+ inputConfig.pointerLock = !deviceInfo.isMobile
97
+ console.log('[Mobile] Touch controls initialized:', deviceInfo)
98
+
99
+ arControls = new ARControls({ placementMode: true, planeDetection: true })
100
+ const arReticle = arControls.createReticle()
101
+ scene.add(arReticle)
102
+
103
+ function createLaserPointer() {
104
+ const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -5)])
105
+ const material = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.6 })
106
+ const line = new THREE.Line(geometry, material)
107
+ line.name = 'laserPointer'
108
+ return line
109
+ }
110
+
111
+ function createTeleportArc() {
112
+ const geometry = new THREE.BufferGeometry()
113
+ const positions = new Float32Array(ARC_SEGMENTS * 3)
114
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
115
+ const material = new THREE.LineBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.6 })
116
+ const line = new THREE.Line(geometry, material)
117
+ line.name = 'teleportArc'
118
+ line.visible = false
119
+ return line
120
+ }
121
+
122
+ function createTeleportMarker() {
123
+ const geometry = new THREE.RingGeometry(0.3, 0.4, 32)
124
+ const material = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.5, side: THREE.DoubleSide })
125
+ const mesh = new THREE.Mesh(geometry, material)
126
+ mesh.name = 'teleportMarker'
127
+ mesh.rotation.x = -Math.PI / 2
128
+ mesh.visible = false
129
+ return mesh
130
+ }
131
+
132
+ function createHandRay() {
133
+ const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -2)])
134
+ const material = new THREE.LineBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.5 })
135
+ const line = new THREE.Line(geometry, material)
136
+ line.name = 'handRay'
137
+ return line
138
+ }
139
+
140
+ function setupControllers() {
141
+ for (const handedness of [0, 1]) {
142
+ const controller = renderer.xr.getController(handedness)
143
+ const grip = renderer.xr.getControllerGrip(handedness)
144
+ const laser = createLaserPointer()
145
+ controller.add(laser)
146
+ laserPointers.set(handedness, laser)
147
+
148
+ const model = controllerModelFactory.createControllerModel(grip)
149
+ grip.add(model)
150
+ controllerModels.set(handedness, model)
151
+ controllerGrips.set(handedness, grip)
152
+
153
+ scene.add(controller)
154
+ scene.add(grip)
155
+
156
+ controller.visible = false
157
+ grip.visible = false
158
+ }
159
+ teleportArc = createTeleportArc()
160
+ scene.add(teleportArc)
161
+ teleportMarker = createTeleportMarker()
162
+ scene.add(teleportMarker)
163
+ }
164
+
165
+ function createWristUI() {
166
+ const canvas = document.createElement('canvas')
167
+ canvas.width = 256
168
+ canvas.height = 128
169
+ const ctx = canvas.getContext('2d')
170
+
171
+ const texture = new THREE.CanvasTexture(canvas)
172
+ const geometry = new THREE.PlaneGeometry(0.12, 0.06)
173
+ const material = new THREE.MeshBasicMaterial({
174
+ map: texture,
175
+ transparent: true,
176
+ opacity: 0.9,
177
+ side: THREE.DoubleSide
178
+ })
179
+ const mesh = new THREE.Mesh(geometry, material)
180
+ mesh.name = 'wristUI'
181
+
182
+ return { mesh, canvas, ctx, texture }
183
+ }
184
+
185
+ function updateWristUI(health, ammo, reloading) {
186
+ if (!wristUIContext) return
187
+
188
+ const ctx = wristUIContext
189
+ const canvas = wristUICanvas
190
+
191
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
192
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
193
+
194
+ ctx.strokeStyle = '#00ffff'
195
+ ctx.lineWidth = 4
196
+ ctx.strokeRect(0, 0, canvas.width, canvas.height)
197
+
198
+ ctx.font = 'bold 36px monospace'
199
+ ctx.textAlign = 'left'
200
+ ctx.fillStyle = health > 60 ? '#00ff00' : health > 30 ? '#ffff00' : '#ff0000'
201
+ ctx.fillText(`HP ${Math.round(health)}`, 10, 45)
202
+
203
+ ctx.textAlign = 'right'
204
+ ctx.fillStyle = reloading ? '#ffff00' : '#00ffff'
205
+ ctx.fillText(reloading ? 'RELOAD' : `${ammo}/30`, 246, 45)
206
+
207
+ ctx.font = '24px monospace'
208
+ ctx.textAlign = 'center'
209
+ ctx.fillStyle = '#ffffff'
210
+ ctx.fillText('SPAWNPOINT VR', 128, 100)
211
+
212
+ wristUI.texture.needsUpdate = true
213
+ }
214
+
215
+ function createVRSettingsPanel() {
216
+ const canvas = document.createElement('canvas')
217
+ canvas.width = 512
218
+ canvas.height = 512
219
+ const ctx = canvas.getContext('2d')
220
+
221
+ const texture = new THREE.CanvasTexture(canvas)
222
+ const geometry = new THREE.PlaneGeometry(0.5, 0.5)
223
+ const material = new THREE.MeshBasicMaterial({
224
+ map: texture,
225
+ transparent: true,
226
+ opacity: 0.95,
227
+ side: THREE.DoubleSide
228
+ })
229
+ const mesh = new THREE.Mesh(geometry, material)
230
+ mesh.name = 'vrSettingsPanel'
231
+ mesh.visible = false
232
+ mesh.position.set(0, 0, -0.6)
233
+
234
+ return { mesh, canvas, ctx, texture, visible: false }
235
+ }
236
+
237
+ function updateVRSettingsPanel() {
238
+ if (!vrSettingsPanel) return
239
+
240
+ const ctx = vrSettingsPanel.ctx
241
+ const canvas = vrSettingsPanel.canvas
242
+
243
+ ctx.fillStyle = 'rgba(20, 20, 40, 0.95)'
244
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
245
+
246
+ ctx.strokeStyle = '#00ffff'
247
+ ctx.lineWidth = 4
248
+ ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20)
249
+
250
+ ctx.font = 'bold 32px sans-serif'
251
+ ctx.fillStyle = '#00ffff'
252
+ ctx.textAlign = 'center'
253
+ ctx.fillText('VR SETTINGS', 256, 50)
254
+
255
+ ctx.font = '24px sans-serif'
256
+ ctx.textAlign = 'left'
257
+ ctx.fillStyle = '#ffffff'
258
+
259
+ ctx.fillText(`Snap Turn: ${vrSettings.snapTurnAngle}°`, 40, 120)
260
+ ctx.fillText('[B/Y] to cycle', 280, 120)
261
+
262
+ ctx.fillText(`Smooth Turn: ${vrSettings.smoothTurnSpeed === 0 ? 'OFF' : vrSettings.smoothTurnSpeed.toFixed(1)}`, 40, 180)
263
+ ctx.fillText('[X/A] to cycle', 280, 180)
264
+
265
+ ctx.fillText(`Vignette: ${vrSettings.vignetteEnabled ? 'ON' : 'OFF'}`, 40, 240)
266
+ ctx.fillText('[Grip] to toggle', 280, 240)
267
+
268
+ ctx.fillText(`Height: ${vrSettings.playerHeight.toFixed(2)}m`, 40, 300)
269
+ ctx.fillText('[Menu] adjust', 280, 300)
270
+
271
+ ctx.fillStyle = vrSettings.teleportEnabled ? '#00ff00' : '#ff0000'
272
+ ctx.fillText(`Teleport: ${vrSettings.teleportEnabled ? 'ON' : 'OFF'}`, 40, 360)
273
+ ctx.fillStyle = '#ffffff'
274
+ ctx.fillText('[Trigger] toggle', 280, 360)
275
+
276
+ ctx.fillStyle = '#888888'
277
+ ctx.font = '20px sans-serif'
278
+ ctx.textAlign = 'center'
279
+ ctx.fillText('Press [Menu] button to close', 256, 480)
280
+
281
+ vrSettingsPanel.texture.needsUpdate = true
282
+ }
283
+
284
+ function toggleVRSettings() {
285
+ if (!vrSettingsPanel) {
286
+ vrSettingsPanel = createVRSettingsPanel()
287
+ camera.add(vrSettingsPanel.mesh)
288
+ }
289
+ vrSettingsPanel.visible = !vrSettingsPanel.visible
290
+ vrSettingsPanel.mesh.visible = vrSettingsPanel.visible
291
+ if (vrSettingsPanel.visible) updateVRSettingsPanel()
292
+ }
293
+
294
+ function setupHands() {
295
+ for (const handedness of [0, 1]) {
296
+ const hand = renderer.xr.getHand(handedness)
297
+ const handModel = handModelFactory.createHandModel(hand)
298
+ hand.add(handModel)
299
+ handModels.set(handedness, { hand, model: handModel })
300
+
301
+ const ray = createHandRay()
302
+ hand.add(ray)
303
+ handRays.set(handedness, ray)
304
+
305
+ if (handedness === 0 && !wristUI) {
306
+ wristUI = createWristUI()
307
+ wristUICanvas = wristUI.canvas
308
+ wristUIContext = wristUI.ctx
309
+ wristUI.mesh.position.set(0, -0.05, 0.08)
310
+ wristUI.mesh.rotation.x = -Math.PI / 3
311
+ hand.add(wristUI.mesh)
312
+ }
313
+
314
+ scene.add(hand)
315
+ hand.visible = false
316
+ }
317
+ }
318
+
319
+ function detectHandGestures(hand, handedness) {
320
+ const joints = hand.joints
321
+ if (!joints) return { pinch: false, grab: false, point: false }
322
+
323
+ const thumbTip = joints['thumb-tip']
324
+ const indexTip = joints['index-finger-tip']
325
+ const middleTip = joints['middle-finger-tip']
326
+ const ringTip = joints['ring-finger-tip']
327
+ const pinkyTip = joints['pinky-finger-tip']
328
+ const indexMcp = joints['index-finger-metacarpal']
329
+ const wrist = joints['wrist']
330
+
331
+ if (!thumbTip || !indexTip || !wrist) return { pinch: false, grab: false, point: false }
332
+
333
+ const pinchDist = thumbTip.position.distanceTo(indexTip.position)
334
+ const pinch = pinchDist < 0.02
335
+
336
+ let grab = false
337
+ if (middleTip && ringTip && pinkyTip) {
338
+ const palmDist = wrist.position.distanceTo(middleTip.position)
339
+ const tipsToPalm = [middleTip, ringTip, pinkyTip].every(tip => wrist.position.distanceTo(tip.position) < palmDist * 0.7)
340
+ grab = tipsToPalm
341
+ }
342
+
343
+ let point = false
344
+ if (indexMcp && middleTip && ringTip && pinkyTip) {
345
+ const indexExtended = indexTip.position.distanceTo(wrist.position) > indexMcp.position.distanceTo(wrist.position) * 1.5
346
+ const othersCurled = [middleTip, ringTip, pinkyTip].every(tip => wrist.position.distanceTo(tip.position) < 0.08)
347
+ point = indexExtended && othersCurled
348
+ }
349
+
350
+ return { pinch, grab, point, pinchDist }
351
+ }
352
+
353
+ function updateControllerVisibility() {
354
+ const inVR = renderer.xr.isPresenting
355
+ const session = renderer.xr.getSession()
356
+ let hasHands = false
357
+
358
+ if (session) {
359
+ for (const source of session.inputSources) {
360
+ if (source.hand) {
361
+ hasHands = true
362
+ break
363
+ }
364
+ }
365
+ }
366
+
367
+ handsDetected = hasHands
368
+
369
+ for (const handedness of [0, 1]) {
370
+ const grip = controllerGrips.get(handedness)
371
+ const controller = renderer.xr.getController(handedness)
372
+ const handData = handModels.get(handedness)
373
+
374
+ if (grip) grip.visible = inVR && !handsDetected
375
+ if (controller) controller.visible = inVR && !handsDetected
376
+ if (handData) handData.hand.visible = inVR && handsDetected
377
+ }
378
+ }
379
+
380
+ function updateTeleportArc() {
381
+ if (!renderer.xr.isPresenting || !teleportArc || !teleportMarker || !vrSettings.teleportEnabled) {
382
+ if (teleportArc) teleportArc.visible = false
383
+ if (teleportMarker) teleportMarker.visible = false
384
+ return
385
+ }
386
+ const session = renderer.xr.getSession()
387
+ if (!session) return
388
+
389
+ let origin = null
390
+ let direction = null
391
+ let triggerTeleport = false
392
+
393
+ if (handsDetected) {
394
+ const leftHand = handModels.get(0)
395
+ if (leftHand) {
396
+ const gestures = detectHandGestures(leftHand.hand, 0)
397
+ const joints = leftHand.hand.joints
398
+ if (joints && joints['index-finger-tip']) {
399
+ joints['index-finger-tip'].getWorldPosition(_tmpOrigin)
400
+ joints['index-finger-tip'].getWorldDirection(_tmpDir)
401
+ origin = _tmpOrigin.clone()
402
+ direction = _tmpDir.clone().multiplyScalar(-1)
403
+ triggerTeleport = gestures.pinch
404
+ }
405
+ }
406
+ } else {
407
+ let leftPressed = false
408
+ let rightPressed = false
409
+ let leftController = null
410
+ for (const source of session.inputSources) {
411
+ if (source.handedness === 'left' && source.gamepad) {
412
+ leftController = renderer.xr.getController(0)
413
+ leftPressed = source.gamepad.buttons[0]?.pressed
414
+ }
415
+ if (source.handedness === 'right' && source.gamepad) {
416
+ rightPressed = source.gamepad.buttons[1]?.pressed
417
+ }
418
+ }
419
+ if (leftController && (leftPressed || rightPressed)) {
420
+ leftController.getWorldPosition(_tmpOrigin)
421
+ leftController.getWorldDirection(_tmpDir).multiplyScalar(-1)
422
+ origin = _tmpOrigin.clone()
423
+ direction = _tmpDir.clone()
424
+ triggerTeleport = rightPressed
425
+ }
426
+ }
427
+
428
+ if (!origin || !direction) {
429
+ teleportArc.visible = false
430
+ teleportMarker.visible = false
431
+ teleportTarget = null
432
+ return
433
+ }
434
+
435
+ const hit = computeParabolicArc(origin, direction, ARC_VELOCITY, ARC_GRAVITY)
436
+ if (hit && hit.valid) {
437
+ teleportTarget = hit.point
438
+ teleportMarker.position.set(hit.point.x, hit.point.y + 0.02, hit.point.z)
439
+ teleportMarker.material.color.setHex(0x00ff00)
440
+ teleportMarker.visible = true
441
+ if (triggerTeleport && !isTeleporting) {
442
+ executeTeleport(teleportTarget)
443
+ }
444
+ } else {
445
+ teleportTarget = null
446
+ teleportMarker.visible = false
447
+ }
448
+ }
449
+
450
+ const _tmpOrigin = new THREE.Vector3()
451
+ const _tmpDir = new THREE.Vector3()
452
+ const _tmpPoints = []
453
+
454
+ function computeParabolicArc(origin, direction, velocity, gravity) {
455
+ _tmpPoints.length = 0
456
+ const dt = 0.05
457
+ const positions = teleportArc.geometry.attributes.position.array
458
+ let idx = 0
459
+ let hit = null
460
+ for (let i = 0; i < ARC_SEGMENTS; i++) {
461
+ const t = i * dt
462
+ const x = origin.x + direction.x * velocity * t
463
+ const y = origin.y + direction.y * velocity * t + 0.5 * gravity * t * t
464
+ const z = origin.z + direction.z * velocity * t
465
+ if (idx < positions.length) {
466
+ positions[idx++] = x
467
+ positions[idx++] = y
468
+ positions[idx++] = z
469
+ }
470
+ if (!hit && y < 0.1) {
471
+ const prevT = (i - 1) * dt
472
+ const prevY = origin.y + direction.y * velocity * prevT + 0.5 * gravity * prevT * prevT
473
+ if (prevY > 0.1) {
474
+ const frac = (0.1 - prevY) / (y - prevY)
475
+ const hitT = prevT + frac * dt
476
+ hit = {
477
+ point: new THREE.Vector3(
478
+ origin.x + direction.x * velocity * hitT,
479
+ 0,
480
+ origin.z + direction.z * velocity * hitT
481
+ ),
482
+ valid: true
483
+ }
484
+ }
485
+ }
486
+ }
487
+ teleportArc.geometry.attributes.position.needsUpdate = true
488
+ teleportArc.visible = true
489
+ return hit
490
+ }
491
+
492
+ function createFadeQuad() {
493
+ const geometry = new THREE.PlaneGeometry(2, 2)
494
+ const material = new THREE.MeshBasicMaterial({
495
+ color: 0x000000,
496
+ transparent: true,
497
+ opacity: 0,
498
+ depthTest: false,
499
+ depthWrite: false
500
+ })
501
+ const quad = new THREE.Mesh(geometry, material)
502
+ quad.renderOrder = 9999
503
+ return quad
504
+ }
505
+
506
+ function updateFade(dt) {
507
+ if (!fadeQuad) {
508
+ fadeQuad = createFadeQuad()
509
+ camera.add(fadeQuad)
510
+ fadeQuad.position.z = -0.1
511
+ }
512
+
513
+ if (fadeState === 'in') {
514
+ fadeOpacity += FADE_SPEED * dt
515
+ if (fadeOpacity >= 1) {
516
+ fadeOpacity = 1
517
+ fadeState = 'delay'
518
+ setTimeout(() => { fadeState = 'out' }, FADE_DELAY)
519
+ }
520
+ } else if (fadeState === 'out') {
521
+ fadeOpacity -= FADE_SPEED * dt
522
+ if (fadeOpacity <= 0) {
523
+ fadeOpacity = 0
524
+ fadeState = 'none'
525
+ }
526
+ }
527
+
528
+ fadeQuad.material.opacity = fadeOpacity
529
+ fadeQuad.visible = fadeOpacity > 0.01
530
+ }
531
+
532
+ function createVignette() {
533
+ const canvas = document.createElement('canvas')
534
+ canvas.width = 512
535
+ canvas.height = 512
536
+ const ctx = canvas.getContext('2d')
537
+
538
+ const gradient = ctx.createRadialGradient(256, 256, 100, 256, 256, 400)
539
+ gradient.addColorStop(0, 'rgba(0, 0, 0, 0)')
540
+ gradient.addColorStop(0.5, 'rgba(0, 0, 0, 0.3)')
541
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 0.8)')
542
+
543
+ ctx.fillStyle = gradient
544
+ ctx.fillRect(0, 0, 512, 512)
545
+
546
+ const texture = new THREE.CanvasTexture(canvas)
547
+ const geometry = new THREE.PlaneGeometry(2, 2)
548
+ const material = new THREE.MeshBasicMaterial({
549
+ map: texture,
550
+ transparent: true,
551
+ opacity: 0,
552
+ depthTest: false,
553
+ depthWrite: false
554
+ })
555
+ const mesh = new THREE.Mesh(geometry, material)
556
+ mesh.renderOrder = 9998
557
+ return mesh
558
+ }
559
+
560
+ function updateVignette(dt, isMoving) {
561
+ if (!vrSettings.vignetteEnabled) {
562
+ if (vignetteMesh) vignetteMesh.visible = false
563
+ return
564
+ }
565
+
566
+ if (!vignetteMesh) {
567
+ vignetteMesh = createVignette()
568
+ camera.add(vignetteMesh)
569
+ vignetteMesh.position.z = -0.15
570
+ }
571
+
572
+ vignetteTargetOpacity = isMoving ? 0.6 : 0
573
+ vignetteOpacity += (vignetteTargetOpacity - vignetteOpacity) * 5 * dt
574
+
575
+ vignetteMesh.material.opacity = vignetteOpacity
576
+ vignetteMesh.visible = vignetteOpacity > 0.01
577
+ }
578
+
579
+ function executeTeleport(targetPoint) {
580
+ isTeleporting = true
581
+ fadeState = 'in'
582
+
583
+ setTimeout(() => {
584
+ const base = xrBaseReferenceSpace || renderer.xr.getReferenceSpace()
585
+ if (!base) {
586
+ isTeleporting = false
587
+ fadeState = 'out'
588
+ return
589
+ }
590
+ const offsetPosition = { x: -targetPoint.x, y: -targetPoint.y, z: -targetPoint.z }
591
+ const transform = new XRRigidTransform(offsetPosition, { x: 0, y: 0, z: 0, w: 1 })
592
+ renderer.xr.setReferenceSpace(base.getOffsetReferenceSpace(transform))
593
+ }, 200)
594
+
595
+ setTimeout(() => { isTeleporting = false }, 400)
596
+ }
597
+
598
+ scene.add(camera)
599
+ const ambient = new THREE.AmbientLight(0xfff4d6, 0.3)
600
+ scene.add(ambient)
601
+ const studio = new THREE.DirectionalLight(0x4488ff, 0.4)
602
+ studio.position.set(-20, 30, -10)
603
+ studio.castShadow = false
604
+ scene.add(studio)
605
+ const sun = new THREE.DirectionalLight(0xffffff, 1.5)
606
+ sun.position.set(21, 50, 20)
21
607
  sun.castShadow = true
22
- sun.shadow.mapSize.set(2048, 2048)
23
- sun.shadow.camera.near = 0.5
24
- sun.shadow.camera.far = 200
25
- const sc = sun.shadow.camera
26
- sc.left = -80; sc.right = 80; sc.top = 80; sc.bottom = -80
608
+ sun.shadow.mapSize.set(1024, 1024)
609
+ sun.shadow.bias = 0.0038
610
+ sun.shadow.normalBias = 0.6
611
+ sun.shadow.radius = 12
612
+ sun.shadow.blurSamples = 8
613
+ sun.shadow.camera.left = -80; sun.shadow.camera.right = 80; sun.shadow.camera.top = 80; sun.shadow.camera.bottom = -80
614
+ sun.shadow.camera.near = 0.5; sun.shadow.camera.far = 200
27
615
  scene.add(sun)
616
+ scene.add(sun.target)
617
+
618
+ function fitShadowFrustum() {
619
+ const box = new THREE.Box3()
620
+ scene.traverse(o => { if (o.isMesh && (o.castShadow || o.receiveShadow) && o.geometry) box.expandByObject(o) })
621
+ if (box.isEmpty()) return
622
+ const center = box.getCenter(new THREE.Vector3())
623
+ const size = box.getSize(new THREE.Vector3())
624
+ const pad = 2
625
+ const half = (Math.max(size.x, size.z) / 2 + pad) * 1.06
626
+ const sc = sun.shadow.camera
627
+ sc.left = -half; sc.right = half; sc.top = half; sc.bottom = -half
628
+ const lightDir = new THREE.Vector3().subVectors(sun.target.position, sun.position).normalize()
629
+ const corners = [new THREE.Vector3(box.min.x, box.min.y, box.min.z), new THREE.Vector3(box.max.x, box.max.y, box.max.z)]
630
+ let minProj = Infinity, maxProj = -Infinity
631
+ for (const c of corners) { const d = new THREE.Vector3().subVectors(c, sun.position).dot(lightDir); minProj = Math.min(minProj, d); maxProj = Math.max(maxProj, d) }
632
+ sc.near = Math.max(0.5, minProj - 10); sc.far = maxProj + 10
633
+ sc.updateProjectionMatrix()
634
+ sun.target.position.copy(center)
635
+ sun.target.updateMatrixWorld()
636
+ }
637
+
638
+ function applySceneConfig(s) {
639
+ if (s.skyColor != null) { scene.background = new THREE.Color(s.skyColor) }
640
+ if (s.fogColor != null) { scene.fog = new THREE.Fog(s.fogColor, s.fogNear ?? 80, s.fogFar ?? 200) }
641
+ if (s.ambientColor != null) { ambient.color.set(s.ambientColor); ambient.intensity = s.ambientIntensity ?? 0.3 }
642
+ if (s.sunColor != null) { sun.color.set(s.sunColor); sun.intensity = s.sunIntensity ?? 1.5 }
643
+ if (s.sunPosition) sun.position.set(...s.sunPosition)
644
+ if (s.fillColor != null) { studio.color.set(s.fillColor); studio.intensity = s.fillIntensity ?? 0.4 }
645
+ if (s.fillPosition) studio.position.set(...s.fillPosition)
646
+ if (s.shadowMapSize) sun.shadow.mapSize.set(s.shadowMapSize, s.shadowMapSize)
647
+ if (s.shadowBias != null) sun.shadow.bias = s.shadowBias
648
+ if (s.shadowNormalBias != null) sun.shadow.normalBias = s.shadowNormalBias
649
+ if (s.shadowRadius != null) sun.shadow.radius = s.shadowRadius
650
+ if (s.shadowBlurSamples != null) sun.shadow.blurSamples = s.shadowBlurSamples
651
+ if (s.fov) { camera.fov = s.fov; camera.updateProjectionMatrix() }
652
+ }
28
653
 
29
654
  const ground = new THREE.Mesh(new THREE.PlaneGeometry(200, 200), new THREE.MeshStandardMaterial({ color: 0x444444 }))
30
655
  ground.rotation.x = -Math.PI / 2
@@ -32,57 +657,283 @@ ground.receiveShadow = true
32
657
  scene.add(ground)
33
658
 
34
659
  const gltfLoader = new GLTFLoader()
660
+ gltfLoader.register((parser) => new VRMLoaderPlugin(parser))
35
661
  const playerMeshes = new Map()
662
+ const playerAnimators = new Map()
663
+ const playerVrms = new Map()
664
+ const playerStates = new Map()
36
665
  const entityMeshes = new Map()
666
+ const entityParentMap = new Map()
667
+ const entityGroups = new Map()
37
668
  const appModules = new Map()
38
669
  const entityAppMap = new Map()
39
- const inputHandler = new InputHandler()
670
+ const playerTargets = new Map()
671
+ let inputHandler = null
40
672
  const uiRoot = document.getElementById('ui-root')
41
673
  const clickPrompt = document.getElementById('click-prompt')
674
+ if (deviceInfo.isMobile && clickPrompt) clickPrompt.style.display = 'none'
42
675
  const cam = createCameraController(camera, scene)
43
676
  cam.restore(JSON.parse(sessionStorage.getItem('cam') || 'null'))
44
677
  sessionStorage.removeItem('cam')
45
- let lastShootTime = 0
678
+ let latestState = null
679
+ let latestInput = null
680
+ let uiTimer = 0
46
681
  let lastFrameTime = performance.now()
682
+ let fpsFrames = 0, fpsLast = performance.now(), fpsDisplay = 0
683
+ let vrmBuffer = null
684
+ let animAssets = null
685
+ let assetsReady = null
686
+ let assetsLoaded = false
687
+
688
+ function detectVrmVersion(buffer) {
689
+ try {
690
+ const arrayBuffer = buffer instanceof ArrayBuffer ? buffer : buffer.buffer
691
+ const view = new DataView(arrayBuffer)
692
+ const jsonLen = view.getUint32(12, true)
693
+ const json = JSON.parse(new TextDecoder().decode(new Uint8Array(arrayBuffer, 20, jsonLen)))
694
+ if (json.extensions?.VRM) return '0'
695
+ } catch (e) {}
696
+ return '1'
697
+ }
47
698
 
48
- function createPlayerMesh(id, isLocal) {
699
+ function initAssets(playerModelUrl) {
700
+ loadingMgr.setStage('DOWNLOAD')
701
+ assetsReady = loadingMgr.fetchWithProgress(playerModelUrl).then(b => {
702
+ vrmBuffer = b
703
+ loadingMgr.setStage('PROCESS')
704
+ return loadAnimationLibrary(detectVrmVersion(b), null)
705
+ }).then(result => {
706
+ animAssets = result
707
+ assetsLoaded = true
708
+ checkAllLoaded()
709
+ })
710
+ }
711
+
712
+ async function createPlayerVRM(id) {
49
713
  const group = new THREE.Group()
50
- const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.4, 1.0, 4, 8), new THREE.MeshStandardMaterial({ color: isLocal ? 0x00ff88 : 0xff4444 }))
51
- body.position.y = 0.9; body.castShadow = true; group.add(body)
52
- const head = new THREE.Mesh(new THREE.SphereGeometry(0.25, 8, 6), new THREE.MeshStandardMaterial({ color: isLocal ? 0x00cc66 : 0xcc3333 }))
53
- head.position.y = 1.7; head.castShadow = true; group.add(head)
54
714
  scene.add(group)
715
+ playerMeshes.set(id, group)
716
+ if (assetsReady) await assetsReady
717
+ if (!vrmBuffer) return group
718
+ try {
719
+ const gltf = await gltfLoader.parseAsync(vrmBuffer.buffer.slice(0), '')
720
+ const vrm = gltf.userData.vrm
721
+ VRMUtils.removeUnnecessaryVertices(vrm.scene)
722
+ VRMUtils.combineSkeletons(vrm.scene)
723
+ const vrmVersion = detectVrmVersion(vrmBuffer)
724
+ vrm.scene.rotation.y = Math.PI
725
+ vrm.scene.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true } })
726
+ const pc = worldConfig.player || {}
727
+ const modelScale = pc.modelScale || 1.323
728
+ const feetOffsetRatio = pc.feetOffset || 0.212
729
+ vrm.scene.scale.multiplyScalar(modelScale)
730
+ vrm.scene.position.y = -feetOffsetRatio * modelScale
731
+ group.userData.feetOffset = 1.3
732
+ group.add(vrm.scene)
733
+ playerVrms.set(id, vrm)
734
+ initVRMFeatures(id, vrm)
735
+ if (animAssets) {
736
+ const animator = createPlayerAnimator(vrm, animAssets, vrmVersion, worldConfig.animation || {})
737
+ playerAnimators.set(id, animator)
738
+ }
739
+ if (id === client.playerId && vrm.humanoid) {
740
+ const head = vrm.humanoid.getRawBoneNode('head')
741
+ if (head) cam.setCameraBone(head)
742
+ if (head) cam.setHeadBone(head)
743
+ if (cam.getMode() === 'fps' && head) head.scale.set(0, 0, 0)
744
+ }
745
+ } catch (e) { console.error('[vrm]', id, e.message) }
55
746
  return group
56
747
  }
57
748
 
749
+ const playerExpressions = new Map()
750
+ const playerBlinkTimers = new Map()
751
+
752
+ function initVRMFeatures(id, vrm) {
753
+ const features = { vrm, expressions: null, lookAt: null, springBone: null, meta: null, blinkTimer: 0, nextBlink: Math.random() * 2 + 2 }
754
+ if (vrm.expressionManager) {
755
+ features.expressions = vrm.expressionManager
756
+ features.expressions.setValue('blink', 0)
757
+ }
758
+ if (vrm.lookAt) {
759
+ features.lookAt = vrm.lookAt
760
+ features.lookAt.smoothFactor = 0.1
761
+ }
762
+ if (vrm.springBoneManager) features.springBone = vrm.springBoneManager
763
+ if (vrm.meta) features.meta = vrm.meta
764
+ playerExpressions.set(id, features)
765
+ }
766
+
767
+ function updateVRMFeatures(id, dt, targetPosition) {
768
+ const features = playerExpressions.get(id)
769
+ if (!features) return
770
+ if (features.springBone) features.springBone.update(dt)
771
+ if (features.lookAt && targetPosition) {
772
+ const lookTarget = new THREE.Vector3(targetPosition.x, targetPosition.y + 1.6, targetPosition.z)
773
+ features.lookAt.lookAt(lookTarget)
774
+ }
775
+ if (features.expressions) {
776
+ features.blinkTimer += dt
777
+ if (features.blinkTimer >= features.nextBlink) {
778
+ features.expressions.setValue('blink', 1)
779
+ if (features.blinkTimer >= features.nextBlink + 0.15) {
780
+ features.expressions.setValue('blink', 0)
781
+ features.blinkTimer = 0
782
+ features.nextBlink = Math.random() * 3 + 2
783
+ }
784
+ }
785
+ }
786
+ }
787
+
788
+ function setVRMExpression(id, expressionName, value) {
789
+ const features = playerExpressions.get(id)
790
+ if (features?.expressions) features.expressions.setValue(expressionName, value)
791
+ }
792
+
58
793
  function removePlayerMesh(id) {
59
794
  const mesh = playerMeshes.get(id)
60
795
  if (!mesh) return
61
796
  scene.remove(mesh)
797
+ const animator = playerAnimators.get(id)
798
+ if (animator) animator.dispose()
799
+ playerAnimators.delete(id)
800
+ const vrm = playerVrms.get(id)
801
+ if (vrm) VRMUtils.deepDispose(vrm.scene)
802
+ playerVrms.delete(id)
62
803
  mesh.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose() })
63
804
  playerMeshes.delete(id)
805
+ playerTargets.delete(id)
806
+ playerStates.delete(id)
807
+ playerExpressions.delete(id)
64
808
  }
65
809
 
66
810
  function evaluateAppModule(code) {
67
811
  try {
68
812
  const stripped = code.replace(/^import\s+.*$/gm, '')
69
- const wrapped = stripped.replace(/export\s+default\s+/, 'return ')
813
+ const wrapped = stripped.replace(/export\s+default\s*/, 'return ').replace(/export\s+/g, '')
70
814
  return new Function(wrapped)()
71
- } catch (e) { console.error('[app-eval]', e.message); return null }
815
+ } catch (e) { console.error('[app-eval]', e.message, e.stack); return null }
816
+ }
817
+
818
+ const PLACEHOLDER_DIMS = {
819
+ door: [1.5, 2.5, 0.1],
820
+ platform: [4, 0.5, 4],
821
+ trigger: [2, 3, 2],
822
+ hazard: [2, 2, 2],
823
+ lootBox: [1, 1.5, 1],
824
+ pillar: [1, 4, 1]
825
+ }
826
+
827
+ function createEditorPlaceholder(entityId, templateName, custom) {
828
+ const dims = PLACEHOLDER_DIMS[templateName] || [1, 1, 1]
829
+ const geo = new THREE.BoxGeometry(dims[0], dims[1], dims[2])
830
+ const color = custom?.color ?? 0xcccccc
831
+ const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.8, metalness: 0.1, transparent: true, opacity: 0.7 })
832
+ const group = new THREE.Group()
833
+ const mesh = new THREE.Mesh(geo, mat)
834
+ mesh.castShadow = true
835
+ mesh.receiveShadow = true
836
+ mesh.userData.isPlaceholder = true
837
+ mesh.userData.templateName = templateName
838
+ group.add(mesh)
839
+ group.userData.spin = custom?.spin || 0
840
+ group.userData.hover = custom?.hover || 0
841
+ return group
842
+ }
843
+
844
+ const MESH_BUILDERS = {
845
+ box: (c) => new THREE.BoxGeometry(c.sx || 1, c.sy || 1, c.sz || 1),
846
+ cylinder: (c) => new THREE.CylinderGeometry(c.r || 0.4, c.r || 0.4, c.h || 0.1, c.seg || 16),
847
+ sphere: (c) => new THREE.SphereGeometry(c.r || 0.5, c.seg || 16, c.seg || 16)
848
+ }
849
+
850
+ function buildEntityMesh(entityId, custom) {
851
+ const c = custom || {}
852
+ const geoType = c.mesh || 'box'
853
+ const geo = MESH_BUILDERS[geoType] ? MESH_BUILDERS[geoType](c) : MESH_BUILDERS.box(c)
854
+ const mat = new THREE.MeshStandardMaterial({
855
+ color: c.color ?? 0xff8800, roughness: c.roughness ?? 1, metalness: c.metalness ?? 0,
856
+ emissive: c.emissive ?? 0x000000, emissiveIntensity: c.emissiveIntensity ?? 0
857
+ })
858
+ const group = new THREE.Group()
859
+ const mesh = new THREE.Mesh(geo, mat)
860
+ if (c.rotX) mesh.rotation.x = c.rotX
861
+ if (c.rotZ) mesh.rotation.z = c.rotZ
862
+ mesh.castShadow = true; mesh.receiveShadow = true
863
+ group.add(mesh)
864
+ if (c.light) { group.add(new THREE.PointLight(c.light, c.lightIntensity || 1, c.lightRange || 4)) }
865
+ if (c.spin) group.userData.spin = c.spin
866
+ if (c.hover) group.userData.hover = c.hover
867
+ return group
868
+ }
869
+
870
+ const pendingLoads = new Set()
871
+
872
+ function rebuildEntityHierarchy(entities) {
873
+ for (const e of entities) {
874
+ entityParentMap.set(e.id, e.parent || null)
875
+ }
876
+
877
+ for (const e of entities) {
878
+ const mesh = entityMeshes.get(e.id)
879
+ if (!mesh) continue
880
+
881
+ const parentId = entityParentMap.get(e.id)
882
+ const currentParent = mesh.parent && mesh.parent !== scene ? mesh.parent : null
883
+ const currentParentId = Array.from(entityParentMap.entries()).find(([k, v]) => v === null || entityMeshes.get(k) === currentParent)?.[0]
884
+
885
+ if (parentId === null) {
886
+ if (currentParent && currentParent !== scene) {
887
+ scene.add(mesh)
888
+ }
889
+ } else {
890
+ const parentMesh = entityMeshes.get(parentId)
891
+ if (parentMesh && parentMesh !== currentParent) {
892
+ parentMesh.add(mesh)
893
+ }
894
+ }
895
+ }
72
896
  }
73
897
 
74
898
  function loadEntityModel(entityId, entityState) {
75
- if (!entityState.model) return
899
+ if (entityMeshes.has(entityId) || pendingLoads.has(entityId)) return
900
+ pendingLoads.add(entityId)
901
+
902
+ const isEditorPlaceholder = entityState.custom?.editorPlaceholder === true
903
+ const smartObjectTemplate = entityState.custom?.template
904
+
905
+ if (!entityState.model || isEditorPlaceholder) {
906
+ let group
907
+ if (isEditorPlaceholder && smartObjectTemplate) {
908
+ group = createEditorPlaceholder(entityId, smartObjectTemplate, entityState.custom)
909
+ } else {
910
+ group = buildEntityMesh(entityId, entityState.custom)
911
+ }
912
+ group.position.set(...entityState.position)
913
+ if (entityState.rotation) group.quaternion.set(...entityState.rotation)
914
+ scene.add(group)
915
+ entityMeshes.set(entityId, group)
916
+ pendingLoads.delete(entityId)
917
+ if (!environmentLoaded) { environmentLoaded = true; checkAllLoaded() }
918
+ return
919
+ }
920
+ loadingMgr.setStage('RESOURCES')
76
921
  const url = entityState.model.startsWith('./') ? '/' + entityState.model.slice(2) : entityState.model
77
922
  gltfLoader.load(url, (gltf) => {
78
923
  const model = gltf.scene
79
924
  model.position.set(...entityState.position)
80
925
  if (entityState.rotation) model.quaternion.set(...entityState.rotation)
81
- model.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true } })
926
+ model.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; if (c.material) { c.material.shadowSide = THREE.DoubleSide; c.material.roughness = 1; c.material.metalness = 0; if (c.material.specularIntensity !== undefined) c.material.specularIntensity = 0 } } })
82
927
  scene.add(model)
83
928
  entityMeshes.set(entityId, model)
929
+ const colliders = []
930
+ model.traverse(c => { if (c.isMesh && c.name === 'Collider') colliders.push(c) })
931
+ if (colliders.length) cam.setEnvironment(colliders)
84
932
  scene.remove(ground)
85
- }, undefined, (err) => console.error('[gltf]', entityId, err))
933
+ fitShadowFrustum()
934
+ pendingLoads.delete(entityId)
935
+ if (!environmentLoaded) { environmentLoaded = true; checkAllLoaded() }
936
+ }, undefined, (err) => { console.error('[gltf]', entityId, err); pendingLoads.delete(entityId) })
86
937
  }
87
938
 
88
939
  function renderAppUI(state) {
@@ -93,89 +944,514 @@ function renderAppUI(state) {
93
944
  const appClient = appModules.get(appName)
94
945
  if (!appClient?.render) continue
95
946
  try {
96
- const result = appClient.render({ entity, state: entity.custom || {}, h: createElement })
947
+ const result = appClient.render({ entity, state: entity.custom || {}, h: createElement, engine: engineCtx, players: state.players })
97
948
  if (result?.ui) uiFragments.push({ id: entity.id, ui: result.ui })
98
949
  } catch (e) { console.error('[ui]', entity.id, e.message) }
99
950
  }
100
- const local = state.players.find(p => p.id === client.playerId)
101
- const hp = local?.health ?? 100
102
- const hudVdom = createElement('div', { id: 'hud' },
103
- createElement('div', { id: 'crosshair' }, '+'),
104
- createElement('div', { id: 'health-bar' },
105
- createElement('div', { id: 'health-fill', style: `width:${hp}%;background:${hp > 60 ? '#0f0' : hp > 30 ? '#ff0' : '#f00'}` }),
106
- createElement('span', { id: 'health-text' }, String(hp))
107
- ),
108
- createElement('div', { id: 'info' }, `Players: ${state.players.length} | Tick: ${client.currentTick}`),
951
+ const hudVdom = createElement('div', { id: 'hud' },
952
+ createElement('div', { id: 'info' }, `FPS: ${fpsDisplay} | Players: ${state.players.length} | Tick: ${client.currentTick} | RTT: ${Math.round(client.getRTT())}ms | Buf: ${client.getBufferHealth()}`),
109
953
  ...uiFragments.map(f => createElement('div', { 'data-app': f.id }, f.ui))
110
954
  )
111
955
  try { applyDiff(uiRoot, hudVdom) } catch (e) { console.error('[ui] diff:', e.message) }
112
956
  }
113
957
 
114
958
  const client = new PhysicsNetworkClient({
115
- serverUrl: `ws://${window.location.host}/ws`,
959
+ url: `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`,
960
+ predictionEnabled: true,
961
+ smoothInterpolation: true,
116
962
  onStateUpdate: (state) => {
117
- for (const p of state.players) {
118
- if (!playerMeshes.has(p.id)) playerMeshes.set(p.id, createPlayerMesh(p.id, p.id === client.playerId))
963
+ const smoothState = client.getSmoothState()
964
+ for (const p of smoothState.players) {
965
+ if (!playerMeshes.has(p.id)) createPlayerVRM(p.id)
119
966
  const mesh = playerMeshes.get(p.id)
120
- mesh.position.set(p.position[0], p.position[1] - 1.3, p.position[2])
121
- if (p.rotation) mesh.quaternion.set(...p.rotation)
967
+ const feetOff = mesh?.userData?.feetOffset ?? 1.3
968
+ const tx = p.position[0], ty = p.position[1] - feetOff, tz = p.position[2]
969
+ playerTargets.set(p.id, { x: tx, y: ty, z: tz })
970
+ playerStates.set(p.id, p)
971
+ const dx = tx - mesh.position.x, dy = ty - mesh.position.y, dz = tz - mesh.position.z
972
+ if (!mesh.userData.initialized || dx * dx + dy * dy + dz * dz > 100) { mesh.position.set(tx, ty, tz); mesh.userData.initialized = true }
122
973
  }
123
- renderAppUI(state)
974
+ for (const e of smoothState.entities) {
975
+ const mesh = entityMeshes.get(e.id)
976
+ if (mesh && e.position) mesh.position.set(...e.position)
977
+ if (mesh && e.rotation) mesh.quaternion.set(...e.rotation)
978
+ if (!entityMeshes.has(e.id)) loadEntityModel(e.id, e)
979
+ }
980
+ rebuildEntityHierarchy(smoothState.entities)
981
+ latestState = state
982
+ if (!firstSnapshotReceived) { firstSnapshotReceived = true; checkAllLoaded() }
124
983
  },
125
- onPlayerJoined: (id) => { if (!playerMeshes.has(id)) playerMeshes.set(id, createPlayerMesh(id, id === client.playerId)) },
984
+ onPlayerJoined: (id) => { if (!playerMeshes.has(id)) createPlayerVRM(id) },
126
985
  onPlayerLeft: (id) => removePlayerMesh(id),
127
986
  onEntityAdded: (id, state) => loadEntityModel(id, state),
128
- onWorldDef: (wd) => { if (wd.entities) for (const e of wd.entities) { if (e.app) entityAppMap.set(e.id, e.app); if (e.model && !entityMeshes.has(e.id)) loadEntityModel(e.id, e) } },
129
- onAppModule: (d) => { const a = evaluateAppModule(d.code); if (a?.client) appModules.set(d.app, a.client) },
987
+ onEntityRemoved: (id) => { const m = entityMeshes.get(id); if (m) { scene.remove(m); m.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose() }); entityMeshes.delete(id) }; pendingLoads.delete(id) },
988
+ onWorldDef: (wd) => {
989
+ loadingMgr.setStage('SERVER_SYNC')
990
+ worldConfig = wd
991
+ if (wd.playerModel) initAssets(wd.playerModel.startsWith('./') ? '/' + wd.playerModel.slice(2) : wd.playerModel)
992
+ else { assetsReady = Promise.resolve(); assetsLoaded = true; checkAllLoaded() }
993
+ if (wd.entities) for (const e of wd.entities) { if (e.app) entityAppMap.set(e.id, e.app); if (e.model && !entityMeshes.has(e.id)) loadEntityModel(e.id, e) }
994
+ if (wd.scene) applySceneConfig(wd.scene)
995
+ if (wd.camera) cam.applyConfig(wd.camera)
996
+ if (wd.input) {
997
+ inputConfig = { pointerLock: true, ...wd.input }
998
+ if (!inputConfig.pointerLock) { clickPrompt.style.display = 'none' }
999
+ }
1000
+ },
1001
+ onAppModule: (d) => {
1002
+ loadingMgr.setStage('APPS')
1003
+ const a = evaluateAppModule(d.code)
1004
+ if (a?.client) {
1005
+ appModules.set(d.app, a.client)
1006
+ if (a.client.setup) try { a.client.setup(engineCtx) } catch (e) { console.error('[app-setup]', d.app, e.message) }
1007
+ }
1008
+ },
130
1009
  onAssetUpdate: () => {},
131
- onAppEvent: () => {},
1010
+ onAppEvent: (payload) => {
1011
+ for (const [, mod] of appModules) { if (mod.onEvent) try { mod.onEvent(payload, engineCtx) } catch (e) { console.error('[app-event]', e.message) } }
1012
+ },
132
1013
  onHotReload: () => { sessionStorage.setItem('cam', JSON.stringify(cam.save())); location.reload() },
133
1014
  debug: false
134
1015
  })
135
1016
 
1017
+ const engineCtx = {
1018
+ scene, camera, renderer,
1019
+ get client() { return client },
1020
+ get playerId() { return client.playerId },
1021
+ get cam() { return cam },
1022
+ get worldConfig() { return worldConfig },
1023
+ get inputConfig() { return inputConfig },
1024
+ get _tps() { return engineCtx._tpsState },
1025
+ set _tps(val) { engineCtx._tpsState = val },
1026
+ playerVrms,
1027
+ setInputConfig(cfg) { Object.assign(inputConfig, cfg); if (!inputConfig.pointerLock) { clickPrompt.style.display = 'none'; if (document.pointerLockElement) document.exitPointerLock() } },
1028
+ players: {
1029
+ getMesh: (id) => playerMeshes.get(id),
1030
+ getState: (id) => playerStates.get(id),
1031
+ getAnimator: (id) => playerAnimators.get(id),
1032
+ setExpression: (id, name, val) => setVRMExpression(id, name, val),
1033
+ setAiming: (id, val) => { const s = playerStates.get(id); if (s) s._aiming = val }
1034
+ },
1035
+ createElement,
1036
+ THREE,
1037
+ get mobileControls() { return mobileControls }
1038
+ }
1039
+
1040
+ initFacialSystem(engineCtx)
1041
+
136
1042
  let inputLoopId = null
1043
+ let loadingScreenHidden = false
1044
+ let environmentLoaded = false
1045
+ let firstSnapshotReceived = false
1046
+ let lastShootState = false
1047
+ let lastHealth = 100
1048
+
1049
+ function checkAllLoaded() {
1050
+ if (loadingScreenHidden) return
1051
+ if (!assetsLoaded) return
1052
+ if (!environmentLoaded) return
1053
+ if (!firstSnapshotReceived) return
1054
+ loadingMgr.setStage('INIT')
1055
+ loadingMgr.complete()
1056
+ loadingScreen.hide().catch(() => {})
1057
+ loadingScreenHidden = true
1058
+ }
1059
+
1060
+ function initInputHandler() {
1061
+ inputHandler = new InputHandler({
1062
+ renderer,
1063
+ snapTurnAngle: vrSettings.snapTurnAngle,
1064
+ smoothTurnSpeed: vrSettings.smoothTurnSpeed,
1065
+ onMenuPressed: () => {
1066
+ if (renderer.xr.isPresenting) toggleVRSettings()
1067
+ }
1068
+ })
1069
+
1070
+ if (mobileControls) {
1071
+ inputHandler.setMobileControls(mobileControls)
1072
+ }
1073
+
1074
+ renderer.xr.addEventListener('sessionstart', () => {
1075
+ if (inputHandler) {
1076
+ inputHandler.vrYaw = cam.yaw
1077
+ console.log('[VR] Session started, vrYaw initialized to:', cam.yaw)
1078
+ }
1079
+ setTimeout(() => {
1080
+ xrBaseReferenceSpace = renderer.xr.getReferenceSpace()
1081
+ if (!xrBaseReferenceSpace) return
1082
+ const local = client.state?.players?.find(p => p.id === client.playerId)
1083
+ if (local?.position) {
1084
+ const headHeight = local.crouch ? 1.1 : 1.6
1085
+ const pos = { x: -local.position[0], y: -(local.position[1] + headHeight), z: -local.position[2] }
1086
+ const t = new XRRigidTransform(pos, { x: 0, y: 0, z: 0, w: 1 })
1087
+ renderer.xr.setReferenceSpace(xrBaseReferenceSpace.getOffsetReferenceSpace(t))
1088
+ camera.position.set(local.position[0], local.position[1] + headHeight, local.position[2])
1089
+ console.log('[VR] Camera synced to player position:', local.position, 'headHeight:', headHeight)
1090
+ }
1091
+ }, 100)
1092
+ })
1093
+ renderer.xr.addEventListener('sessionend', () => {
1094
+ xrBaseReferenceSpace = null
1095
+ })
1096
+ }
1097
+
1098
+ async function initAR() {
1099
+ const supported = await arControls.init(renderer)
1100
+ if (supported) {
1101
+ arButton = await createARButton(renderer, async () => {
1102
+ const started = await arControls.start()
1103
+ if (started) {
1104
+ arEnabled = true
1105
+ scene.background = null
1106
+ ground.visible = false
1107
+ console.log('[AR] AR mode started')
1108
+ return true
1109
+ }
1110
+ return false
1111
+ }, async () => {
1112
+ await arControls.end()
1113
+ arEnabled = false
1114
+ scene.background = new THREE.Color(0x87ceeb)
1115
+ ground.visible = true
1116
+ if (arButton) {
1117
+ arButton.textContent = 'Enter XR'
1118
+ arButton.style.background = 'rgba(0, 150, 0, 0.8)'
1119
+ }
1120
+ console.log('[AR] AR mode ended')
1121
+ })
1122
+ if (arButton) {
1123
+ document.body.appendChild(arButton)
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ let settingsTriggerCooldown = false
1129
+ let settingsSnapCooldown = false
1130
+ let settingsSmoothCooldown = false
1131
+
1132
+ const SMOOTH_TURN_SPEEDS = [0, 1.5, 3.0, 4.5]
1133
+ const SNAP_TURN_ANGLES = [15, 30, 45, 60, 90]
1134
+
1135
+ function cycleSmoothTurnSpeed() {
1136
+ const idx = SMOOTH_TURN_SPEEDS.indexOf(vrSettings.smoothTurnSpeed)
1137
+ vrSettings.smoothTurnSpeed = SMOOTH_TURN_SPEEDS[(idx + 1) % SMOOTH_TURN_SPEEDS.length]
1138
+ if (inputHandler) inputHandler.setSmoothTurnSpeed(vrSettings.smoothTurnSpeed)
1139
+ updateVRSettingsPanel()
1140
+ }
1141
+
1142
+ function cycleSnapTurnAngle() {
1143
+ const idx = SNAP_TURN_ANGLES.indexOf(vrSettings.snapTurnAngle)
1144
+ vrSettings.snapTurnAngle = SNAP_TURN_ANGLES[(idx + 1) % SNAP_TURN_ANGLES.length]
1145
+ if (inputHandler) inputHandler.setSnapTurnAngle(vrSettings.snapTurnAngle)
1146
+ updateVRSettingsPanel()
1147
+ }
1148
+
137
1149
  function startInputLoop() {
138
1150
  if (inputLoopId) return
1151
+ if (!inputHandler) initInputHandler()
139
1152
  inputLoopId = setInterval(() => {
140
1153
  if (!client.connected) return
141
1154
  const input = inputHandler.getInput()
142
- input.yaw = cam.yaw; input.pitch = cam.pitch
143
- client.sendInput(input)
144
- if (input.shoot && Date.now() - lastShootTime > 100) {
145
- lastShootTime = Date.now()
146
- const local = client.state?.players?.find(p => p.id === client.playerId)
147
- if (local) {
148
- const pos = local.position
149
- client.sendFire({ origin: [pos[0], pos[1] + 0.9, pos[2]], direction: cam.getAimDirection(pos) })
150
- const flash = new THREE.PointLight(0xffaa00, 3, 8)
151
- flash.position.set(pos[0], pos[1] + 0.5, pos[2])
152
- scene.add(flash)
153
- setTimeout(() => scene.remove(flash), 60)
1155
+ latestInput = input
1156
+
1157
+ if (input.editToggle && !cam.getEditMode()) {
1158
+ cam.setEditMode(true)
1159
+ console.log('[EditMode] Enabled')
1160
+ } else if (!input.editToggle && cam.getEditMode() && inputHandler.editModeCooldown === false) {
1161
+ cam.setEditMode(false)
1162
+ console.log('[EditMode] Disabled')
1163
+ }
1164
+
1165
+ if (input.yaw !== undefined) {
1166
+ cam.setVRYaw(input.yaw)
1167
+ } else {
1168
+ input.yaw = cam.yaw
1169
+ input.pitch = cam.pitch
1170
+ }
1171
+
1172
+ if (input.zoom !== undefined && input.zoom !== 0) {
1173
+ cam.onWheel({ deltaY: -input.zoom * 100, preventDefault: () => {} })
1174
+ }
1175
+
1176
+ if (input.isMobile && input.yawDelta !== undefined) {
1177
+ cam.setVRYaw(input.yaw)
1178
+ }
1179
+ if (input.isMobile && input.pitchDelta !== undefined) {
1180
+ cam.adjustVRPitch(input.pitchDelta)
1181
+ }
1182
+
1183
+ if (vrSettingsPanel?.visible) {
1184
+ if (input.shoot && !settingsTriggerCooldown) {
1185
+ vrSettings.teleportEnabled = !vrSettings.teleportEnabled
1186
+ settingsTriggerCooldown = true
1187
+ updateVRSettingsPanel()
1188
+ }
1189
+ if (input.sprint && !settingsSmoothCooldown) {
1190
+ cycleSmoothTurnSpeed()
1191
+ settingsSmoothCooldown = true
1192
+ }
1193
+ if (input.reload && !settingsSnapCooldown) {
1194
+ cycleSnapTurnAngle()
1195
+ settingsSnapCooldown = true
1196
+ }
1197
+ }
1198
+
1199
+ if (!vrSettingsPanel?.visible || !input.shoot) {
1200
+ settingsTriggerCooldown = false
1201
+ }
1202
+ if (!vrSettingsPanel?.visible || !input.sprint) {
1203
+ settingsSmoothCooldown = false
1204
+ }
1205
+ if (!vrSettingsPanel?.visible || !input.reload) {
1206
+ settingsSnapCooldown = false
1207
+ }
1208
+
1209
+ if (input.shoot && !lastShootState) {
1210
+ inputHandler.pulse('right', 0.5, 100)
1211
+ }
1212
+ lastShootState = input.shoot
1213
+ const local = client.state?.players?.find(p => p.id === client.playerId)
1214
+ if (local) {
1215
+ if (local.health < lastHealth) {
1216
+ inputHandler.pulse('left', 0.8, 200)
1217
+ inputHandler.pulse('right', 0.8, 200)
154
1218
  }
1219
+ lastHealth = local.health
155
1220
  }
1221
+ for (const [, mod] of appModules) { if (mod.onInput) try { mod.onInput(input, engineCtx) } catch (e) { console.error('[app-input]', e.message) } }
1222
+ client.sendInput(input)
156
1223
  }, 1000 / 60)
157
1224
  }
158
1225
 
159
- renderer.domElement.addEventListener('click', () => { if (!document.pointerLockElement) renderer.domElement.requestPointerLock() })
1226
+ renderer.domElement.addEventListener('click', () => { if (inputConfig.pointerLock && !document.pointerLockElement) renderer.domElement.requestPointerLock() })
160
1227
  document.addEventListener('pointerlockchange', () => {
161
1228
  const locked = document.pointerLockElement === renderer.domElement
162
- clickPrompt.style.display = locked ? 'none' : 'block'
1229
+ clickPrompt.style.display = locked ? 'none' : (inputConfig.pointerLock ? 'block' : 'none')
163
1230
  if (locked) document.addEventListener('mousemove', cam.onMouseMove)
164
1231
  else document.removeEventListener('mousemove', cam.onMouseMove)
165
1232
  })
166
1233
  renderer.domElement.addEventListener('wheel', cam.onWheel, { passive: false })
1234
+ renderer.domElement.addEventListener('mousedown', (e) => { for (const [, mod] of appModules) { if (mod.onMouseDown) try { mod.onMouseDown(e, engineCtx) } catch (ex) {} } })
1235
+ renderer.domElement.addEventListener('mouseup', (e) => { for (const [, mod] of appModules) { if (mod.onMouseUp) try { mod.onMouseUp(e, engineCtx) } catch (ex) {} } })
1236
+ renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault())
167
1237
  window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight) })
168
1238
 
169
- function animate() {
170
- requestAnimationFrame(animate)
171
- const now = performance.now()
172
- const frameDt = Math.min((now - lastFrameTime) / 1000, 0.1)
1239
+ let modelLoadQueue = []
1240
+ function createGimbal(scale = 1) {
1241
+ const gimbal = new THREE.Group()
1242
+ const lineGeom = new THREE.BufferGeometry()
1243
+ const linePoints = [
1244
+ new THREE.Vector3(-scale, 0, 0), new THREE.Vector3(scale, 0, 0),
1245
+ new THREE.Vector3(0, -scale, 0), new THREE.Vector3(0, scale, 0),
1246
+ new THREE.Vector3(0, 0, -scale), new THREE.Vector3(0, 0, scale)
1247
+ ]
1248
+ lineGeom.setFromPoints(linePoints)
1249
+ const lineMat = new THREE.LineBasicMaterial({ color: 0xcccccc, transparent: true, opacity: 0.6 })
1250
+ const lines = new THREE.LineSegments(lineGeom, lineMat)
1251
+ gimbal.add(lines)
1252
+ const ringGeoms = [
1253
+ { rot: [Math.PI / 2, 0, 0], color: 0xff0000 },
1254
+ { rot: [0, Math.PI / 2, 0], color: 0x00ff00 },
1255
+ { rot: [0, 0, 0], color: 0x0000ff }
1256
+ ]
1257
+ for (const r of ringGeoms) {
1258
+ const ringGeo = new THREE.TorusGeometry(scale * 0.9, scale * 0.08, 16, 100)
1259
+ const ringMat = new THREE.MeshBasicMaterial({ color: r.color, transparent: true, opacity: 0.5 })
1260
+ const ring = new THREE.Mesh(ringGeo, ringMat)
1261
+ ring.rotation.fromArray(r.rot)
1262
+ gimbal.add(ring)
1263
+ }
1264
+ gimbal.userData.isGimbal = true
1265
+ return gimbal
1266
+ }
1267
+
1268
+ function loadQueuedModels() {
1269
+ if (modelLoadQueue.length === 0) return
1270
+ const file = modelLoadQueue.shift()
1271
+ const reader = new FileReader()
1272
+ reader.onload = (e) => {
1273
+ try {
1274
+ const buffer = e.target.result
1275
+ gltfLoader.parse(buffer, '', (gltf) => {
1276
+ const local = client.state?.players?.find(p => p.id === client.playerId)
1277
+ if (!local) return
1278
+ const sy = Math.sin(cam.yaw), cy = Math.cos(cam.yaw)
1279
+ const spawnDist = 1.0
1280
+ const spawnHeight = 0.3
1281
+ const standingHeight = local.crouch ? 1.1 : 1.6
1282
+ const x = local.position[0] + sy * spawnDist
1283
+ const y = local.position[1] + standingHeight + spawnHeight
1284
+ const z = local.position[2] + cy * spawnDist
1285
+ const group = new THREE.Group()
1286
+ if (gltf.scene) group.add(gltf.scene)
1287
+ const gimbal = createGimbal(0.5)
1288
+ gimbal.position.copy(group.position)
1289
+ group.add(gimbal)
1290
+ group.position.set(x, y, z)
1291
+ group.userData.isDroppedModel = true
1292
+ scene.add(group)
1293
+ const envApp = appModules.get('environment')
1294
+ if (envApp?.onEvent) {
1295
+ envApp.onEvent({
1296
+ type: 'dropModel',
1297
+ position: [x, y, z],
1298
+ rotation: [0, 0, 0, 1],
1299
+ modelPath: file.name,
1300
+ scale: [1, 1, 1]
1301
+ }, engineCtx)
1302
+ }
1303
+ console.log('[ModelLoader] Loaded:', file.name, 'position:', [x, y, z])
1304
+ setTimeout(loadQueuedModels, 100)
1305
+ }, (err) => {
1306
+ console.error('[ModelLoader] Parse error:', err.message)
1307
+ setTimeout(loadQueuedModels, 100)
1308
+ })
1309
+ } catch (err) {
1310
+ console.error('[ModelLoader] Load error:', err.message)
1311
+ setTimeout(loadQueuedModels, 100)
1312
+ }
1313
+ }
1314
+ reader.readAsArrayBuffer(file)
1315
+ }
1316
+
1317
+ document.addEventListener('dragover', (e) => {
1318
+ if (!cam.getEditMode()) return
1319
+ e.preventDefault()
1320
+ e.stopPropagation()
1321
+ renderer.domElement.style.opacity = '0.8'
1322
+ })
1323
+
1324
+ document.addEventListener('dragleave', (e) => {
1325
+ if (!cam.getEditMode()) return
1326
+ renderer.domElement.style.opacity = '1'
1327
+ })
1328
+
1329
+ document.addEventListener('drop', (e) => {
1330
+ if (!cam.getEditMode()) return
1331
+ e.preventDefault()
1332
+ e.stopPropagation()
1333
+ renderer.domElement.style.opacity = '1'
1334
+ const files = e.dataTransfer.files
1335
+ for (let i = 0; i < files.length; i++) {
1336
+ const file = files[i]
1337
+ if (file.type === 'model/gltf-binary' || file.type === 'model/gltf+json' || file.name.endsWith('.glb') || file.name.endsWith('.gltf')) {
1338
+ modelLoadQueue.push(file)
1339
+ }
1340
+ }
1341
+ if (modelLoadQueue.length > 0) {
1342
+ loadQueuedModels()
1343
+ }
1344
+ })
1345
+
1346
+ let smoothDt = 1 / 60
1347
+ function animate(timestamp) {
1348
+ const now = timestamp || performance.now()
1349
+ const rawDt = Math.min((now - lastFrameTime) / 1000, 0.1)
173
1350
  lastFrameTime = now
1351
+ smoothDt += (rawDt - smoothDt) * 0.2
1352
+ const frameDt = smoothDt
1353
+ fpsFrames++
1354
+ if (now - fpsLast >= 1000) { fpsDisplay = fpsFrames; fpsFrames = 0; fpsLast = now }
1355
+ const lerpFactor = 1.0 - Math.exp(-16.0 * frameDt)
1356
+ for (const [id, target] of playerTargets) {
1357
+ const mesh = playerMeshes.get(id)
1358
+ if (!mesh) continue
1359
+ const ps = playerStates.get(id)
1360
+ const vx = ps?.velocity?.[0] || 0, vy = ps?.velocity?.[1] || 0, vz = ps?.velocity?.[2] || 0
1361
+ const goalX = target.x + vx * frameDt, goalY = target.y + vy * frameDt, goalZ = target.z + vz * frameDt
1362
+ mesh.position.x += (goalX - mesh.position.x) * lerpFactor
1363
+ mesh.position.y += (goalY - mesh.position.y) * lerpFactor
1364
+ mesh.position.z += (goalZ - mesh.position.z) * lerpFactor
1365
+ }
1366
+ for (const [id, animator] of playerAnimators) {
1367
+ const ps = playerStates.get(id)
1368
+ if (!ps) continue
1369
+ animator.update(frameDt, ps.velocity, ps.onGround, ps.health, ps._aiming || false, ps.crouch || 0)
1370
+ const mesh = playerMeshes.get(id)
1371
+ if (!mesh) continue
1372
+ const vx = ps.velocity?.[0] || 0, vz = ps.velocity?.[2] || 0
1373
+ if (Math.sqrt(vx * vx + vz * vz) > 0.5) mesh.userData.lastYaw = Math.atan2(vx, vz)
1374
+ if (mesh.userData.lastYaw !== undefined) {
1375
+ let diff = mesh.userData.lastYaw - mesh.rotation.y
1376
+ while (diff > Math.PI) diff -= Math.PI * 2
1377
+ while (diff < -Math.PI) diff += Math.PI * 2
1378
+ mesh.rotation.y += diff * lerpFactor
1379
+ }
1380
+ const target = playerTargets.get(id)
1381
+ updateVRMFeatures(id, frameDt, target)
1382
+ if (id !== client.playerId && ps.lookPitch !== undefined) {
1383
+ const vrm = playerVrms.get(id)
1384
+ if (vrm?.humanoid) {
1385
+ const head = vrm.humanoid.getNormalizedBoneNode('head')
1386
+ if (head) head.rotation.x = -(ps.lookPitch || 0) * 0.6
1387
+ }
1388
+ }
1389
+ }
1390
+ for (const [eid, mesh] of entityMeshes) {
1391
+ if (mesh.userData.spin) mesh.rotation.y += mesh.userData.spin * frameDt
1392
+ if (mesh.userData.hover) {
1393
+ mesh.userData.hoverTime = (mesh.userData.hoverTime || 0) + frameDt
1394
+ const child = mesh.children[0]
1395
+ if (child) child.position.y = Math.sin(mesh.userData.hoverTime * 2) * mesh.userData.hover
1396
+ }
1397
+ }
1398
+ for (const [, mod] of appModules) { if (mod.onFrame) try { mod.onFrame(frameDt, engineCtx) } catch (e) {} }
1399
+ if (engineCtx.facial) engineCtx.facial.update(frameDt)
1400
+ uiTimer += frameDt
1401
+ if (latestState && uiTimer >= 0.25) { uiTimer = 0; renderAppUI(latestState) }
174
1402
  const local = client.state?.players?.find(p => p.id === client.playerId)
175
- cam.update(local, playerMeshes.get(client.playerId), frameDt)
1403
+ const inVR = renderer.xr.isPresenting
1404
+ if (!inVR || cam.getEditMode()) {
1405
+ cam.update(local, playerMeshes.get(client.playerId), frameDt, latestInput)
1406
+ }
1407
+ if (inVR && !cam.getEditMode() && local?.position && xrBaseReferenceSpace && !isTeleporting) {
1408
+ const headHeight = local.crouch ? 1.1 : 1.6
1409
+ const pos = { x: -local.position[0], y: -(local.position[1] + headHeight), z: -local.position[2] }
1410
+ const t = new XRRigidTransform(pos, { x: 0, y: 0, z: 0, w: 1 })
1411
+ renderer.xr.setReferenceSpace(xrBaseReferenceSpace.getOffsetReferenceSpace(t))
1412
+ }
1413
+ if (inVR && local && wristUI) {
1414
+ const tps = appModules.get('tps-game')?._tps
1415
+ const ammo = tps?.ammo ?? 0
1416
+ const reloading = tps?.reloading ?? false
1417
+ updateWristUI(local.health ?? 100, ammo, reloading)
1418
+ }
1419
+ updateControllerVisibility()
1420
+ updateTeleportArc()
1421
+ updateFade(frameDt)
1422
+
1423
+ let isMoving = false
1424
+ if (inVR && local?.velocity) {
1425
+ const speed = Math.sqrt(local.velocity[0] ** 2 + local.velocity[2] ** 2)
1426
+ isMoving = speed > 0.5
1427
+ }
1428
+ updateVignette(frameDt, isMoving)
1429
+
1430
+ if (arEnabled) {
1431
+ const xrFrame = renderer.xr.getFrame()
1432
+ if (xrFrame) {
1433
+ arControls.update(xrFrame, camera, scene)
1434
+ const local = client.state?.players?.find(p => p.id === client.playerId)
1435
+ if (local?.position && !arControls.anchorPlaced) {
1436
+ arControls.setInitialFPSPosition(local.position, cam.yaw)
1437
+ }
1438
+ }
1439
+ }
1440
+
176
1441
  renderer.render(scene, camera)
177
1442
  }
178
- animate()
1443
+ renderer.setAnimationLoop(animate)
179
1444
 
180
- client.connect().then(() => { console.log('Connected'); startInputLoop() }).catch(err => console.error('Connection failed:', err))
181
- window.debug = { scene, camera, renderer, client, playerMeshes, entityMeshes, appModules, inputHandler }
1445
+ client.connect().then(() => { console.log('Connected'); startInputLoop(); initAR() }).catch(err => console.error('Connection failed:', err))
1446
+ setupControllers()
1447
+ setupHands()
1448
+ window.__VR_DEBUG__ = false
1449
+ window.debug = {
1450
+ scene, camera, renderer, client, playerMeshes, entityMeshes, appModules, inputHandler, playerVrms, playerAnimators, loadingMgr, loadingScreen, controllerModels, controllerGrips, handModels, mobileControls, arControls,
1451
+ enableVRDebug: () => { window.__VR_DEBUG__ = true; console.log('[VR] Debug enabled - button/axis logging active') },
1452
+ disableVRDebug: () => { window.__VR_DEBUG__ = false; console.log('[VR] Debug disabled') },
1453
+ vrInput: () => inputHandler?.getInput() || null,
1454
+ vrSettings: () => vrSettings,
1455
+ deviceInfo: () => deviceInfo,
1456
+ placeARAnchor: () => arControls?.placeAnchor() || arControls?.placeAtCamera()
1457
+ }