react-three-game 0.0.60 → 0.0.61

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 (58) hide show
  1. package/package.json +9 -3
  2. package/.gitattributes +0 -2
  3. package/.github/copilot-instructions.md +0 -83
  4. package/.github/workflows/nextjs.yml +0 -99
  5. package/.gitmodules +0 -3
  6. package/assets/architecture.png +0 -0
  7. package/assets/editor.gif +0 -0
  8. package/assets/favicon.ico +0 -0
  9. package/assets/react-three-game-logo.png +0 -0
  10. package/dist/tools/dragdrop/page.d.ts +0 -1
  11. package/dist/tools/dragdrop/page.js +0 -11
  12. package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
  13. package/dist/tools/prefabeditor/EntityEvents.js +0 -85
  14. package/dist/tools/prefabeditor/page.d.ts +0 -1
  15. package/dist/tools/prefabeditor/page.js +0 -5
  16. package/react-three-game-skill/.gitattributes +0 -2
  17. package/react-three-game-skill/README.md +0 -7
  18. package/react-three-game-skill/react-three-game/SKILL.md +0 -514
  19. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
  20. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
  21. package/src/helpers/SoundManager.ts +0 -130
  22. package/src/helpers/index.ts +0 -91
  23. package/src/index.ts +0 -59
  24. package/src/shared/ContactShadow.tsx +0 -74
  25. package/src/shared/GameCanvas.tsx +0 -52
  26. package/src/tools/assetviewer/page.tsx +0 -425
  27. package/src/tools/dragdrop/DragDropLoader.tsx +0 -159
  28. package/src/tools/dragdrop/index.ts +0 -4
  29. package/src/tools/dragdrop/modelLoader.ts +0 -204
  30. package/src/tools/dragdrop/page.tsx +0 -45
  31. package/src/tools/prefabeditor/Dropdown.tsx +0 -112
  32. package/src/tools/prefabeditor/EditorContext.tsx +0 -25
  33. package/src/tools/prefabeditor/EditorTree.tsx +0 -452
  34. package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
  35. package/src/tools/prefabeditor/EditorUI.tsx +0 -204
  36. package/src/tools/prefabeditor/EventSystem.tsx +0 -36
  37. package/src/tools/prefabeditor/GameEvents.ts +0 -191
  38. package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
  39. package/src/tools/prefabeditor/PrefabEditor.tsx +0 -256
  40. package/src/tools/prefabeditor/PrefabRoot.tsx +0 -767
  41. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
  42. package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
  43. package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
  44. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
  45. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
  46. package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
  47. package/src/tools/prefabeditor/components/Input.tsx +0 -820
  48. package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
  49. package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
  50. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
  51. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
  52. package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
  53. package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
  54. package/src/tools/prefabeditor/components/index.ts +0 -26
  55. package/src/tools/prefabeditor/page.tsx +0 -10
  56. package/src/tools/prefabeditor/styles.ts +0 -235
  57. package/src/tools/prefabeditor/types.ts +0 -20
  58. package/src/tools/prefabeditor/utils.ts +0 -312
@@ -1,472 +0,0 @@
1
- # Advanced Physics & Patterns
2
-
3
- ## Physics Type Decision Tree
4
-
5
- ```
6
- Need physics?
7
- ├─ No → Don't add Physics component
8
- └─ Yes → Does it move?
9
- ├─ Never moves (walls, floor, static props)
10
- │ └─ type: "fixed"
11
-
12
- ├─ Moves via forces/gravity (balls, boxes, ragdolls)
13
- │ └─ type: "dynamic"
14
- │ ├─ Fast moving? → ccd: true
15
- │ └─ Heavy? → mass: 10+
16
-
17
- ├─ Scripted animation (moving platforms, doors)
18
- │ └─ type: "kinematicPosition"
19
- │ └─ Update transform via updateNodeById
20
-
21
- └─ Velocity-driven (conveyor belts, wind zones)
22
- └─ type: "kinematicVelocity"
23
- └─ Set velocity via RigidBody ref
24
- ```
25
-
26
- **Type descriptions**:
27
- - **fixed**: Immovable, infinite mass (ground, walls, buildings)
28
- - **dynamic**: Affected by forces and gravity (player, projectiles, props)
29
- - **kinematicPosition**: Move via code, push dynamic bodies (elevators, doors)
30
- - **kinematicVelocity**: Set constant velocity, push dynamic bodies (conveyors)
31
-
32
- **Performance tip**: Use `fixed` for anything that never moves - it's cheapest.
33
-
34
- ## Physics Material Properties
35
-
36
- Complete reference for `Physics` component properties:
37
-
38
- | Property | Type | Default | Description |
39
- |----------|------|---------|-------------|
40
- | `type` | `'dynamic'` \| `'fixed'` \| `'kinematicPosition'` \| `'kinematicVelocity'` | `'dynamic'` | Body type (see decision tree above) |
41
- | `mass` | `number` | `1` | Body mass (dynamic only) |
42
- | `restitution` | `number` | `0` | Bounciness (0 = no bounce, 1 = perfect bounce) |
43
- | `friction` | `number` | `0.5` | Surface friction (0 = ice, 1+ = sticky) |
44
- | `linearDamping` | `number` | `0` | Velocity decay (0 = none, 1 = full stop) |
45
- | `angularDamping` | `number` | `0` | Rotation decay |
46
- | `gravityScale` | `number` | `1` | Gravity multiplier (0 = floating, 2 = heavy) |
47
- | `lockTranslations` | `boolean` | `false` | Freeze position |
48
- | `lockRotations` | `boolean` | `false` | Freeze rotation |
49
- | `enabledTranslations` | `[bool, bool, bool]` | `[true, true, true]` | Lock per axis (X, Y, Z) |
50
- | `enabledRotations` | `[bool, bool, bool]` | `[true, true, true]` | Lock rotation per axis |
51
- | `colliders` | `'hull'` \| `'trimesh'` \| `'cuboid'` \| `'ball'` | auto | Collider shape override (`fixed` defaults to `trimesh`, others to `hull`) |
52
- | `ccd` | `boolean` | `false` | Continuous collision detection (fast objects) |
53
- | `sensor` | `boolean` | `false` | Trigger only, no collision response |
54
- | `activeCollisionTypes` | `'all'` | - | Enable kinematic/fixed collision detection (default: dynamic only) |
55
- | `collisionGroups` | `number` | - | Rapier collision groups bitfield |
56
- | `solverGroups` | `number` | - | Rapier solver groups bitfield |
57
-
58
- **Example - Bouncy Ball**:
59
- ```json
60
- {
61
- "physics": {
62
- "type": "Physics",
63
- "properties": {
64
- "type": "dynamic",
65
- "mass": 0.5,
66
- "restitution": 0.9,
67
- "friction": 0.1,
68
- "linearDamping": 0.05
69
- }
70
- }
71
- }
72
- ```
73
-
74
- **Example - Ice Surface**:
75
- ```json
76
- {
77
- "physics": {
78
- "type": "Physics",
79
- "properties": {
80
- "type": "fixed",
81
- "friction": 0,
82
- "restitution": 0.1
83
- }
84
- }
85
- }
86
- ```
87
-
88
- ## Force & Impulse Application
89
-
90
- Access RigidBody refs via `PrefabRootRef.rigidBodyRefs` to apply physics forces to prefab objects:
91
-
92
- ```tsx
93
- import { useRef, useEffect } from 'react';
94
- import { PrefabEditor } from 'react-three-game';
95
- import type { PrefabEditorRef } from 'react-three-game';
96
- import type { RapierRigidBody } from '@react-three/rapier';
97
-
98
- function ForceApplier({ editorRef }: { editorRef: React.RefObject<PrefabEditorRef> }) {
99
- useEffect(() => {
100
- const interval = setInterval(() => {
101
- const rootRef = editorRef.current?.rootRef.current;
102
- if (!rootRef) return;
103
-
104
- // Access RigidBody ref by node ID
105
- const rigidBody = rootRef.rigidBodyRefs.get('ball') as RapierRigidBody;
106
-
107
- if (rigidBody) {
108
- // Apply upward impulse
109
- rigidBody.applyImpulse({ x: 0, y: 5, z: 0 }, true);
110
-
111
- // Or apply continuous force
112
- rigidBody.addForce({ x: 0, y: 10, z: 0 }, true);
113
-
114
- // Apply torque
115
- rigidBody.applyTorqueImpulse({ x: 0, y: 1, z: 0 }, true);
116
- }
117
- }, 2000);
118
-
119
- return () => clearInterval(interval);
120
- }, [editorRef]);
121
-
122
- return null;
123
- }
124
-
125
- function Scene() {
126
- const editorRef = useRef<PrefabEditorRef>(null);
127
-
128
- return (
129
- <PrefabEditor ref={editorRef} initialPrefab={prefab}>
130
- <ForceApplier editorRef={editorRef} />
131
- </PrefabEditor>
132
- );
133
- }
134
- ```
135
-
136
- **Alternative: Custom R3F components**
137
-
138
- For fully custom physics objects, create R3F components with their own RigidBody refs:
139
-
140
- ```tsx
141
- import { useRef } from 'react';
142
- import { RigidBody } from '@react-three/rapier';
143
- import type { RapierRigidBody } from '@react-three/rapier';
144
- import { useFrame } from '@react-three/fiber';
145
- import { PrefabEditor } from 'react-three-game';
146
-
147
- function PhysicsBall() {
148
- const rigidBodyRef = useRef<RapierRigidBody>(null);
149
-
150
- useFrame(() => {
151
- if (rigidBodyRef.current) {
152
- // Apply jump force on interval
153
- rigidBodyRef.current.applyImpulse({ x: 0, y: 5, z: 0 }, true);
154
- }
155
- });
156
-
157
- return (
158
- <RigidBody ref={rigidBodyRef} position={[0, 5, 0]} type="dynamic">
159
- <mesh castShadow>
160
- <sphereGeometry args={[0.5, 32, 32]} />
161
- <meshStandardMaterial color="orange" />
162
- </mesh>
163
- </RigidBody>
164
- );
165
- }
166
-
167
- <PrefabEditor initialPrefab={prefab}>
168
- <PhysicsBall />
169
- </PrefabEditor>
170
- ```
171
-
172
- **Alternative: Kinematic position updates**
173
-
174
- For smooth animated movement without forces, use `kinematicPosition` and update via `updateNodeById`:
175
-
176
- ```tsx
177
- import { useRef } from 'react';
178
- import { useFrame } from '@react-three/fiber';
179
- import { PrefabEditor, updateNodeById } from 'react-three-game';
180
- import type { PrefabEditorRef } from 'react-three-game';
181
-
182
- function KinematicMover({ editorRef }: { editorRef: React.RefObject<PrefabEditorRef> }) {
183
- useFrame(({ clock }) => {
184
- const prefab = editorRef.current?.prefab;
185
- if (!prefab) return;
186
-
187
- const y = 2 + Math.sin(clock.elapsedTime * 2) * 3;
188
-
189
- const newRoot = updateNodeById(prefab.root, "platform", node => ({
190
- ...node,
191
- components: {
192
- ...node.components,
193
- transform: {
194
- ...node.components!.transform!,
195
- properties: {
196
- ...node.components!.transform!.properties,
197
- position: [0, y, 0]
198
- }
199
- }
200
- }
201
- }));
202
-
203
- editorRef.current!.setPrefab({ ...prefab, root: newRoot });
204
- });
205
-
206
- return null;
207
- }
208
- ```
209
-
210
- **Rapier RigidBody methods**:
211
- - `applyImpulse(vector, wakeUp)` - Instantaneous velocity change
212
- - `addForce(vector, wakeUp)` - Continuous force application
213
- - `applyTorqueImpulse(vector, wakeUp)` - Rotational impulse
214
- - `addTorque(vector, wakeUp)` - Continuous torque
215
- - `setLinvel(vector, wakeUp)` - Set linear velocity directly
216
- - `setAngvel(vector, wakeUp)` - Set angular velocity directly
217
-
218
- ## Tilted Surfaces & Containment
219
-
220
- **⚠️ Tilted walls don't contain objects** - physics objects slide off angled surfaces.
221
-
222
- ### ❌ Wrong Approach
223
- ```json
224
- {
225
- "id": "tilted-wall",
226
- "components": {
227
- "transform": { "type": "Transform", "properties": { "rotation": [0, 0, 0.3] } },
228
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [10, 5, 1] } },
229
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
230
- }
231
- }
232
- ```
233
- Objects will **slide off** the tilted surface.
234
-
235
- ### ✅ Correct Pattern - Perpendicular Walls
236
- ```json
237
- {
238
- "id": "container",
239
- "children": [
240
- {
241
- "id": "floor",
242
- "components": {
243
- "transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
244
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [20, 1, 20] } },
245
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
246
- }
247
- },
248
- {
249
- "id": "wall-north",
250
- "components": {
251
- "transform": { "type": "Transform", "properties": { "position": [0, 2.5, -10] } },
252
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [20, 5, 1] } },
253
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
254
- }
255
- },
256
- {
257
- "id": "wall-south",
258
- "components": {
259
- "transform": { "type": "Transform", "properties": { "position": [0, 2.5, 10] } },
260
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [20, 5, 1] } },
261
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
262
- }
263
- },
264
- {
265
- "id": "wall-east",
266
- "components": {
267
- "transform": { "type": "Transform", "properties": { "position": [10, 2.5, 0] } },
268
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [1, 5, 20] } },
269
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
270
- }
271
- },
272
- {
273
- "id": "wall-west",
274
- "components": {
275
- "transform": { "type": "Transform", "properties": { "position": [-10, 2.5, 0] } },
276
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [1, 5, 20] } },
277
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
278
- }
279
- }
280
- ]
281
- }
282
- ```
283
-
284
- **Key principle**: Walls must be **perpendicular to gravity** to contain dynamic objects.
285
-
286
- ## Instanced Physics
287
-
288
- When using `"instanced": true` on models, physics behaves differently than standard objects. Physics instancing is designed for batched `fixed` and `dynamic` bodies, where instances of the same model share an `InstancedRigidBodies` path for better performance.
289
-
290
- ### Standard vs Instanced Physics
291
-
292
- | Aspect | Standard Physics | Instanced Physics |
293
- |--------|------------------|-------------------|
294
- | RigidBody Component | Individual `<RigidBody>` per object | Single `<InstancedRigidBodies>` group per model + supported physics type |
295
- | Ref Access | `rigidBodyRefs.get(nodeId)` returns single RigidBody | Not accessible via `rigidBodyRefs` |
296
- | Force Application | Direct per-object | Must access via InstancedRigidBodies ref |
297
- | Collider Type | `hull` (dynamic) or `trimesh` (fixed) | Auto-selected by instanced physics path |
298
- | Performance | One draw call per object | One draw call for all instances |
299
-
300
- ### Defining Instanced Objects
301
-
302
- Set `"instanced": true` in the model component. **Instances with the same model path and supported physics type are automatically batched**:
303
-
304
- ```json
305
- {
306
- "id": "tree1",
307
- "components": {
308
- "transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
309
- "model": { "type": "Model", "properties": { "filename": "models/tree.glb", "instanced": true } },
310
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
311
- }
312
- }
313
- ```
314
-
315
- Add multiple instances - they'll be automatically batched:
316
-
317
- ```json
318
- {
319
- "id": "tree2",
320
- "components": {
321
- "transform": { "type": "Transform", "properties": { "position": [5, 0, 3] } },
322
- "model": { "type": "Model", "properties": { "filename": "models/tree.glb", "instanced": true } },
323
- "physics": { "type": "Physics", "properties": { "type": "fixed" } }
324
- }
325
- }
326
- ```
327
-
328
- ### Force Application on Instanced Objects
329
-
330
- **Instanced physics bodies are not individually accessible.** For objects requiring force/impulse control, kinematic motion, or per-body refs, use non-instanced physics (`"instanced": false` or omit the property).
331
-
332
- ### When to Use Instanced Physics
333
-
334
- ✅ **Good for:**
335
- - Many copies of the same static object (trees, rocks, buildings)
336
- - Large scenes with 100+ similar objects
337
- - Fixed physics bodies that never move
338
- - Background props and decorations
339
-
340
- ❌ **Avoid for:**
341
- - Objects requiring individual force/impulse control
342
- - Dynamic objects with unique behaviors
343
- - Objects that need to be individually removed/spawned
344
- - Fewer than ~20 instances (overhead not worth it)
345
-
346
- ### Performance Notes
347
-
348
- - **Batching**: All instances with the same `filename` and `physics.type` are rendered in a single draw call
349
- - **Supported body types**: The instanced physics path is intended for `fixed` and `dynamic` bodies; use standard non-instanced physics for kinematic bodies
350
- - **Scale handling**: Visual scale is applied per-instance, but collider scale may differ
351
- - **Transform updates**: Use `updateNodeById` to move instances (triggers re-sync)
352
- - **Memory**: One set of GPU buffers shared across all instances
353
-
354
- ## Sensors & Collision Events
355
-
356
- Sensors are colliders that detect intersections without generating physical contact forces. Use them for trigger zones, pickup areas, damage zones, and gameplay triggers.
357
-
358
- ### Creating a Sensor
359
-
360
- Set `sensor: true` in the Physics component:
361
-
362
- ```json
363
- {
364
- "id": "trigger-zone",
365
- "components": {
366
- "transform": { "type": "Transform", "properties": { "position": [0, 1, 0] } },
367
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [4, 2, 4] } },
368
- "physics": { "type": "Physics", "properties": { "type": "fixed", "sensor": true } }
369
- }
370
- }
371
- ```
372
-
373
- **Kinematic/Fixed Collision Detection**: By default, sensors only detect `dynamic` bodies. For kinematic sensors (like bullets) or to detect kinematic players, add `"activeCollisionTypes": "all"`:
374
-
375
- ```json
376
- {
377
- "physics": {
378
- "type": "Physics",
379
- "properties": {
380
- "type": "kinematicPosition",
381
- "sensor": true,
382
- "activeCollisionTypes": "all" // Detects walls, floors, kinematic bodies
383
- }
384
- }
385
- }
386
- ```
387
-
388
- ### Physics Event Payload
389
-
390
- All physics events include:
391
-
392
- ```typescript
393
- {
394
- sourceEntityId: string; // The prefab entity that owns the collider
395
- targetEntityId: string | null; // The other entity (if it's a prefab entity)
396
- targetRigidBody: RapierRigidBody; // Direct access to the other RigidBody
397
- }
398
- ```
399
-
400
- `targetEntityId` is `null` when colliding with non-prefab physics bodies (custom R3F components). Use `targetRigidBody` to inspect those.
401
-
402
- ### Common Sensor Patterns
403
-
404
- **Pickup Item:**
405
- ```json
406
- {
407
- "id": "coin",
408
- "components": {
409
- "transform": { "type": "Transform", "properties": { "position": [5, 0.5, 0] } },
410
- "model": { "type": "Model", "properties": { "filename": "models/coin.glb" } },
411
- "physics": { "type": "Physics", "properties": { "type": "fixed", "sensor": true } }
412
- }
413
- }
414
- ```
415
-
416
- ```tsx
417
- useGameEvent('sensor:enter', (payload) => {
418
- if (payload.sourceEntityId === 'coin' && payload.targetEntityId === 'player') {
419
- removeCoin();
420
- gameEvents.emit('score:change', { delta: 100, total: score + 100 });
421
- }
422
- }, [score]);
423
- ```
424
-
425
- **Damage Zone:**
426
- ```json
427
- {
428
- "id": "lava",
429
- "components": {
430
- "transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
431
- "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [10, 0.5, 10] } },
432
- "material": { "type": "Material", "properties": { "color": "#ff4400" } },
433
- "physics": { "type": "Physics", "properties": { "type": "fixed", "sensor": true } }
434
- }
435
- }
436
- ```
437
-
438
- ```tsx
439
- useGameEvent('sensor:enter', ({ sourceEntityId, targetEntityId }) => {
440
- if (sourceEntityId === 'lava') {
441
- gameEvents.emit('player:damage', { entityId: targetEntityId, amount: 50 });
442
- }
443
- }, []);
444
- ```
445
-
446
- **Level Transition:**
447
- ```tsx
448
- useGameEvent('sensor:enter', ({ sourceEntityId, targetEntityId }) => {
449
- if (sourceEntityId === 'exit-door' && targetEntityId === 'player') {
450
- loadNextLevel();
451
- }
452
- }, []);
453
- ```
454
-
455
- ### Interop with Custom R3F Physics
456
-
457
- For custom RigidBody components to participate in the event system, set `userData.entityId`:
458
-
459
- ```tsx
460
- <RigidBody userData={{ entityId: 'player' }}>
461
- <PlayerMesh />
462
- </RigidBody>
463
- ```
464
-
465
- Now when prefab sensors detect this body, `targetEntityId` will be `'player'`.
466
-
467
- ### Tips
468
-
469
- - Sensors fire events for **all** intersecting bodies - filter by ID
470
- - `sensor:exit` fires when something leaves a sensor zone
471
- - `collision:enter/exit` fires for non-sensor physics bodies
472
- - Entity IDs stored in `RigidBody.userData.entityId`
@@ -1,6 +0,0 @@
1
- For large scenes, bake the shadows.
2
-
3
- // trigger a shadow update when needed
4
- envMesh.castShadow = true;
5
- directionalLight.current.shadow.autoUpdate = false;
6
- directionalLight.current.shadow.needsUpdate = true;
@@ -1,130 +0,0 @@
1
-
2
- class SoundManager {
3
- private static _instance: SoundManager | null = null
4
-
5
- public context: AudioContext
6
- private buffers = new Map<string, AudioBuffer>()
7
-
8
- private masterGain: GainNode
9
- private sfxGain: GainNode
10
- private musicGain: GainNode
11
-
12
- private constructor() {
13
- const AudioCtx =
14
- window.AudioContext || (window as any).webkitAudioContext
15
-
16
- this.context = new AudioCtx()
17
-
18
- this.masterGain = this.context.createGain()
19
- this.sfxGain = this.context.createGain()
20
- this.musicGain = this.context.createGain()
21
-
22
- this.sfxGain.connect(this.masterGain)
23
- this.musicGain.connect(this.masterGain)
24
- this.masterGain.connect(this.context.destination)
25
-
26
- this.masterGain.gain.value = 1
27
- this.sfxGain.gain.value = 1
28
- this.musicGain.gain.value = 1
29
- }
30
-
31
- /** Singleton accessor */
32
- static get instance(): SoundManager {
33
- if (typeof window === 'undefined') {
34
- // Return a dummy instance for SSR
35
- return new Proxy({} as SoundManager, {
36
- get: () => () => {}
37
- })
38
- }
39
- if (!SoundManager._instance) {
40
- SoundManager._instance = new SoundManager()
41
- }
42
- return SoundManager._instance
43
- }
44
-
45
- /** Required once after user gesture (browser) */
46
- resume() {
47
- if (this.context.state !== "running") {
48
- this.context.resume()
49
- }
50
- }
51
-
52
- /** Preload a sound from URL */
53
- async load(path: string, url: string) {
54
- if (this.buffers.has(path)) return
55
-
56
- const res = await fetch(url)
57
- const arrayBuffer = await res.arrayBuffer()
58
- const buffer = await this.context.decodeAudioData(arrayBuffer)
59
-
60
- this.buffers.set(path, buffer)
61
- }
62
-
63
- /** Play from already-loaded buffer (fails silently if not loaded) */
64
- playSync(
65
- path: string,
66
- {
67
- volume = 1,
68
- playbackRate = 1,
69
- detune = 0,
70
- pitch = 1,
71
- }: {
72
- volume?: number
73
- playbackRate?: number
74
- detune?: number
75
- pitch?: number
76
- } = {}
77
- ) {
78
- this.resume()
79
-
80
- const buffer = this.buffers.get(path)
81
- if (!buffer) return
82
-
83
- const src = this.context.createBufferSource()
84
- const gain = this.context.createGain()
85
-
86
- src.buffer = buffer
87
- src.playbackRate.value = playbackRate * pitch
88
- src.detune.value = detune
89
-
90
- gain.gain.value = volume
91
-
92
- src.connect(gain)
93
- gain.connect(this.sfxGain)
94
-
95
- src.start()
96
- }
97
-
98
- /** Load and play SFX - accepts file path directly */
99
- async play(
100
- path: string,
101
- options?: {
102
- volume?: number
103
- playbackRate?: number
104
- detune?: number
105
- pitch?: number
106
- }
107
- ) {
108
- // Auto-load from path if not already loaded
109
- if (!this.buffers.has(path)) {
110
- await this.load(path, path)
111
- }
112
-
113
- this.playSync(path, options)
114
- }
115
-
116
- /** Volume controls */
117
- setMasterVolume(v: number) {
118
- this.masterGain.gain.value = v
119
- }
120
-
121
- setSfxVolume(v: number) {
122
- this.sfxGain.gain.value = v
123
- }
124
-
125
- setMusicVolume(v: number) {
126
- this.musicGain.gain.value = v
127
- }
128
- }
129
-
130
- export const sound = SoundManager.instance
@@ -1,91 +0,0 @@
1
- import type { GameObject } from "../tools/prefabeditor/types";
2
-
3
- export type Vec3 = [number, number, number];
4
-
5
- export interface GroundOptions {
6
- /** GameObject id. Defaults to "ground". */
7
- id?: string;
8
-
9
- /** Plane size. Defaults to 50. */
10
- size?: number;
11
-
12
- /** Transform overrides. */
13
- position?: Vec3;
14
- rotation?: Vec3;
15
- scale?: Vec3;
16
-
17
- /** Material overrides. */
18
- color?: string;
19
- texture?: string;
20
- /** When true, set repeat wrapping. Defaults to true if texture is provided. */
21
- repeat?: boolean;
22
- /** Texture repeat counts when repeat=true. Defaults to [25,25]. */
23
- repeatCount?: [number, number];
24
-
25
- /** Physics body type. Defaults to "fixed". */
26
- physicsType?: "fixed" | "dynamic" | "kinematic";
27
-
28
- /** Set true to disable the node. */
29
- disabled?: boolean;
30
- }
31
-
32
- /**
33
- * Create a ready-to-use plane ground GameObject.
34
- *
35
- * Designed to reduce prefab boilerplate:
36
- * - Transform (rotated to lie flat)
37
- * - Geometry (plane)
38
- * - Material (optional texture + repeat)
39
- * - Physics (fixed by default)
40
- */
41
- export function ground(options: GroundOptions = {}): GameObject {
42
- const {
43
- id = "ground",
44
- size = 50,
45
- position = [0, 0, 0],
46
- rotation = [-Math.PI / 2, 0, 0],
47
- scale = [1, 1, 1],
48
- color = "#eeeeee",
49
- texture,
50
- repeat = texture ? true : false,
51
- repeatCount = [25, 25],
52
- physicsType = "fixed",
53
- disabled = false,
54
- } = options;
55
-
56
- return {
57
- id,
58
- disabled,
59
- components: {
60
- transform: {
61
- type: "Transform",
62
- properties: {
63
- position,
64
- rotation,
65
- scale,
66
- },
67
- },
68
- geometry: {
69
- type: "Geometry",
70
- properties: {
71
- geometryType: "plane",
72
- args: [size, size],
73
- },
74
- },
75
- material: {
76
- type: "Material",
77
- properties: {
78
- color,
79
- ...(texture ? { texture } : {}),
80
- ...(repeat ? { repeat: true, repeatCount } : {}),
81
- },
82
- },
83
- physics: {
84
- type: "Physics",
85
- properties: {
86
- type: physicsType,
87
- },
88
- },
89
- },
90
- };
91
- }