@series-inc/rundot-3d-engine 0.3.0
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.
- package/LICENSE.txt +6 -0
- package/README.md +80 -0
- package/dist/ComponentRegistry-V_7WauAE.d.ts +448 -0
- package/dist/chunk-ZNDJR3RD.js +5623 -0
- package/dist/chunk-ZNDJR3RD.js.map +1 -0
- package/dist/index.d.ts +1484 -0
- package/dist/index.js +1390 -0
- package/dist/index.js.map +1 -0
- package/dist/systems/index.d.ts +3356 -0
- package/dist/systems/index.js +9652 -0
- package/dist/systems/index.js.map +1 -0
- package/docs/core/Component.md +321 -0
- package/docs/core/GameObject.md +204 -0
- package/docs/core/VenusGame.md +316 -0
- package/docs/patterns/ComponentCommunication.md +337 -0
- package/docs/patterns/CreatingGameObjects.md +290 -0
- package/docs/patterns/MeshColliders.md +338 -0
- package/docs/patterns/MeshLoading.md +316 -0
- package/docs/physics/Colliders.md +249 -0
- package/docs/physics/PhysicsSystem.md +151 -0
- package/docs/physics/RigidBodyComponent.md +201 -0
- package/docs/rendering/AssetManager.md +308 -0
- package/docs/rendering/InstancedRenderer.md +286 -0
- package/docs/rendering/MeshRenderer.md +286 -0
- package/docs/rendering/SkeletalRenderer.md +308 -0
- package/docs/systems/AnimationSystem.md +75 -0
- package/docs/systems/AudioSystem.md +79 -0
- package/docs/systems/InputManager.md +101 -0
- package/docs/systems/LightingSystem.md +101 -0
- package/docs/systems/NavigationSystem.md +246 -0
- package/docs/systems/ParticleSystem.md +44 -0
- package/docs/systems/PrefabSystem.md +60 -0
- package/docs/systems/SplineSystem.md +194 -0
- package/docs/systems/StowKitSystem.md +77 -0
- package/docs/systems/TweenSystem.md +132 -0
- package/docs/systems/UISystem.md +73 -0
- package/package.json +62 -0
- package/scripts/postinstall.mjs +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1390 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssetManager,
|
|
3
|
+
Component,
|
|
4
|
+
ComponentUpdater,
|
|
5
|
+
GameObject,
|
|
6
|
+
InstancedMeshManager,
|
|
7
|
+
PrefabComponent,
|
|
8
|
+
PrefabInstance,
|
|
9
|
+
RigidBodyComponentThree,
|
|
10
|
+
SkeletonCache,
|
|
11
|
+
StowKitSystem,
|
|
12
|
+
VenusGame,
|
|
13
|
+
__decorateClass
|
|
14
|
+
} from "./chunk-ZNDJR3RD.js";
|
|
15
|
+
|
|
16
|
+
// src/engine/interaction/InteractionZone.ts
|
|
17
|
+
import * as THREE from "three";
|
|
18
|
+
var InteractionZone = class extends Component {
|
|
19
|
+
id;
|
|
20
|
+
active = true;
|
|
21
|
+
onEnterCallback;
|
|
22
|
+
onExitCallback;
|
|
23
|
+
entitiesInZone = /* @__PURE__ */ new Set();
|
|
24
|
+
rigidBody = null;
|
|
25
|
+
visualMesh = null;
|
|
26
|
+
options;
|
|
27
|
+
constructor(onEnter, onExit, options = {}) {
|
|
28
|
+
super();
|
|
29
|
+
this.id = `interaction_${Math.random().toString(36).substr(2, 9)}`;
|
|
30
|
+
this.onEnterCallback = onEnter;
|
|
31
|
+
this.onExitCallback = onExit;
|
|
32
|
+
this.options = {
|
|
33
|
+
width: 2,
|
|
34
|
+
depth: 2,
|
|
35
|
+
active: true,
|
|
36
|
+
show: true,
|
|
37
|
+
...options
|
|
38
|
+
};
|
|
39
|
+
this.active = this.options.active ?? true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Set interaction callbacks
|
|
43
|
+
*/
|
|
44
|
+
setCallbacks(callbacks) {
|
|
45
|
+
this.onEnterCallback = callbacks.onEnter;
|
|
46
|
+
this.onExitCallback = callbacks.onExit;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Called when component is attached to GameObject
|
|
50
|
+
*/
|
|
51
|
+
onCreate() {
|
|
52
|
+
if (this.options.show) {
|
|
53
|
+
this.createVisualMesh();
|
|
54
|
+
}
|
|
55
|
+
this.createTriggerCollider();
|
|
56
|
+
this.setActive(this.active);
|
|
57
|
+
}
|
|
58
|
+
onEnabled() {
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create the visual mesh for the interaction zone
|
|
62
|
+
*/
|
|
63
|
+
createVisualMesh() {
|
|
64
|
+
const width = this.options.width;
|
|
65
|
+
const height = 0.1;
|
|
66
|
+
const depth = this.options.depth;
|
|
67
|
+
const geometry = new THREE.BoxGeometry(width, height, depth);
|
|
68
|
+
const material = new THREE.MeshBasicMaterial({
|
|
69
|
+
color: 0,
|
|
70
|
+
// Black like the original
|
|
71
|
+
transparent: true,
|
|
72
|
+
opacity: 0.15,
|
|
73
|
+
// Original opacity
|
|
74
|
+
side: THREE.DoubleSide
|
|
75
|
+
});
|
|
76
|
+
this.visualMesh = new THREE.Mesh(geometry, material);
|
|
77
|
+
this.gameObject.add(this.visualMesh);
|
|
78
|
+
this.visualMesh.position.y += 0.1;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Create the physics trigger collider
|
|
82
|
+
*/
|
|
83
|
+
createTriggerCollider() {
|
|
84
|
+
this.rigidBody = new RigidBodyComponentThree({
|
|
85
|
+
type: "static" /* STATIC */,
|
|
86
|
+
shape: "box" /* BOX */,
|
|
87
|
+
size: new THREE.Vector3(this.options.width, 0.1, this.options.depth),
|
|
88
|
+
// Fixed height of 0.1
|
|
89
|
+
isSensor: true
|
|
90
|
+
// This makes it a trigger collider
|
|
91
|
+
// No collision groups = default behavior (can detect anything that wants to hit it)
|
|
92
|
+
});
|
|
93
|
+
if (this.options.centerOffset) {
|
|
94
|
+
this.gameObject.position.x += this.options.centerOffset.x;
|
|
95
|
+
this.gameObject.position.z += this.options.centerOffset.y;
|
|
96
|
+
}
|
|
97
|
+
this.gameObject.addComponent(this.rigidBody);
|
|
98
|
+
this.rigidBody.registerOnTriggerEnter(this.onTriggerEnter.bind(this));
|
|
99
|
+
this.rigidBody.registerOnTriggerExit(this.onTriggerExit.bind(this));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Handle trigger enter event (to be called by physics system)
|
|
103
|
+
*/
|
|
104
|
+
onTriggerEnter(other) {
|
|
105
|
+
if (!this.active) {
|
|
106
|
+
console.warn(`\u{1F3AF} InteractionZone ${this.id}: Ignoring enter event - zone is inactive`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!this.gameObject.isEnabled()) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!this.entitiesInZone.has(other)) {
|
|
113
|
+
this.entitiesInZone.add(other);
|
|
114
|
+
if (this.onEnterCallback) {
|
|
115
|
+
this.onEnterCallback(other);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Handle trigger exit event (to be called by physics system)
|
|
121
|
+
*/
|
|
122
|
+
onTriggerExit(other) {
|
|
123
|
+
if (!this.active) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!this.gameObject.isEnabled()) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (this.entitiesInZone.has(other)) {
|
|
130
|
+
this.entitiesInZone.delete(other);
|
|
131
|
+
if (this.onExitCallback) {
|
|
132
|
+
this.onExitCallback(other);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get all entities currently in the zone
|
|
138
|
+
*/
|
|
139
|
+
getEntitiesInZone() {
|
|
140
|
+
return Array.from(this.entitiesInZone);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check if a specific entity is in the zone
|
|
144
|
+
*/
|
|
145
|
+
hasEntity(entity) {
|
|
146
|
+
return this.entitiesInZone.has(entity);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Set active state
|
|
150
|
+
*/
|
|
151
|
+
setActive(active) {
|
|
152
|
+
this.active = active;
|
|
153
|
+
if (this.visualMesh) {
|
|
154
|
+
this.visualMesh.visible = active;
|
|
155
|
+
}
|
|
156
|
+
if (this.rigidBody) {
|
|
157
|
+
}
|
|
158
|
+
if (!active) {
|
|
159
|
+
this.entitiesInZone.clear();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if the interaction zone is active
|
|
164
|
+
*/
|
|
165
|
+
isActive() {
|
|
166
|
+
return this.active;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the visual mesh
|
|
170
|
+
*/
|
|
171
|
+
getVisualMesh() {
|
|
172
|
+
return this.visualMesh;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get the collider component
|
|
176
|
+
*/
|
|
177
|
+
getCollider() {
|
|
178
|
+
return this.rigidBody;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get the GameObject this zone is attached to
|
|
182
|
+
*/
|
|
183
|
+
getGameObject() {
|
|
184
|
+
return this.gameObject;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Component cleanup
|
|
188
|
+
*/
|
|
189
|
+
onCleanup() {
|
|
190
|
+
this.entitiesInZone.clear();
|
|
191
|
+
if (this.visualMesh) {
|
|
192
|
+
this.visualMesh.geometry.dispose();
|
|
193
|
+
if (this.visualMesh.material instanceof THREE.Material) {
|
|
194
|
+
this.visualMesh.material.dispose();
|
|
195
|
+
}
|
|
196
|
+
this.gameObject.remove(this.visualMesh);
|
|
197
|
+
this.visualMesh = null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/engine/mobile/VirtualJoystickThree.ts
|
|
203
|
+
import * as THREE2 from "three";
|
|
204
|
+
var VirtualJoystickThree = class _VirtualJoystickThree extends Component {
|
|
205
|
+
// Configuration
|
|
206
|
+
options;
|
|
207
|
+
// Mobile detection
|
|
208
|
+
static isMobileDevice() {
|
|
209
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
210
|
+
}
|
|
211
|
+
// UI Elements
|
|
212
|
+
joystickContainer = null;
|
|
213
|
+
joystickBase = null;
|
|
214
|
+
joystickKnob = null;
|
|
215
|
+
mobileHint = null;
|
|
216
|
+
// State
|
|
217
|
+
isActive = false;
|
|
218
|
+
startPosition = new THREE2.Vector2();
|
|
219
|
+
currentPosition = new THREE2.Vector2();
|
|
220
|
+
direction = new THREE2.Vector2();
|
|
221
|
+
magnitude = 0;
|
|
222
|
+
// Input tracking
|
|
223
|
+
joystickPointerId = null;
|
|
224
|
+
isDragging = false;
|
|
225
|
+
joystickRadius = 0;
|
|
226
|
+
// Event handlers (need to bind them for proper cleanup)
|
|
227
|
+
boundPointerDown = this.onPointerDown.bind(this);
|
|
228
|
+
boundPointerMove = this.onPointerMove.bind(this);
|
|
229
|
+
boundPointerUp = this.onPointerUp.bind(this);
|
|
230
|
+
boundTouchStart = this.onTouchStart.bind(this);
|
|
231
|
+
boundTouchMove = this.onTouchMove.bind(this);
|
|
232
|
+
boundTouchEnd = this.onTouchEnd.bind(this);
|
|
233
|
+
constructor(options = {}) {
|
|
234
|
+
super();
|
|
235
|
+
this.options = {
|
|
236
|
+
size: options.size ?? 120,
|
|
237
|
+
knobSize: options.knobSize ?? 40,
|
|
238
|
+
deadZone: options.deadZone ?? 0.15,
|
|
239
|
+
maxDistance: options.maxDistance ?? 50,
|
|
240
|
+
color: options.color ?? "white",
|
|
241
|
+
visible: options.visible ?? true,
|
|
242
|
+
opacity: options.opacity ?? 0.2
|
|
243
|
+
};
|
|
244
|
+
this.joystickRadius = this.options.maxDistance;
|
|
245
|
+
}
|
|
246
|
+
onCreate() {
|
|
247
|
+
this.createJoystickUI();
|
|
248
|
+
this.createMobileHint();
|
|
249
|
+
this.setupInputHandlers();
|
|
250
|
+
}
|
|
251
|
+
onCleanup() {
|
|
252
|
+
this.cleanupUI();
|
|
253
|
+
this.removeInputHandlers();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Create the joystick UI elements using HTML/CSS
|
|
257
|
+
*/
|
|
258
|
+
createJoystickUI() {
|
|
259
|
+
this.joystickContainer = document.createElement("div");
|
|
260
|
+
this.joystickContainer.id = "virtual-joystick-container";
|
|
261
|
+
this.joystickContainer.style.cssText = `
|
|
262
|
+
position: fixed;
|
|
263
|
+
width: ${this.options.size + this.options.knobSize}px;
|
|
264
|
+
height: ${this.options.size + this.options.knobSize}px;
|
|
265
|
+
display: none;
|
|
266
|
+
pointer-events: none;
|
|
267
|
+
z-index: 1000;
|
|
268
|
+
user-select: none;
|
|
269
|
+
touch-action: none;
|
|
270
|
+
`;
|
|
271
|
+
this.joystickBase = document.createElement("div");
|
|
272
|
+
this.joystickBase.style.cssText = `
|
|
273
|
+
position: absolute;
|
|
274
|
+
width: ${this.options.size}px;
|
|
275
|
+
height: ${this.options.size}px;
|
|
276
|
+
border: 4px solid ${this.options.color};
|
|
277
|
+
border-radius: 50%;
|
|
278
|
+
background: rgba(255, 255, 255, 0.2);
|
|
279
|
+
left: 50%;
|
|
280
|
+
top: 50%;
|
|
281
|
+
transform: translate(-50%, -50%);
|
|
282
|
+
box-sizing: border-box;
|
|
283
|
+
opacity: ${this.options.opacity};
|
|
284
|
+
`;
|
|
285
|
+
this.joystickKnob = document.createElement("div");
|
|
286
|
+
this.joystickKnob.style.cssText = `
|
|
287
|
+
position: absolute;
|
|
288
|
+
width: ${this.options.knobSize}px;
|
|
289
|
+
height: ${this.options.knobSize}px;
|
|
290
|
+
border: 2px solid ${this.options.color};
|
|
291
|
+
border-radius: 50%;
|
|
292
|
+
background: rgba(255, 255, 255, 0.8);
|
|
293
|
+
left: 50%;
|
|
294
|
+
top: 50%;
|
|
295
|
+
transform: translate(-50%, -50%);
|
|
296
|
+
transition: none;
|
|
297
|
+
box-sizing: border-box;
|
|
298
|
+
opacity: ${this.options.opacity};
|
|
299
|
+
`;
|
|
300
|
+
this.joystickContainer.appendChild(this.joystickBase);
|
|
301
|
+
this.joystickContainer.appendChild(this.joystickKnob);
|
|
302
|
+
document.body.appendChild(this.joystickContainer);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Create mobile hint for touch controls (only shows on mobile devices)
|
|
306
|
+
*/
|
|
307
|
+
createMobileHint() {
|
|
308
|
+
if (!_VirtualJoystickThree.isMobileDevice()) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this.mobileHint = document.createElement("div");
|
|
312
|
+
this.mobileHint.id = "mobile-joystick-hint";
|
|
313
|
+
this.mobileHint.textContent = "Touch & drag to move";
|
|
314
|
+
this.mobileHint.style.cssText = `
|
|
315
|
+
position: fixed;
|
|
316
|
+
bottom: 20px;
|
|
317
|
+
left: 50%;
|
|
318
|
+
transform: translateX(-50%);
|
|
319
|
+
background: rgba(0, 0, 0, 0.8);
|
|
320
|
+
color: white;
|
|
321
|
+
padding: 10px 20px;
|
|
322
|
+
border-radius: 20px;
|
|
323
|
+
font-family: Arial, sans-serif;
|
|
324
|
+
font-size: 14px;
|
|
325
|
+
z-index: 999;
|
|
326
|
+
pointer-events: none;
|
|
327
|
+
user-select: none;
|
|
328
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
329
|
+
animation: fadeInOut 4s ease-in-out;
|
|
330
|
+
`;
|
|
331
|
+
if (!document.querySelector("#mobile-hint-styles")) {
|
|
332
|
+
const style = document.createElement("style");
|
|
333
|
+
style.id = "mobile-hint-styles";
|
|
334
|
+
style.textContent = `
|
|
335
|
+
@keyframes fadeInOut {
|
|
336
|
+
0% { opacity: 0; transform: translateX(-50%) translateY(20px); }
|
|
337
|
+
20% { opacity: 1; transform: translateX(-50%) translateY(0px); }
|
|
338
|
+
80% { opacity: 1; transform: translateX(-50%) translateY(0px); }
|
|
339
|
+
100% { opacity: 0; transform: translateX(-50%) translateY(-20px); }
|
|
340
|
+
}
|
|
341
|
+
`;
|
|
342
|
+
document.head.appendChild(style);
|
|
343
|
+
}
|
|
344
|
+
document.body.appendChild(this.mobileHint);
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
if (this.mobileHint && this.mobileHint.parentNode) {
|
|
347
|
+
this.mobileHint.parentNode.removeChild(this.mobileHint);
|
|
348
|
+
this.mobileHint = null;
|
|
349
|
+
}
|
|
350
|
+
}, 4e3);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Setup input handlers for pointer and touch events
|
|
354
|
+
*/
|
|
355
|
+
setupInputHandlers() {
|
|
356
|
+
document.addEventListener("pointerdown", this.boundPointerDown, {
|
|
357
|
+
passive: false
|
|
358
|
+
});
|
|
359
|
+
document.addEventListener("pointermove", this.boundPointerMove, {
|
|
360
|
+
passive: false
|
|
361
|
+
});
|
|
362
|
+
document.addEventListener("pointerup", this.boundPointerUp, {
|
|
363
|
+
passive: false
|
|
364
|
+
});
|
|
365
|
+
document.addEventListener("touchstart", this.boundTouchStart, {
|
|
366
|
+
passive: false
|
|
367
|
+
});
|
|
368
|
+
document.addEventListener("touchmove", this.boundTouchMove, {
|
|
369
|
+
passive: false
|
|
370
|
+
});
|
|
371
|
+
document.addEventListener("touchend", this.boundTouchEnd, {
|
|
372
|
+
passive: false
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Remove input handlers
|
|
377
|
+
*/
|
|
378
|
+
removeInputHandlers() {
|
|
379
|
+
document.removeEventListener("pointerdown", this.boundPointerDown);
|
|
380
|
+
document.removeEventListener("pointermove", this.boundPointerMove);
|
|
381
|
+
document.removeEventListener("pointerup", this.boundPointerUp);
|
|
382
|
+
document.removeEventListener("touchstart", this.boundTouchStart);
|
|
383
|
+
document.removeEventListener("touchmove", this.boundTouchMove);
|
|
384
|
+
document.removeEventListener("touchend", this.boundTouchEnd);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Handle pointer down event
|
|
388
|
+
*/
|
|
389
|
+
onPointerDown(event) {
|
|
390
|
+
if (this.isActive || !event.isPrimary) return;
|
|
391
|
+
this.startJoystick(event.clientX, event.clientY, event.pointerId);
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Handle touch start event (fallback)
|
|
396
|
+
*/
|
|
397
|
+
onTouchStart(event) {
|
|
398
|
+
if (this.isActive || event.touches.length === 0) return;
|
|
399
|
+
const touch = event.touches[0];
|
|
400
|
+
this.startJoystick(touch.clientX, touch.clientY, touch.identifier);
|
|
401
|
+
event.preventDefault();
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Start the joystick at the given position
|
|
405
|
+
*/
|
|
406
|
+
startJoystick(x, y, pointerId) {
|
|
407
|
+
this.isActive = true;
|
|
408
|
+
this.isDragging = true;
|
|
409
|
+
this.joystickPointerId = pointerId;
|
|
410
|
+
if (this.mobileHint && this.mobileHint.parentNode) {
|
|
411
|
+
this.mobileHint.parentNode.removeChild(this.mobileHint);
|
|
412
|
+
this.mobileHint = null;
|
|
413
|
+
}
|
|
414
|
+
this.startPosition.set(x, y);
|
|
415
|
+
this.currentPosition.set(x, y);
|
|
416
|
+
if (this.joystickContainer) {
|
|
417
|
+
this.joystickContainer.style.display = this.options.visible ? "block" : "none";
|
|
418
|
+
this.joystickContainer.style.left = `${x - (this.options.size + this.options.knobSize) / 2}px`;
|
|
419
|
+
this.joystickContainer.style.top = `${y - (this.options.size + this.options.knobSize) / 2}px`;
|
|
420
|
+
}
|
|
421
|
+
if (this.joystickKnob) {
|
|
422
|
+
this.joystickKnob.style.transform = "translate(-50%, -50%)";
|
|
423
|
+
}
|
|
424
|
+
this.updateDirection();
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Handle pointer move event
|
|
428
|
+
*/
|
|
429
|
+
onPointerMove(event) {
|
|
430
|
+
if (!this.isActive || !this.isDragging || event.pointerId !== this.joystickPointerId) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this.updateJoystick(event.clientX, event.clientY);
|
|
434
|
+
event.preventDefault();
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Handle touch move event (fallback)
|
|
438
|
+
*/
|
|
439
|
+
onTouchMove(event) {
|
|
440
|
+
if (!this.isActive || !this.isDragging) return;
|
|
441
|
+
for (let i = 0; i < event.touches.length; i++) {
|
|
442
|
+
const touch = event.touches[i];
|
|
443
|
+
if (touch.identifier === this.joystickPointerId) {
|
|
444
|
+
this.updateJoystick(touch.clientX, touch.clientY);
|
|
445
|
+
event.preventDefault();
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Update joystick position and direction
|
|
452
|
+
*/
|
|
453
|
+
updateJoystick(x, y) {
|
|
454
|
+
this.currentPosition.set(x, y);
|
|
455
|
+
this.updateKnobPosition();
|
|
456
|
+
this.updateDirection();
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Handle pointer up event
|
|
460
|
+
*/
|
|
461
|
+
onPointerUp(event) {
|
|
462
|
+
if (!this.isActive || event.pointerId !== this.joystickPointerId) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.endJoystick();
|
|
466
|
+
event.preventDefault();
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Handle touch end event (fallback)
|
|
470
|
+
*/
|
|
471
|
+
onTouchEnd(event) {
|
|
472
|
+
if (!this.isActive) return;
|
|
473
|
+
let touchEnded = true;
|
|
474
|
+
for (let i = 0; i < event.touches.length; i++) {
|
|
475
|
+
if (event.touches[i].identifier === this.joystickPointerId) {
|
|
476
|
+
touchEnded = false;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (touchEnded) {
|
|
481
|
+
this.endJoystick();
|
|
482
|
+
event.preventDefault();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* End the joystick interaction
|
|
487
|
+
*/
|
|
488
|
+
endJoystick() {
|
|
489
|
+
this.isActive = false;
|
|
490
|
+
this.isDragging = false;
|
|
491
|
+
this.joystickPointerId = null;
|
|
492
|
+
if (this.joystickContainer) {
|
|
493
|
+
this.joystickContainer.style.display = "none";
|
|
494
|
+
}
|
|
495
|
+
this.direction.set(0, 0);
|
|
496
|
+
this.magnitude = 0;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Update knob position based on current pointer position
|
|
500
|
+
*/
|
|
501
|
+
updateKnobPosition() {
|
|
502
|
+
if (!this.joystickKnob) return;
|
|
503
|
+
const offset = this.currentPosition.clone().sub(this.startPosition);
|
|
504
|
+
const distance = offset.length();
|
|
505
|
+
if (distance > this.joystickRadius) {
|
|
506
|
+
offset.normalize().multiplyScalar(this.joystickRadius);
|
|
507
|
+
}
|
|
508
|
+
this.joystickKnob.style.transform = `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))`;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Update direction vector based on knob position
|
|
512
|
+
*/
|
|
513
|
+
updateDirection() {
|
|
514
|
+
const offset = this.currentPosition.clone().sub(this.startPosition);
|
|
515
|
+
const distance = offset.length();
|
|
516
|
+
if (distance < this.options.deadZone * this.joystickRadius) {
|
|
517
|
+
this.direction.set(0, 0);
|
|
518
|
+
this.magnitude = 0;
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const normalizedDistance = Math.min(distance / this.joystickRadius, 1);
|
|
522
|
+
this.direction = offset.normalize();
|
|
523
|
+
this.magnitude = normalizedDistance;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Get the current input direction as a Vector3 (Y=0 for movement)
|
|
527
|
+
*/
|
|
528
|
+
getDirection() {
|
|
529
|
+
if (!this.isActive || this.magnitude === 0) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return new THREE2.Vector3(
|
|
533
|
+
this.direction.x * this.magnitude,
|
|
534
|
+
0,
|
|
535
|
+
-this.direction.y * this.magnitude
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Get the current input magnitude (0-1)
|
|
540
|
+
*/
|
|
541
|
+
getMagnitude() {
|
|
542
|
+
return this.magnitude;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Check if the joystick is currently active
|
|
546
|
+
*/
|
|
547
|
+
isActiveJoystick() {
|
|
548
|
+
return this.isActive;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Toggle joystick visual visibility (joystick remains functional)
|
|
552
|
+
*/
|
|
553
|
+
setVisible(visible) {
|
|
554
|
+
this.options.visible = visible;
|
|
555
|
+
if (this.joystickContainer && this.isActive) {
|
|
556
|
+
this.joystickContainer.style.display = visible ? "block" : "none";
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get current visibility state
|
|
561
|
+
*/
|
|
562
|
+
isVisible() {
|
|
563
|
+
return this.options.visible;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Set joystick opacity (0-1)
|
|
567
|
+
*/
|
|
568
|
+
setOpacity(opacity) {
|
|
569
|
+
this.options.opacity = Math.max(0, Math.min(1, opacity));
|
|
570
|
+
if (this.joystickBase) {
|
|
571
|
+
this.joystickBase.style.opacity = `${this.options.opacity}`;
|
|
572
|
+
}
|
|
573
|
+
if (this.joystickKnob) {
|
|
574
|
+
this.joystickKnob.style.opacity = `${this.options.opacity}`;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Get current opacity
|
|
579
|
+
*/
|
|
580
|
+
getOpacity() {
|
|
581
|
+
return this.options.opacity;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Clean up UI resources
|
|
585
|
+
*/
|
|
586
|
+
cleanupUI() {
|
|
587
|
+
if (this.joystickContainer && this.joystickContainer.parentNode) {
|
|
588
|
+
this.joystickContainer.parentNode.removeChild(this.joystickContainer);
|
|
589
|
+
}
|
|
590
|
+
if (this.mobileHint && this.mobileHint.parentNode) {
|
|
591
|
+
this.mobileHint.parentNode.removeChild(this.mobileHint);
|
|
592
|
+
}
|
|
593
|
+
this.joystickContainer = null;
|
|
594
|
+
this.joystickBase = null;
|
|
595
|
+
this.joystickKnob = null;
|
|
596
|
+
this.mobileHint = null;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// src/engine/movement/MovementController.ts
|
|
601
|
+
import * as THREE3 from "three";
|
|
602
|
+
var MovementController = class extends Component {
|
|
603
|
+
// Public configuration properties
|
|
604
|
+
maxMoveSpeed = 8;
|
|
605
|
+
acceleration = 40;
|
|
606
|
+
turnSpeed = 12;
|
|
607
|
+
// How fast to rotate (radians per second)
|
|
608
|
+
rigidBodyComponent = null;
|
|
609
|
+
targetRotationY = 0;
|
|
610
|
+
currentRotationY = 0;
|
|
611
|
+
// Pre-allocated vector for velocity queries to avoid GC pressure
|
|
612
|
+
_currentVelocity = new THREE3.Vector3();
|
|
613
|
+
/**
|
|
614
|
+
* Called when the component is created and attached to a GameObject
|
|
615
|
+
*/
|
|
616
|
+
onCreate() {
|
|
617
|
+
this.findRigidBodyComponentThree();
|
|
618
|
+
this.currentRotationY = this.gameObject.rotation.y;
|
|
619
|
+
this.targetRotationY = this.currentRotationY;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Find the rigid body component on this GameObject
|
|
623
|
+
*/
|
|
624
|
+
findRigidBodyComponentThree() {
|
|
625
|
+
this.rigidBodyComponent = this.gameObject.getComponent(RigidBodyComponentThree) || null;
|
|
626
|
+
if (!this.rigidBodyComponent) {
|
|
627
|
+
console.warn("MovementController: No RigidBodyComponentThree found on GameObject");
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Set the rigid body component this controller should manage
|
|
632
|
+
*/
|
|
633
|
+
setRigidBodyComponentThree(rigidBodyComponent) {
|
|
634
|
+
this.rigidBodyComponent = rigidBodyComponent;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Move the entity based on input direction
|
|
638
|
+
* @param inputDirection Normalized direction vector (or null for no movement)
|
|
639
|
+
* @param deltaTime Time since last frame in seconds
|
|
640
|
+
*/
|
|
641
|
+
move(inputDirection, deltaTime) {
|
|
642
|
+
if (!this.rigidBodyComponent) return;
|
|
643
|
+
const targetVelocity = this.calculateTargetVelocity(inputDirection);
|
|
644
|
+
const smoothedVelocity = this.smoothVelocity(targetVelocity, deltaTime);
|
|
645
|
+
this.rigidBodyComponent.setVelocity(smoothedVelocity);
|
|
646
|
+
this.updateRotation(inputDirection, deltaTime);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Calculate target velocity based on input direction
|
|
650
|
+
*/
|
|
651
|
+
calculateTargetVelocity(inputDirection) {
|
|
652
|
+
const targetVelocity = new THREE3.Vector3(0, 0, 0);
|
|
653
|
+
if (inputDirection && inputDirection.length() > 0.01) {
|
|
654
|
+
targetVelocity.x = inputDirection.x * this.maxMoveSpeed;
|
|
655
|
+
targetVelocity.z = inputDirection.z * this.maxMoveSpeed;
|
|
656
|
+
}
|
|
657
|
+
targetVelocity.y = 0;
|
|
658
|
+
return targetVelocity;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Smooth velocity towards target using acceleration
|
|
662
|
+
*/
|
|
663
|
+
smoothVelocity(targetVelocity, deltaTime) {
|
|
664
|
+
if (!this.rigidBodyComponent) return targetVelocity;
|
|
665
|
+
this.rigidBodyComponent.getVelocity(this._currentVelocity);
|
|
666
|
+
const maxDelta = this.acceleration * deltaTime;
|
|
667
|
+
const smoothedVelocity = new THREE3.Vector3();
|
|
668
|
+
smoothedVelocity.x = this.moveTowards(this._currentVelocity.x, targetVelocity.x, maxDelta);
|
|
669
|
+
smoothedVelocity.z = this.moveTowards(this._currentVelocity.z, targetVelocity.z, maxDelta);
|
|
670
|
+
smoothedVelocity.y = 0;
|
|
671
|
+
return smoothedVelocity;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Update rotation smoothly towards movement direction using quaternion slerp
|
|
675
|
+
*/
|
|
676
|
+
updateRotation(inputDirection, deltaTime) {
|
|
677
|
+
if (this.rigidBodyComponent) {
|
|
678
|
+
const rigidBody = this.rigidBodyComponent.getRigidBody();
|
|
679
|
+
if (rigidBody) {
|
|
680
|
+
this.rigidBodyComponent.setAngularVelocity(new THREE3.Vector3(0, 0, 0));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (!inputDirection || inputDirection.length() < 0.01) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const targetRotationY = Math.atan2(inputDirection.x, inputDirection.z);
|
|
687
|
+
const targetQuaternion = new THREE3.Quaternion();
|
|
688
|
+
targetQuaternion.setFromAxisAngle(new THREE3.Vector3(0, 1, 0), targetRotationY);
|
|
689
|
+
const currentQuaternion = new THREE3.Quaternion();
|
|
690
|
+
currentQuaternion.setFromAxisAngle(new THREE3.Vector3(0, 1, 0), this.currentRotationY);
|
|
691
|
+
const slerpFactor = Math.min(1, this.turnSpeed * deltaTime);
|
|
692
|
+
const resultQuaternion = new THREE3.Quaternion();
|
|
693
|
+
resultQuaternion.slerpQuaternions(currentQuaternion, targetQuaternion, slerpFactor);
|
|
694
|
+
const euler = new THREE3.Euler();
|
|
695
|
+
euler.setFromQuaternion(resultQuaternion, "YXZ");
|
|
696
|
+
this.currentRotationY = euler.y;
|
|
697
|
+
if (this.rigidBodyComponent) {
|
|
698
|
+
const rigidBody = this.rigidBodyComponent.getRigidBody();
|
|
699
|
+
if (rigidBody) {
|
|
700
|
+
const rapierQuat = {
|
|
701
|
+
x: resultQuaternion.x,
|
|
702
|
+
y: resultQuaternion.y,
|
|
703
|
+
z: resultQuaternion.z,
|
|
704
|
+
w: resultQuaternion.w
|
|
705
|
+
};
|
|
706
|
+
rigidBody.setRotation(rapierQuat, true);
|
|
707
|
+
} else {
|
|
708
|
+
this.gameObject.rotation.y = this.currentRotationY;
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
this.gameObject.rotation.y = this.currentRotationY;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Utility function to move a value towards a target at a given rate
|
|
716
|
+
*/
|
|
717
|
+
moveTowards(current, target, maxDelta) {
|
|
718
|
+
const delta = target - current;
|
|
719
|
+
if (Math.abs(delta) <= maxDelta) {
|
|
720
|
+
return target;
|
|
721
|
+
}
|
|
722
|
+
return current + Math.sign(delta) * maxDelta;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get current movement state for debugging
|
|
726
|
+
*/
|
|
727
|
+
getMovementState() {
|
|
728
|
+
return {
|
|
729
|
+
maxMoveSpeed: this.maxMoveSpeed,
|
|
730
|
+
acceleration: this.acceleration,
|
|
731
|
+
turnSpeed: this.turnSpeed,
|
|
732
|
+
currentRotationY: this.currentRotationY,
|
|
733
|
+
targetRotationY: this.targetRotationY,
|
|
734
|
+
hasRigidBody: !!this.rigidBodyComponent
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Clean up resources when the component is removed
|
|
739
|
+
*/
|
|
740
|
+
onCleanup() {
|
|
741
|
+
this.rigidBodyComponent = null;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// src/engine/player/PlayerControllerThree.ts
|
|
746
|
+
import * as THREE4 from "three";
|
|
747
|
+
var PlayerControllerThree = class extends Component {
|
|
748
|
+
// Movement parameters
|
|
749
|
+
moveSpeed = 5;
|
|
750
|
+
runSpeed = 8;
|
|
751
|
+
rotationSpeed = 2;
|
|
752
|
+
// Controls configuration
|
|
753
|
+
controls = {
|
|
754
|
+
forward: "KeyW",
|
|
755
|
+
backward: "KeyS",
|
|
756
|
+
left: "KeyA",
|
|
757
|
+
right: "KeyD",
|
|
758
|
+
run: "ShiftLeft",
|
|
759
|
+
interact: "KeyE"
|
|
760
|
+
};
|
|
761
|
+
// Input state
|
|
762
|
+
keys = /* @__PURE__ */ new Set();
|
|
763
|
+
mouseX = 0;
|
|
764
|
+
mouseY = 0;
|
|
765
|
+
mouseSensitivity = 2e-3;
|
|
766
|
+
isPointerLocked = false;
|
|
767
|
+
// Camera reference
|
|
768
|
+
camera;
|
|
769
|
+
cameraHeight = 1.7;
|
|
770
|
+
// Eye level height
|
|
771
|
+
// Movement state
|
|
772
|
+
velocity = new THREE4.Vector3();
|
|
773
|
+
direction = new THREE4.Vector3();
|
|
774
|
+
constructor(camera) {
|
|
775
|
+
super();
|
|
776
|
+
this.camera = camera;
|
|
777
|
+
}
|
|
778
|
+
onCreate() {
|
|
779
|
+
this.setupEventListeners();
|
|
780
|
+
this.setupPointerLock();
|
|
781
|
+
this.updateCameraPosition();
|
|
782
|
+
console.log("\u{1F3AE} Player controller initialized");
|
|
783
|
+
console.log("\u{1F4CB} Controls: WASD to move, Shift to run, E to interact, Click to look around");
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Set up keyboard and mouse event listeners
|
|
787
|
+
*/
|
|
788
|
+
setupEventListeners() {
|
|
789
|
+
document.addEventListener("keydown", this.onKeyDown.bind(this));
|
|
790
|
+
document.addEventListener("keyup", this.onKeyUp.bind(this));
|
|
791
|
+
document.addEventListener("mousemove", this.onMouseMove.bind(this));
|
|
792
|
+
document.addEventListener("click", this.onClick.bind(this));
|
|
793
|
+
document.addEventListener("pointerlockchange", this.onPointerLockChange.bind(this));
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Set up pointer lock for mouse look
|
|
797
|
+
*/
|
|
798
|
+
setupPointerLock() {
|
|
799
|
+
const canvas = document.getElementById("renderCanvas");
|
|
800
|
+
if (canvas) {
|
|
801
|
+
canvas.addEventListener("click", () => {
|
|
802
|
+
if (!this.isPointerLocked) {
|
|
803
|
+
canvas.requestPointerLock();
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Handle key down events
|
|
810
|
+
*/
|
|
811
|
+
onKeyDown(event) {
|
|
812
|
+
this.keys.add(event.code);
|
|
813
|
+
if (event.code === this.controls.interact) {
|
|
814
|
+
this.onInteract();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Handle key up events
|
|
819
|
+
*/
|
|
820
|
+
onKeyUp(event) {
|
|
821
|
+
this.keys.delete(event.code);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Handle mouse movement for looking around
|
|
825
|
+
*/
|
|
826
|
+
onMouseMove(event) {
|
|
827
|
+
if (!this.isPointerLocked) return;
|
|
828
|
+
this.mouseX += event.movementX * this.mouseSensitivity;
|
|
829
|
+
this.mouseY += event.movementY * this.mouseSensitivity;
|
|
830
|
+
this.mouseY = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.mouseY));
|
|
831
|
+
this.camera.rotation.order = "YXZ";
|
|
832
|
+
this.camera.rotation.y = -this.mouseX;
|
|
833
|
+
this.camera.rotation.x = -this.mouseY;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Handle canvas clicks for pointer lock
|
|
837
|
+
*/
|
|
838
|
+
onClick(event) {
|
|
839
|
+
const canvas = document.getElementById("renderCanvas");
|
|
840
|
+
if (event.target === canvas && !this.isPointerLocked) {
|
|
841
|
+
canvas.requestPointerLock();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Handle pointer lock state changes
|
|
846
|
+
*/
|
|
847
|
+
onPointerLockChange() {
|
|
848
|
+
this.isPointerLocked = document.pointerLockElement !== null;
|
|
849
|
+
if (this.isPointerLocked) {
|
|
850
|
+
console.log("\u{1F5B1}\uFE0F Mouse locked - look around!");
|
|
851
|
+
} else {
|
|
852
|
+
console.log("\u{1F5B1}\uFE0F Mouse unlocked - click canvas to lock again");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Handle interaction key press
|
|
857
|
+
*/
|
|
858
|
+
onInteract() {
|
|
859
|
+
console.log("\u{1F91D} Player trying to interact...");
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Update method - called every frame
|
|
863
|
+
*/
|
|
864
|
+
update(deltaTime) {
|
|
865
|
+
this.updateMovement(deltaTime);
|
|
866
|
+
this.updateCameraPosition();
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Update player movement based on input
|
|
870
|
+
*/
|
|
871
|
+
updateMovement(deltaTime) {
|
|
872
|
+
this.direction.set(0, 0, 0);
|
|
873
|
+
const isRunning = this.keys.has(this.controls.run);
|
|
874
|
+
const currentSpeed = isRunning ? this.runSpeed : this.moveSpeed;
|
|
875
|
+
const forward = new THREE4.Vector3();
|
|
876
|
+
const right = new THREE4.Vector3();
|
|
877
|
+
this.camera.getWorldDirection(forward);
|
|
878
|
+
forward.y = 0;
|
|
879
|
+
forward.normalize();
|
|
880
|
+
right.crossVectors(forward, this.camera.up).normalize();
|
|
881
|
+
if (this.keys.has(this.controls.forward)) {
|
|
882
|
+
this.direction.add(forward);
|
|
883
|
+
}
|
|
884
|
+
if (this.keys.has(this.controls.backward)) {
|
|
885
|
+
this.direction.sub(forward);
|
|
886
|
+
}
|
|
887
|
+
if (this.keys.has(this.controls.right)) {
|
|
888
|
+
this.direction.add(right);
|
|
889
|
+
}
|
|
890
|
+
if (this.keys.has(this.controls.left)) {
|
|
891
|
+
this.direction.sub(right);
|
|
892
|
+
}
|
|
893
|
+
if (this.direction.length() > 0) {
|
|
894
|
+
this.direction.normalize();
|
|
895
|
+
this.velocity.copy(this.direction).multiplyScalar(currentSpeed * deltaTime);
|
|
896
|
+
this.gameObject.position.add(this.velocity);
|
|
897
|
+
}
|
|
898
|
+
this.gameObject.position.y = 0;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Update camera position to follow player
|
|
902
|
+
*/
|
|
903
|
+
updateCameraPosition() {
|
|
904
|
+
this.camera.position.copy(this.gameObject.position);
|
|
905
|
+
this.camera.position.y += this.cameraHeight;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Get current movement state for debugging
|
|
909
|
+
*/
|
|
910
|
+
getMovementState() {
|
|
911
|
+
const lookDirection = new THREE4.Vector3();
|
|
912
|
+
this.camera.getWorldDirection(lookDirection);
|
|
913
|
+
return {
|
|
914
|
+
position: this.gameObject.position.clone(),
|
|
915
|
+
isMoving: this.direction.length() > 0,
|
|
916
|
+
isRunning: this.keys.has(this.controls.run),
|
|
917
|
+
lookDirection
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Set player position
|
|
922
|
+
*/
|
|
923
|
+
setPosition(position) {
|
|
924
|
+
this.gameObject.position.copy(position);
|
|
925
|
+
this.updateCameraPosition();
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Clean up event listeners
|
|
929
|
+
*/
|
|
930
|
+
onCleanup() {
|
|
931
|
+
document.removeEventListener("keydown", this.onKeyDown.bind(this));
|
|
932
|
+
document.removeEventListener("keyup", this.onKeyUp.bind(this));
|
|
933
|
+
document.removeEventListener("mousemove", this.onMouseMove.bind(this));
|
|
934
|
+
document.removeEventListener("click", this.onClick.bind(this));
|
|
935
|
+
document.removeEventListener("pointerlockchange", this.onPointerLockChange.bind(this));
|
|
936
|
+
if (this.isPointerLocked) {
|
|
937
|
+
document.exitPointerLock();
|
|
938
|
+
}
|
|
939
|
+
console.log("\u{1F3AE} Player controller cleaned up");
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
// src/engine/render/SkeletalRenderer.ts
|
|
944
|
+
import * as THREE5 from "three";
|
|
945
|
+
var SkeletalRenderer = class extends Component {
|
|
946
|
+
_group = null;
|
|
947
|
+
_assetPath;
|
|
948
|
+
_material;
|
|
949
|
+
_skeletalModel = null;
|
|
950
|
+
constructor(assetPath, material) {
|
|
951
|
+
super();
|
|
952
|
+
this._assetPath = assetPath;
|
|
953
|
+
this._material = material || null;
|
|
954
|
+
}
|
|
955
|
+
onCreate() {
|
|
956
|
+
this.createSkeletalMesh();
|
|
957
|
+
}
|
|
958
|
+
createSkeletalMesh() {
|
|
959
|
+
if (!this._assetPath) {
|
|
960
|
+
throw new Error("SkeletalRenderer: No asset path specified");
|
|
961
|
+
}
|
|
962
|
+
this._group = new THREE5.Group();
|
|
963
|
+
this._group.name = `skeletal_${this._assetPath.split("/").pop()}`;
|
|
964
|
+
this.gameObject.add(this._group);
|
|
965
|
+
this._skeletalModel = AssetManager.getSkeletalClone(this._assetPath);
|
|
966
|
+
if (!this._skeletalModel) {
|
|
967
|
+
throw new Error(
|
|
968
|
+
`No skeletal model found for '${this._assetPath}'. Make sure to preload with AssetManager.preloadSkeletalModel() first.`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
if (this._material) {
|
|
972
|
+
this._skeletalModel.traverse((child) => {
|
|
973
|
+
if (child instanceof THREE5.Mesh) {
|
|
974
|
+
child.material = this._material;
|
|
975
|
+
child.frustumCulled = false;
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
} else {
|
|
979
|
+
this._skeletalModel.traverse((child) => {
|
|
980
|
+
if (child instanceof THREE5.Mesh) {
|
|
981
|
+
child.frustumCulled = false;
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
this._group.add(this._skeletalModel);
|
|
986
|
+
this.applyShadowsToGroup(this._group);
|
|
987
|
+
}
|
|
988
|
+
applyShadowsToGroup(group) {
|
|
989
|
+
group.traverse((child) => {
|
|
990
|
+
if (child instanceof THREE5.Mesh) {
|
|
991
|
+
child.castShadow = true;
|
|
992
|
+
child.receiveShadow = true;
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
// ========== Public API ==========
|
|
997
|
+
/**
|
|
998
|
+
* Get the wrapper group (attached to GameObject)
|
|
999
|
+
*/
|
|
1000
|
+
getGroup() {
|
|
1001
|
+
return this._group;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Get the skeletal model (for animation setup)
|
|
1005
|
+
*/
|
|
1006
|
+
getSkeletalModel() {
|
|
1007
|
+
return this._skeletalModel;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Get the asset path being rendered
|
|
1011
|
+
*/
|
|
1012
|
+
getAssetPath() {
|
|
1013
|
+
return this._assetPath;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Enable or disable visibility
|
|
1017
|
+
*/
|
|
1018
|
+
setVisible(visible) {
|
|
1019
|
+
if (this._group) {
|
|
1020
|
+
this._group.visible = visible;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Get visibility state
|
|
1025
|
+
*/
|
|
1026
|
+
isVisible() {
|
|
1027
|
+
return this._group?.visible ?? false;
|
|
1028
|
+
}
|
|
1029
|
+
// ========== Component Lifecycle ==========
|
|
1030
|
+
onCleanup() {
|
|
1031
|
+
if (this._group) {
|
|
1032
|
+
this.gameObject.remove(this._group);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
onEnabled() {
|
|
1036
|
+
this.setVisible(true);
|
|
1037
|
+
}
|
|
1038
|
+
onDisabled() {
|
|
1039
|
+
this.setVisible(false);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
// src/engine/render/InstancedRenderer.ts
|
|
1044
|
+
var InstancedRenderer = class extends Component {
|
|
1045
|
+
batchKey;
|
|
1046
|
+
options;
|
|
1047
|
+
instanceId = null;
|
|
1048
|
+
/**
|
|
1049
|
+
* Create an InstancedRenderer
|
|
1050
|
+
* @param batchKey The batch key to register with. If no batch exists, one is auto-created.
|
|
1051
|
+
* @param options Configuration options (or just pass `true`/`false` for isDynamic)
|
|
1052
|
+
*/
|
|
1053
|
+
constructor(batchKey, options = {}) {
|
|
1054
|
+
super();
|
|
1055
|
+
this.batchKey = batchKey;
|
|
1056
|
+
this.options = typeof options === "boolean" ? { isDynamic: options } : options;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Register with the batch when the component is created.
|
|
1060
|
+
* If no batch exists, one will be created automatically from this GameObject's mesh.
|
|
1061
|
+
*/
|
|
1062
|
+
onCreate() {
|
|
1063
|
+
const manager = InstancedMeshManager.getInstance();
|
|
1064
|
+
if (!manager.isReady()) {
|
|
1065
|
+
console.error(
|
|
1066
|
+
`InstancedRenderer: Manager not initialized. Call InstancedMeshManager.getInstance().initialize(scene) first.`
|
|
1067
|
+
);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
this.instanceId = manager.addInstance(this.batchKey, this.gameObject, {
|
|
1071
|
+
isDynamic: this.options.isDynamic ?? true,
|
|
1072
|
+
castShadow: this.options.castShadow ?? false,
|
|
1073
|
+
receiveShadow: this.options.receiveShadow ?? false,
|
|
1074
|
+
initialCapacity: this.options.initialCapacity
|
|
1075
|
+
});
|
|
1076
|
+
if (!this.instanceId) {
|
|
1077
|
+
console.error(`InstancedRenderer: Failed to add instance to batch '${this.batchKey}'`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Set the visibility of this instance
|
|
1082
|
+
*/
|
|
1083
|
+
setVisible(visible) {
|
|
1084
|
+
if (this.instanceId) {
|
|
1085
|
+
InstancedMeshManager.getInstance().setInstanceVisible(this.batchKey, this.instanceId, visible);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Get the visibility of this instance
|
|
1090
|
+
*/
|
|
1091
|
+
getVisible() {
|
|
1092
|
+
if (this.instanceId) {
|
|
1093
|
+
return InstancedMeshManager.getInstance().getInstanceVisible(this.batchKey, this.instanceId);
|
|
1094
|
+
}
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Show the instance (convenience method)
|
|
1099
|
+
*/
|
|
1100
|
+
show() {
|
|
1101
|
+
this.setVisible(true);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Hide the instance (convenience method)
|
|
1105
|
+
*/
|
|
1106
|
+
hide() {
|
|
1107
|
+
this.setVisible(false);
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Get the batch key this renderer is registered with
|
|
1111
|
+
*/
|
|
1112
|
+
getBatchKey() {
|
|
1113
|
+
return this.batchKey;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Check if this renderer is successfully registered with a batch
|
|
1117
|
+
*/
|
|
1118
|
+
isRegistered() {
|
|
1119
|
+
return this.instanceId !== null;
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Get the instance ID (for debugging)
|
|
1123
|
+
*/
|
|
1124
|
+
getInstanceId() {
|
|
1125
|
+
return this.instanceId;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Mark this instance as needing a matrix update.
|
|
1129
|
+
* Only relevant for static instances - dynamic instances update every frame anyway.
|
|
1130
|
+
* Call this when the transform of a static instance changes.
|
|
1131
|
+
*/
|
|
1132
|
+
markDirty() {
|
|
1133
|
+
if (this.instanceId) {
|
|
1134
|
+
InstancedMeshManager.getInstance().markInstanceDirty(this.batchKey, this.instanceId);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Set whether this instance is dynamic (updates every frame) or static (only when marked dirty).
|
|
1139
|
+
* Use this to optimize performance when items transition between moving and stationary states.
|
|
1140
|
+
* @param isDynamic If true, updates every frame. If false, only updates when markDirty() is called.
|
|
1141
|
+
*/
|
|
1142
|
+
setDynamic(isDynamic) {
|
|
1143
|
+
if (this.instanceId) {
|
|
1144
|
+
InstancedMeshManager.getInstance().setInstanceDynamic(
|
|
1145
|
+
this.batchKey,
|
|
1146
|
+
this.instanceId,
|
|
1147
|
+
isDynamic
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Called when the GameObject becomes enabled
|
|
1153
|
+
*/
|
|
1154
|
+
onEnabled() {
|
|
1155
|
+
this.setVisible(true);
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Called when the GameObject becomes disabled
|
|
1159
|
+
*/
|
|
1160
|
+
onDisabled() {
|
|
1161
|
+
this.setVisible(false);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Unregister from the batch when the component is cleaned up
|
|
1165
|
+
*/
|
|
1166
|
+
onCleanup() {
|
|
1167
|
+
if (this.instanceId) {
|
|
1168
|
+
InstancedMeshManager.getInstance().removeInstance(this.batchKey, this.instanceId);
|
|
1169
|
+
this.instanceId = null;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// src/engine/render/MeshRenderer.ts
|
|
1175
|
+
import * as THREE6 from "three";
|
|
1176
|
+
var MeshRenderer = class extends Component {
|
|
1177
|
+
static fromPrefabJSON(json, _node) {
|
|
1178
|
+
if (!json.mesh?.assetId) {
|
|
1179
|
+
console.error(`[MeshRenderer] stow_mesh component missing mesh.assetId:`, json);
|
|
1180
|
+
return new MeshRenderer("unknown");
|
|
1181
|
+
}
|
|
1182
|
+
const options = PrefabInstance.currentOptions;
|
|
1183
|
+
const castShadow = json.castShadow ?? options?.castShadow ?? true;
|
|
1184
|
+
const receiveShadow = json.receiveShadow ?? options?.receiveShadow ?? true;
|
|
1185
|
+
return new MeshRenderer(json.mesh.assetId, castShadow, receiveShadow);
|
|
1186
|
+
}
|
|
1187
|
+
mesh = null;
|
|
1188
|
+
meshName;
|
|
1189
|
+
castShadow;
|
|
1190
|
+
receiveShadow;
|
|
1191
|
+
_isStatic;
|
|
1192
|
+
isMeshLoaded = false;
|
|
1193
|
+
materialOverride = null;
|
|
1194
|
+
/**
|
|
1195
|
+
* @param meshName The name of the mesh in the StowKit pack
|
|
1196
|
+
* @param castShadow Whether meshes should cast shadows (default: true)
|
|
1197
|
+
* @param receiveShadow Whether meshes should receive shadows (default: true)
|
|
1198
|
+
* @param isStatic Whether this mesh is static (default: false). Static meshes have matrixAutoUpdate disabled for better performance.
|
|
1199
|
+
* @param materialOverride Optional material to use instead of the default StowKit material
|
|
1200
|
+
*/
|
|
1201
|
+
constructor(meshName, castShadow = true, receiveShadow = true, isStatic = false, materialOverride = null) {
|
|
1202
|
+
super();
|
|
1203
|
+
this.meshName = meshName;
|
|
1204
|
+
this.castShadow = castShadow;
|
|
1205
|
+
this.receiveShadow = receiveShadow;
|
|
1206
|
+
this._isStatic = isStatic;
|
|
1207
|
+
this.materialOverride = materialOverride;
|
|
1208
|
+
}
|
|
1209
|
+
onCreate() {
|
|
1210
|
+
const stowkit = StowKitSystem.getInstance();
|
|
1211
|
+
const cachedMesh = stowkit.getMeshSync(this.meshName);
|
|
1212
|
+
if (cachedMesh) {
|
|
1213
|
+
this.addMesh(cachedMesh);
|
|
1214
|
+
} else {
|
|
1215
|
+
stowkit.getMesh(this.meshName);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
update(_deltaTime) {
|
|
1219
|
+
if (this.isMeshLoaded) return;
|
|
1220
|
+
const stowkit = StowKitSystem.getInstance();
|
|
1221
|
+
const cachedMesh = stowkit.getMeshSync(this.meshName);
|
|
1222
|
+
if (cachedMesh) {
|
|
1223
|
+
this.addMesh(cachedMesh);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
addMesh(original) {
|
|
1227
|
+
this.isMeshLoaded = true;
|
|
1228
|
+
this.mesh = StowKitSystem.getInstance().cloneMeshSync(
|
|
1229
|
+
original,
|
|
1230
|
+
this.castShadow,
|
|
1231
|
+
this.receiveShadow
|
|
1232
|
+
);
|
|
1233
|
+
if (this.materialOverride) {
|
|
1234
|
+
this.applyMaterialOverride();
|
|
1235
|
+
}
|
|
1236
|
+
this.gameObject.add(this.mesh);
|
|
1237
|
+
if (this._isStatic) {
|
|
1238
|
+
this.setStatic(true);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Apply the material override to all meshes
|
|
1243
|
+
*/
|
|
1244
|
+
applyMaterialOverride() {
|
|
1245
|
+
if (!this.mesh || !this.materialOverride) return;
|
|
1246
|
+
this.mesh.traverse((child) => {
|
|
1247
|
+
if (child instanceof THREE6.Mesh) {
|
|
1248
|
+
child.material = this.materialOverride;
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Set a material override for all meshes in this renderer.
|
|
1254
|
+
* Call this after the mesh is loaded, or pass it in the constructor.
|
|
1255
|
+
*/
|
|
1256
|
+
setMaterial(material) {
|
|
1257
|
+
this.materialOverride = material;
|
|
1258
|
+
if (this.mesh) {
|
|
1259
|
+
this.applyMaterialOverride();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Check if this mesh is currently static (no automatic matrix updates)
|
|
1264
|
+
*/
|
|
1265
|
+
get isStatic() {
|
|
1266
|
+
return this._isStatic;
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Set whether this mesh is static (no automatic matrix updates).
|
|
1270
|
+
* Static meshes save CPU by not recalculating transforms every frame.
|
|
1271
|
+
* Call forceMatrixUpdate() after moving a static mesh.
|
|
1272
|
+
*/
|
|
1273
|
+
setStatic(isStatic) {
|
|
1274
|
+
this._isStatic = isStatic;
|
|
1275
|
+
if (!this.mesh) return;
|
|
1276
|
+
if (isStatic) {
|
|
1277
|
+
this.forceMatrixUpdate();
|
|
1278
|
+
this.mesh.matrixAutoUpdate = false;
|
|
1279
|
+
this.mesh.traverse((child) => {
|
|
1280
|
+
child.matrixAutoUpdate = false;
|
|
1281
|
+
});
|
|
1282
|
+
this.gameObject.matrixAutoUpdate = false;
|
|
1283
|
+
} else {
|
|
1284
|
+
this.mesh.matrixAutoUpdate = true;
|
|
1285
|
+
this.mesh.traverse((child) => {
|
|
1286
|
+
child.matrixAutoUpdate = true;
|
|
1287
|
+
});
|
|
1288
|
+
this.gameObject.matrixAutoUpdate = true;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Force a one-time matrix update. Call this after moving a static mesh.
|
|
1293
|
+
* Does not change the static/dynamic state.
|
|
1294
|
+
*/
|
|
1295
|
+
forceMatrixUpdate() {
|
|
1296
|
+
if (this.mesh) {
|
|
1297
|
+
this.mesh.updateMatrix();
|
|
1298
|
+
this.mesh.updateMatrixWorld(true);
|
|
1299
|
+
}
|
|
1300
|
+
this.gameObject.updateMatrix();
|
|
1301
|
+
this.gameObject.updateMatrixWorld(true);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Get the mesh group (null if not yet loaded)
|
|
1305
|
+
*/
|
|
1306
|
+
getMesh() {
|
|
1307
|
+
return this.mesh;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Get the name of the mesh this component is managing
|
|
1311
|
+
*/
|
|
1312
|
+
getMeshName() {
|
|
1313
|
+
return this.meshName;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Check if the mesh was successfully loaded
|
|
1317
|
+
*/
|
|
1318
|
+
isLoaded() {
|
|
1319
|
+
return this.mesh !== null;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Set the visibility of the mesh
|
|
1323
|
+
*/
|
|
1324
|
+
setVisible(visible) {
|
|
1325
|
+
if (this.mesh) {
|
|
1326
|
+
this.mesh.visible = visible;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Get bounds of the mesh (useful for physics)
|
|
1331
|
+
*/
|
|
1332
|
+
getBounds() {
|
|
1333
|
+
if (!this.mesh) {
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
return StowKitSystem.getInstance().getBounds(this.mesh);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Cleanup - remove mesh from scene and dispose of resources
|
|
1340
|
+
*/
|
|
1341
|
+
onCleanup() {
|
|
1342
|
+
if (this.mesh) {
|
|
1343
|
+
this.gameObject.remove(this.mesh);
|
|
1344
|
+
this.mesh.traverse((child) => {
|
|
1345
|
+
if (child instanceof THREE6.Mesh) {
|
|
1346
|
+
if (child.geometry) {
|
|
1347
|
+
child.geometry.dispose();
|
|
1348
|
+
}
|
|
1349
|
+
if (child.material) {
|
|
1350
|
+
if (Array.isArray(child.material)) {
|
|
1351
|
+
child.material.forEach((m) => {
|
|
1352
|
+
if (m.map) m.map.dispose();
|
|
1353
|
+
m.dispose();
|
|
1354
|
+
});
|
|
1355
|
+
} else {
|
|
1356
|
+
if (child.material.map) child.material.map.dispose();
|
|
1357
|
+
child.material.dispose();
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
this.mesh = null;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
MeshRenderer = __decorateClass([
|
|
1367
|
+
PrefabComponent("stow_mesh")
|
|
1368
|
+
], MeshRenderer);
|
|
1369
|
+
|
|
1370
|
+
// src/index.ts
|
|
1371
|
+
export * from "@series-inc/rundot-game-sdk";
|
|
1372
|
+
import { default as default2 } from "@series-inc/rundot-game-sdk/api";
|
|
1373
|
+
export {
|
|
1374
|
+
AssetManager,
|
|
1375
|
+
Component,
|
|
1376
|
+
ComponentUpdater,
|
|
1377
|
+
GameObject,
|
|
1378
|
+
InstancedMeshManager,
|
|
1379
|
+
InstancedRenderer,
|
|
1380
|
+
InteractionZone,
|
|
1381
|
+
MeshRenderer,
|
|
1382
|
+
MovementController,
|
|
1383
|
+
PlayerControllerThree,
|
|
1384
|
+
default2 as RundotGameAPI,
|
|
1385
|
+
SkeletalRenderer,
|
|
1386
|
+
SkeletonCache,
|
|
1387
|
+
VenusGame,
|
|
1388
|
+
VirtualJoystickThree
|
|
1389
|
+
};
|
|
1390
|
+
//# sourceMappingURL=index.js.map
|