@vgai/editor 0.4.0 → 0.4.1

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 (137) hide show
  1. package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
  2. package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
  3. package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
  4. package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
  5. package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
  6. package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
  7. package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
  8. package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
  9. package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
  10. package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
  11. package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
  12. package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
  13. package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
  14. package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
  15. package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
  16. package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
  17. package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
  18. package/dist/assets/index-DiUacMXR.css +1 -0
  19. package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
  20. package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
  21. package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
  22. package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
  23. package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
  24. package/dist/index.html +2 -2
  25. package/dist-server/packaged.mjs +27125 -1075
  26. package/package.json +2 -2
  27. package/server/asset-history-snapshots.ts +189 -0
  28. package/server/asset-library-routes.ts +404 -53
  29. package/server/catalog-import-recipe.ts +97 -0
  30. package/server/catalog-reimport.ts +141 -0
  31. package/server/cloud-asset-catalog.ts +71 -5
  32. package/server/dev.ts +1 -1
  33. package/server/editor-server.ts +106 -51
  34. package/server/editor-sse.ts +5 -0
  35. package/server/generative-execution-context.ts +45 -0
  36. package/server/model-import-conversion.ts +345 -0
  37. package/server/packaged.ts +1 -1
  38. package/server/project-hmr-files.ts +3 -5
  39. package/server/project-output-writer.ts +440 -36
  40. package/server/project-tools.ts +529 -0
  41. package/server/server-utils.ts +14 -11
  42. package/src/EditorContext.tsx +4 -4
  43. package/src/action-registry.ts +12 -0
  44. package/src/asset-editor-context.tsx +9 -0
  45. package/src/asset-events.ts +3 -15
  46. package/src/asset-selection.ts +3 -0
  47. package/src/asset-workflow/asset-capabilities.ts +40 -0
  48. package/src/asset-workflow/asset-import-jobs.ts +108 -0
  49. package/src/asset-workflow/asset-library-query.ts +8 -1
  50. package/src/asset-workflow/asset-workflow-quality.ts +2 -0
  51. package/src/asset-workflow/import-contract.ts +2 -0
  52. package/src/asset-workflow/model-inspection.ts +92 -1
  53. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  54. package/src/asset-workflow/project-asset-commands.ts +1 -0
  55. package/src/asset-workflow/project-asset-health.ts +3 -6
  56. package/src/asset-workflow/project-asset-operations.ts +119 -4
  57. package/src/command-listener.ts +37 -9
  58. package/src/components/ApplicationMenus.tsx +13 -15
  59. package/src/components/AssetBrowser.tsx +301 -26
  60. package/src/components/AssetImportDetails.tsx +207 -92
  61. package/src/components/AssetImportJobStatus.tsx +55 -0
  62. package/src/components/CenterDocuments.tsx +28 -1
  63. package/src/components/CommandPalette.tsx +2 -2
  64. package/src/components/DefaultEditorLayout.tsx +4 -0
  65. package/src/components/InspectorToolSection.tsx +12 -10
  66. package/src/components/OnlineAssetBrowser.tsx +197 -11
  67. package/src/components/ToolHost.tsx +8 -8
  68. package/src/components/ViewportOverlay.tsx +13 -26
  69. package/src/components/ViewportPanel.tsx +19 -14
  70. package/src/components/ViewportShadingMenu.tsx +103 -0
  71. package/src/components/WorkspaceDock.tsx +103 -17
  72. package/src/components/asset-documents.tsx +38 -11
  73. package/src/components/asset-editor-persistence.ts +300 -5
  74. package/src/components/asset-selection-section.tsx +83 -9
  75. package/src/components/asset-viewers/AudioViewer.tsx +4 -7
  76. package/src/components/asset-viewers/ModelViewer.tsx +168 -5
  77. package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
  78. package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
  79. package/src/components/asset-workflow.css +30 -3
  80. package/src/components/primitives/editor-icons.ts +1 -0
  81. package/src/components/project-tool-documents.tsx +298 -0
  82. package/src/components/status-contributions.tsx +38 -0
  83. package/src/components/tool-documents.tsx +20 -17
  84. package/src/components/tool-schema-form.tsx +258 -0
  85. package/src/components/workspace-dock.css +16 -0
  86. package/src/editor-api.ts +159 -22
  87. package/src/editor-store.ts +6 -5
  88. package/src/editor-viewport.ts +12 -7
  89. package/src/game-viewport-state.ts +28 -0
  90. package/src/model-thumbnail.ts +97 -9
  91. package/src/play-mode.ts +30 -3
  92. package/src/project-tool-discovery.ts +5 -5
  93. package/src/{project-operations.ts → project-tools.ts} +39 -26
  94. package/src/scene-sync.ts +0 -67
  95. package/src/storage/handle-storage.ts +5 -1
  96. package/src/storage/http-storage.ts +7 -2
  97. package/src/storage/mem-storage.ts +7 -1
  98. package/src/storage/types.ts +2 -0
  99. package/src/theme-css.ts +17 -7
  100. package/src/tool-loader.ts +152 -241
  101. package/src/viewport-shading-boundary.ts +15 -0
  102. package/src/widgets/index.ts +2 -1
  103. package/src/workspace-dock-controller.ts +8 -0
  104. package/src/workspace-document-registry.ts +4 -4
  105. package/src/workspace-state-persistence.ts +36 -10
  106. package/template/.claude/skills/editor/SKILL.md +13 -3
  107. package/template/AGENTS.md +100 -83
  108. package/template/package.json +16 -0
  109. package/template/public/scenes/default.vscn.json +1 -2
  110. package/template/scripts/playtest.ts +7 -37
  111. package/template/server/rooms/game-room.ts +2 -2
  112. package/template/src/data/README.md +3 -2
  113. package/template/src/data/tuning.data.json +1 -3
  114. package/template/src/data/tuning.schema.json +0 -12
  115. package/template/src/data/tuning.schema.ts +2 -9
  116. package/template/src/data/tuning.ts +1 -1
  117. package/template/src/scripts/registry.ts +0 -3
  118. package/template/src/tools/README.md +151 -16
  119. package/template/src/tools/autoplay.tool.ts +31 -0
  120. package/template/src/tools/autoplay.ts +15 -0
  121. package/template/src/tools/tuning.document.tsx +107 -0
  122. package/template/src/tools/tuning.tool.ts +24 -0
  123. package/template/src/tools/tuning.ts +9 -0
  124. package/template/src/ui/Button.stories.tsx +1 -1
  125. package/template/tests/acceptance/example.spec.ts +4 -59
  126. package/template/vgai.game.json +1 -1
  127. package/dist/assets/index-bAGhEnnO.css +0 -1
  128. package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
  129. package/dist/project-scripts.bundle.js +0 -3917
  130. package/server/project-operations.ts +0 -343
  131. package/src/components/project-operation-documents.tsx +0 -226
  132. package/template/src/operations/README.md +0 -88
  133. package/template/src/scripts/components/data-spinner.ts +0 -86
  134. package/template/src/scripts/components/spin-lap-math.ts +0 -27
  135. package/template/src/tools/example.tool.tsx +0 -97
  136. package/template/src/tools/spin.tool.tsx +0 -101
  137. package/template/tests/logic/example.test.ts +0 -31
@@ -1,3917 +0,0 @@
1
- // packages/editor/src/browser-play-entry.ts
2
- import { setAssetPrefix } from "@engine/loader";
3
- import { loadScene as loadScene13, loadSceneFromData as loadSceneFromData2 } from "@engine/scene/scene-loader";
4
- import { applyScenePostProcessing as applyScenePostProcessing3 } from "@engine/setup/setup-renderer";
5
-
6
- // packages/editor/template/src/scripts/components/data-spinner.ts
7
- import { GameComponent } from "@engine/ecs/game-component";
8
- import { z as z2 } from "zod";
9
-
10
- // packages/editor/template/src/data/tuning.ts
11
- import { defineData } from "@engine/data/data-asset";
12
-
13
- // packages/editor/template/src/data/tuning.data.json
14
- var tuning_data_default = {
15
- $schema: "./tuning.schema.json",
16
- boxSpinSpeed: 0.8,
17
- boxColor: "#4488ff"
18
- };
19
-
20
- // packages/editor/template/src/data/tuning.schema.ts
21
- import { z } from "zod";
22
- var TuningSchema = z.object({
23
- boxSpinSpeed: z.number().min(-5).max(5).default(0.8).describe("Demo box spin speed, radians/sec \u2014 edit tuning.data.json while playing to see live tuning"),
24
- boxColor: z.string().default("#4488ff").describe("Demo box color (hex)")
25
- });
26
-
27
- // packages/editor/template/src/data/tuning.ts
28
- var tuning = defineData(TuningSchema, tuning_data_default, "src/data/tuning.data.json");
29
- import.meta.hot?.accept("./tuning.data.json", (m) => tuning.hotSwap(m?.["default"]));
30
-
31
- // packages/editor/template/src/scripts/components/spin-lap-math.ts
32
- var FULL_ROTATION = Math.PI * 2;
33
- function advanceLap(rotationSinceLap, delta) {
34
- let next = rotationSinceLap + delta;
35
- let lapsCompleted = 0;
36
- while (next >= FULL_ROTATION) {
37
- next -= FULL_ROTATION;
38
- lapsCompleted++;
39
- }
40
- return { rotationSinceLap: next, lapsCompleted };
41
- }
42
-
43
- // packages/editor/template/src/scripts/components/data-spinner.ts
44
- var DataSpinner = class extends GameComponent {
45
- constructor() {
46
- super(...arguments);
47
- this.spinScale = 1;
48
- this.rotationSinceLap = 0;
49
- this.lapsCompleted = 0;
50
- }
51
- static {
52
- this.schema = z2.object({
53
- spinScale: z2.number().default(1).describe(
54
- "Per-entity multiplier on the shared boxSpinSpeed \u2014 the project-wide feel stays in src/data/tuning.data.json; this is the one knob that is THIS entity's own."
55
- )
56
- });
57
- }
58
- init(ctx) {
59
- ctx.debug?.registerStateProvider(
60
- "spinner",
61
- () => ({ lapsCompleted: this.lapsCompleted, spinScale: this.spinScale }),
62
- { tier: "observable" }
63
- );
64
- ctx.debug?.registerCommand(
65
- "spinner.reset",
66
- { description: "Reset lapsCompleted to 0.", locus: "client" },
67
- () => {
68
- this.lapsCompleted = 0;
69
- this.rotationSinceLap = 0;
70
- }
71
- );
72
- }
73
- update(dt, ctx) {
74
- const t = tuning.get();
75
- const boost = ctx.input.isPressed("attack") ? 4 : 1;
76
- const delta = t.boxSpinSpeed * this.spinScale * boost * dt;
77
- this.object3D.rotation.y += delta;
78
- const progress = advanceLap(this.rotationSinceLap, delta);
79
- this.rotationSinceLap = progress.rotationSinceLap;
80
- if (progress.lapsCompleted > 0) {
81
- this.lapsCompleted += progress.lapsCompleted;
82
- ctx.debug?.emit("spin-lap", { lapsCompleted: this.lapsCompleted });
83
- }
84
- const material = this.object3D.material;
85
- if (material?.color) material.color.set(t.boxColor);
86
- }
87
- };
88
-
89
- // packages/editor/template/src/scripts/components/scene-camera.ts
90
- import { z as z3 } from "zod";
91
- import { GameComponent as GameComponent2 } from "@engine/ecs/game-component";
92
- var SceneCamera = class extends GameComponent2 {
93
- constructor() {
94
- super(...arguments);
95
- this.x = 3;
96
- this.y = 3;
97
- this.z = 3;
98
- this.lookAtX = 0;
99
- this.lookAtY = 0.5;
100
- this.lookAtZ = 0;
101
- }
102
- static {
103
- this.phase = "preRender";
104
- }
105
- static {
106
- this.schema = z3.object({
107
- x: z3.number().default(3).describe("Camera position X"),
108
- y: z3.number().default(3).describe("Camera position Y"),
109
- z: z3.number().default(3).describe("Camera position Z"),
110
- lookAtX: z3.number().default(0).describe("Look-at target X"),
111
- lookAtY: z3.number().default(0.5).describe("Look-at target Y"),
112
- lookAtZ: z3.number().default(0).describe("Look-at target Z")
113
- });
114
- }
115
- init(ctx) {
116
- ctx.camera.position.set(this.x, this.y, this.z);
117
- ctx.camera.lookAt(this.lookAtX, this.lookAtY, this.lookAtZ);
118
- }
119
- update() {
120
- }
121
- };
122
-
123
- // packages/editor/template/src/scripts/registry.ts
124
- var componentRegistry = {
125
- // GameComponent classes (OOP components with true HMR)
126
- DataSpinner,
127
- SceneCamera
128
- };
129
- var gameComponents = {
130
- DataSpinner,
131
- SceneCamera
132
- };
133
-
134
- // examples/editor-tutorial/src/index.tsx
135
- import gsap from "gsap";
136
- import { BloomEffect, EffectPass, RenderPass } from "postprocessing";
137
- import {
138
- ApplyForce,
139
- Bezier,
140
- ColorOverLife,
141
- ConeEmitter,
142
- ConstantValue,
143
- Gradient,
144
- IntervalValue,
145
- PiecewiseBezier,
146
- Vector3 as QVector3,
147
- SizeOverLife
148
- } from "quarks.core";
149
- import * as THREE from "three";
150
- import { ParticleSystem } from "three.quarks";
151
- import { GameComponent as GameComponent3 } from "@engine/ecs/game-component";
152
- import { shallow, useGame, useGameState } from "@engine/react/game-state";
153
- import { loadScene } from "@engine/scene/scene-loader";
154
-
155
- // examples/editor-tutorial/src/scripts/registry.ts
156
- var gameComponents2 = {};
157
-
158
- // examples/editor-tutorial/src/ui/Button.tsx
159
- import { jsx } from "react/jsx-runtime";
160
-
161
- // examples/editor-tutorial/src/ui/KeyHint.tsx
162
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
163
-
164
- // examples/editor-tutorial/src/ui/Panel.tsx
165
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
166
-
167
- // examples/editor-tutorial/src/ui/Slider.tsx
168
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
169
-
170
- // examples/editor-tutorial/src/ui/StatusDot.tsx
171
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
172
-
173
- // examples/editor-tutorial/src/ui/Toggle.tsx
174
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
175
-
176
- // examples/editor-tutorial/src/index.tsx
177
- import { fromSetup } from "@engine/adapter";
178
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
179
- var ShowcaseControls = class _ShowcaseControls extends GameComponent3 {
180
- constructor() {
181
- super(...arguments);
182
- this.bloomOn = true;
183
- this.muted = false;
184
- this.debugOn = false;
185
- this.collisionCount = 0;
186
- this.camAngle = 0;
187
- this.camHeight = 6;
188
- this.camRadius = 14;
189
- }
190
- static {
191
- this.phase = "gameLogic";
192
- }
193
- static {
194
- this.ORBIT_ANGULAR_SPEED = 1.5;
195
- }
196
- static {
197
- // rad/s
198
- this.ORBIT_HEIGHT_SPEED = 9;
199
- }
200
- static {
201
- // m/s
202
- this.MIN_HEIGHT = 2;
203
- }
204
- static {
205
- this.MAX_HEIGHT = 15;
206
- }
207
- update(dt, ctx) {
208
- this.sceneInstance.update(dt);
209
- if (ctx.input.isJustPressed("showcase_launch")) this.onLaunch();
210
- if (ctx.input.isJustPressed("showcase_debug")) this.onToggleDebug();
211
- if (ctx.input.isJustPressed("showcase_bloom")) this.onToggleBloom();
212
- if (ctx.input.isJustPressed("showcase_mute")) this.onToggleMute();
213
- let orbited = false;
214
- if (ctx.input.isPressed("showcase_orbit_left")) {
215
- this.camAngle -= _ShowcaseControls.ORBIT_ANGULAR_SPEED * dt;
216
- orbited = true;
217
- }
218
- if (ctx.input.isPressed("showcase_orbit_right")) {
219
- this.camAngle += _ShowcaseControls.ORBIT_ANGULAR_SPEED * dt;
220
- orbited = true;
221
- }
222
- if (ctx.input.isPressed("showcase_orbit_raise")) {
223
- this.camHeight = Math.min(
224
- _ShowcaseControls.MAX_HEIGHT,
225
- this.camHeight + _ShowcaseControls.ORBIT_HEIGHT_SPEED * dt
226
- );
227
- orbited = true;
228
- }
229
- if (ctx.input.isPressed("showcase_orbit_lower")) {
230
- this.camHeight = Math.max(
231
- _ShowcaseControls.MIN_HEIGHT,
232
- this.camHeight - _ShowcaseControls.ORBIT_HEIGHT_SPEED * dt
233
- );
234
- orbited = true;
235
- }
236
- if (orbited) {
237
- ctx.camera.position.set(
238
- Math.sin(this.camAngle) * this.camRadius,
239
- this.camHeight,
240
- Math.cos(this.camAngle) * this.camRadius
241
- );
242
- ctx.camera.lookAt(0, 1, 0);
243
- }
244
- }
245
- };
246
- async function setup(ctx) {
247
- const {
248
- scene,
249
- camera,
250
- rapierWorld,
251
- rapier,
252
- collisions,
253
- composer,
254
- audio,
255
- particles,
256
- debugDraw,
257
- input
258
- } = ctx;
259
- const sceneInstance = await loadScene("scenes/editor-tutorial.vscn.json", {
260
- scene: ctx.scene,
261
- rapierWorld: ctx.rapierWorld,
262
- rapier: ctx.rapier,
263
- physics: ctx.physics,
264
- componentRegistry: gameComponents2,
265
- componentManager: ctx.components,
266
- audioListener: ctx.audio.listener
267
- });
268
- camera.position.set(0, 6, 14);
269
- camera.lookAt(0, 1, 0);
270
- let collisionCount = 0;
271
- const collisionHandler = (_hA, _hB, started) => {
272
- if (started) {
273
- collisionCount++;
274
- showcaseControls.collisionCount = collisionCount;
275
- }
276
- };
277
- collisions.onCollision(collisionHandler);
278
- const orbGeo = new THREE.SphereGeometry(0.35, 24, 12);
279
- const orbMat = new THREE.MeshStandardMaterial({
280
- color: 16755234,
281
- emissive: 16737792,
282
- emissiveIntensity: 1.5
283
- });
284
- const orb = new THREE.Mesh(orbGeo, orbMat);
285
- orb.position.set(0, 2, -2);
286
- orb.castShadow = true;
287
- scene.add(orb);
288
- const orbTimeline = gsap.timeline({ repeat: -1, yoyo: true });
289
- orbTimeline.to(orb.position, { y: 4, duration: 2, ease: "power2.inOut" }, 0);
290
- orbTimeline.to(orb.position, { x: 3, duration: 2, ease: "sine.inOut" }, 0);
291
- orbTimeline.to(orb.rotation, { y: Math.PI * 2, duration: 4, ease: "none" }, 0);
292
- orbTimeline.to(orb.scale, { x: 1.3, y: 0.7, z: 1.3, duration: 1, ease: "elastic.out(1,0.3)" }, 0);
293
- const spriteTex = (() => {
294
- const canvas = document.createElement("canvas");
295
- canvas.width = 16;
296
- canvas.height = 16;
297
- const c2d = canvas.getContext("2d");
298
- const grad = c2d.createRadialGradient(8, 8, 0, 8, 8, 8);
299
- grad.addColorStop(0, "rgba(255,255,255,1)");
300
- grad.addColorStop(1, "rgba(255,255,255,0)");
301
- c2d.fillStyle = grad;
302
- c2d.fillRect(0, 0, 16, 16);
303
- return new THREE.CanvasTexture(canvas);
304
- })();
305
- const particleMat = new THREE.MeshBasicMaterial({
306
- map: spriteTex,
307
- transparent: true,
308
- blending: THREE.AdditiveBlending,
309
- depthWrite: false
310
- });
311
- const fireSys = new ParticleSystem({
312
- looping: true,
313
- duration: 2,
314
- shape: new ConeEmitter({ radius: 0.15, angle: 0.3 }),
315
- startLife: new IntervalValue(0.5, 1),
316
- startSpeed: new IntervalValue(1.5, 3),
317
- startSize: new IntervalValue(0.08, 0.18),
318
- startColor: new Gradient(
319
- [
320
- [new QVector3(1, 0.8, 0.1), 0],
321
- [new QVector3(1, 0.2, 0), 0.6],
322
- [new QVector3(0.4, 0.1, 0), 1]
323
- ],
324
- [
325
- [1, 0],
326
- [0.6, 0.5],
327
- [0, 1]
328
- ]
329
- ),
330
- emissionOverTime: new ConstantValue(35),
331
- behaviors: [
332
- new ApplyForce(new QVector3(0, 1, 0), new ConstantValue(2)),
333
- new ColorOverLife(
334
- new Gradient(
335
- [
336
- [new QVector3(1, 0.8, 0.1), 0],
337
- [new QVector3(1, 0.2, 0), 0.5],
338
- [new QVector3(0, 0, 0), 1]
339
- ],
340
- [
341
- [1, 0],
342
- [0.4, 0.7],
343
- [0, 1]
344
- ]
345
- )
346
- ),
347
- new SizeOverLife(new PiecewiseBezier([[new Bezier(1, 0.67, 0.33, 0), 0]]))
348
- ],
349
- material: particleMat,
350
- worldSpace: true
351
- });
352
- fireSys.emitter.position.set(-6, 0, 0);
353
- scene.add(fireSys.emitter);
354
- particles.batchedRenderer.addSystem(fireSys);
355
- const audioCtx = audio.listener.context;
356
- let muted = false;
357
- const positionalAudio = new THREE.PositionalAudio(audio.listener);
358
- positionalAudio.setRefDistance(3);
359
- positionalAudio.setRolloffFactor(1.5);
360
- positionalAudio.setMaxDistance(20);
361
- const oscillator = audioCtx.createOscillator();
362
- oscillator.type = "sine";
363
- oscillator.frequency.value = 220;
364
- const gainNode = audioCtx.createGain();
365
- gainNode.gain.value = 0.08;
366
- oscillator.connect(gainNode);
367
- gainNode.connect(positionalAudio.getOutput());
368
- oscillator.start();
369
- orb.add(positionalAudio);
370
- function resumeAudio() {
371
- audio.resume();
372
- }
373
- window.addEventListener("click", resumeAudio, { once: true });
374
- window.addEventListener("keydown", resumeAudio, { once: true });
375
- const originalPasses = [...composer.passes];
376
- let bloomEnabled = true;
377
- const bloomEffect = new BloomEffect({ luminanceThreshold: 0.5, intensity: 1.2 });
378
- const bloomPass = new EffectPass(camera, bloomEffect);
379
- function rebuildComposer() {
380
- while (composer.passes.length > 0) {
381
- composer.removePass(composer.passes[0]);
382
- }
383
- composer.addPass(new RenderPass(scene, camera));
384
- if (bloomEnabled) composer.addPass(bloomPass);
385
- }
386
- rebuildComposer();
387
- debugDraw.line(new THREE.Vector3(-8, 0.1, -7), new THREE.Vector3(8, 0.1, -7), 65416);
388
- debugDraw.sphere(new THREE.Vector3(0, 1, -7), 0.2, 16729156);
389
- debugDraw.arrow(new THREE.Vector3(0, 1, -7), new THREE.Vector3(0, 1, 0), 1.5, 16776960);
390
- debugDraw.box(new THREE.Vector3(3, 0.5, -7), new THREE.Vector3(1, 1, 1), 4500223);
391
- debugDraw.visible = false;
392
- input.registerAction("showcase_launch", [{ type: "key", code: "KeyF" }]);
393
- input.registerAction("showcase_debug", [{ type: "key", code: "F3" }]);
394
- input.registerAction("showcase_bloom", [{ type: "key", code: "KeyB" }]);
395
- input.registerAction("showcase_mute", [{ type: "key", code: "KeyM" }]);
396
- input.registerAction("showcase_orbit_left", [{ type: "key", code: "KeyA" }]);
397
- input.registerAction("showcase_orbit_right", [{ type: "key", code: "KeyD" }]);
398
- input.registerAction("showcase_orbit_raise", [{ type: "key", code: "KeyW" }]);
399
- input.registerAction("showcase_orbit_lower", [{ type: "key", code: "KeyS" }]);
400
- const controlsGroup = new THREE.Group();
401
- controlsGroup.name = "ShowcaseControls";
402
- scene.add(controlsGroup);
403
- const showcaseControls = new ShowcaseControls();
404
- showcaseControls.sceneInstance = sceneInstance;
405
- showcaseControls.onLaunch = () => launchBall();
406
- showcaseControls.onToggleDebug = () => {
407
- debugDraw.visible = !debugDraw.visible;
408
- showcaseControls.debugOn = debugDraw.visible;
409
- };
410
- showcaseControls.onToggleBloom = () => {
411
- bloomEnabled = !bloomEnabled;
412
- showcaseControls.bloomOn = bloomEnabled;
413
- rebuildComposer();
414
- };
415
- showcaseControls.onToggleMute = () => {
416
- muted = !muted;
417
- showcaseControls.muted = muted;
418
- audio.masterGain.gain.value = muted ? 0 : 1;
419
- };
420
- ctx.components.attach(controlsGroup, showcaseControls);
421
- const launchedBalls = [];
422
- function launchBall() {
423
- const geo = new THREE.SphereGeometry(0.25, 12, 8);
424
- const mat = new THREE.MeshStandardMaterial({ color: 16720418, emissive: 4456448 });
425
- const mesh = new THREE.Mesh(geo, mat);
426
- mesh.castShadow = true;
427
- const startPos = camera.position.clone();
428
- const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion).normalize();
429
- startPos.add(dir.clone().multiplyScalar(1.5));
430
- mesh.position.copy(startPos);
431
- scene.add(mesh);
432
- const bodyDesc = rapier.RigidBodyDesc.dynamic().setTranslation(
433
- startPos.x,
434
- startPos.y,
435
- startPos.z
436
- );
437
- const body = rapierWorld.createRigidBody(bodyDesc);
438
- body.applyImpulse({ x: dir.x * 15, y: dir.y * 15, z: dir.z * 15 }, true);
439
- const collider = rapierWorld.createCollider(
440
- rapier.ColliderDesc.ball(0.25).setRestitution(0.5),
441
- body
442
- );
443
- ctx.physics.add(mesh, body, collider);
444
- launchedBalls.push({ mesh, body, collider });
445
- }
446
- return {
447
- dispose() {
448
- sceneInstance.dispose();
449
- ctx.components.detach(controlsGroup);
450
- scene.remove(controlsGroup);
451
- collisions.offCollision(collisionHandler);
452
- oscillator.stop();
453
- oscillator.disconnect();
454
- gainNode.disconnect();
455
- audio.masterGain.gain.value = 1;
456
- particles.batchedRenderer.deleteSystem(fireSys);
457
- scene.remove(fireSys.emitter);
458
- spriteTex.dispose();
459
- particleMat.dispose();
460
- orbTimeline.kill();
461
- scene.remove(orb);
462
- orbGeo.dispose();
463
- orbMat.dispose();
464
- while (composer.passes.length > 0) composer.removePass(composer.passes[0]);
465
- for (const pass of originalPasses) composer.addPass(pass);
466
- bloomEffect.dispose();
467
- debugDraw.clear();
468
- debugDraw.visible = false;
469
- for (const ball of launchedBalls) {
470
- ctx.physics.remove(ball.mesh);
471
- rapierWorld.removeRigidBody(ball.body);
472
- scene.remove(ball.mesh);
473
- ball.mesh.geometry.dispose();
474
- ball.mesh.material.dispose();
475
- }
476
- scene.fog = null;
477
- }
478
- };
479
- }
480
- var adapter = fromSetup("editor-tutorial", setup);
481
-
482
- // examples/feature-scenes/src/index.ts
483
- import { fromSetup as fromSetup2 } from "@engine/adapter";
484
- import { loadScene as loadScene2 } from "@engine/scene/scene-loader";
485
- var SCENE_PATH = "scenes/instanced-grid.vscn.json";
486
- async function setup2(ctx) {
487
- const sceneInstance = await loadScene2(SCENE_PATH, {
488
- scene: ctx.scene,
489
- rapierWorld: ctx.rapierWorld,
490
- rapier: ctx.rapier,
491
- physics: ctx.physics
492
- });
493
- return {
494
- dispose() {
495
- sceneInstance.dispose();
496
- }
497
- };
498
- }
499
- var adapter2 = fromSetup2("feature-scenes", setup2);
500
-
501
- // examples/fps/src/index.ts
502
- import { fromSetup as fromSetup3 } from "@engine/adapter";
503
- import { loadScene as loadScene3 } from "@engine/scene/scene-loader";
504
- import { queryByComponent } from "@engine/scene/scene-query";
505
- import * as THREE3 from "three";
506
-
507
- // examples/fps/src/components.ts
508
- import * as THREE2 from "three";
509
- import { z as z4 } from "zod";
510
- import { GameComponent as GameComponent4 } from "@engine/ecs/game-component";
511
- var FALL_RESET_Y = -25;
512
- var FPSController = class extends GameComponent4 {
513
- constructor() {
514
- super(...arguments);
515
- // Config (defaulted + validated through the schema)
516
- this.moveSpeed = 8;
517
- this.jumpVelocity = 7;
518
- this.gravity = 15;
519
- this.height = 1.5;
520
- this.radius = 0.3;
521
- // Runtime state (plain instance fields)
522
- this.isGroundedState = true;
523
- this.verticalVelocity = 0;
524
- this.yaw = 0;
525
- this.pitch = 0;
526
- this.pointerLockedState = false;
527
- // Runtime handles
528
- this.controller = null;
529
- this.canvas = null;
530
- this.onPointerLockChange = null;
531
- this.onCanvasClick = null;
532
- this.sensitivity = 2e-3;
533
- // Reusable vectors
534
- this._forward = new THREE2.Vector3();
535
- this._right = new THREE2.Vector3();
536
- this._moveDir = new THREE2.Vector3();
537
- }
538
- static {
539
- this.phase = "prePhysics";
540
- }
541
- static {
542
- this.schema = z4.object({
543
- moveSpeed: z4.number().default(8).describe("Horizontal movement speed (m/s)"),
544
- jumpVelocity: z4.number().default(7).describe("Upward velocity applied on jump (m/s)"),
545
- gravity: z4.number().default(15).describe("Downward gravity magnitude (m/s^2)"),
546
- height: z4.number().default(1.5).describe("Eye height above the body origin (m)"),
547
- radius: z4.number().default(0.3).describe("Capsule radius (m)")
548
- });
549
- }
550
- init(ctx) {
551
- if (!this.rigidBody || !this.collider) return;
552
- const offset = 0.02;
553
- const ctrl = ctx.rapierWorld.createCharacterController(offset);
554
- ctrl.setUp({ x: 0, y: 1, z: 0 });
555
- ctrl.setMaxSlopeClimbAngle(Math.PI / 4);
556
- ctrl.enableAutostep(0.4, 0.2, true);
557
- ctrl.enableSnapToGround(0.3);
558
- ctrl.setSlideEnabled(true);
559
- ctrl.setApplyImpulsesToDynamicBodies(true);
560
- ctrl.setCharacterMass(1);
561
- this.controller = ctrl;
562
- this.isGroundedState = true;
563
- const camComp = ctx.components.getComponents(this.object3D).find((c) => c instanceof FPSCamera);
564
- if (camComp) this.sensitivity = camComp.sensitivity;
565
- const canvas = ctx.composer.getRenderer().domElement;
566
- this.canvas = canvas;
567
- this.onPointerLockChange = () => {
568
- this.pointerLockedState = document.pointerLockElement === canvas;
569
- };
570
- this.onCanvasClick = () => {
571
- if (!this.pointerLockedState) canvas.requestPointerLock();
572
- };
573
- document.addEventListener("pointerlockchange", this.onPointerLockChange);
574
- canvas.addEventListener("click", this.onCanvasClick);
575
- }
576
- update(dt, ctx) {
577
- const body = this.rigidBody;
578
- const collider = this.collider;
579
- const controller = this.controller;
580
- if (!body || !collider || !controller) return;
581
- const input = ctx.input;
582
- if (this.pointerLockedState) {
583
- const md = input.getMouseDelta();
584
- this.yaw -= md.x * this.sensitivity;
585
- this.pitch -= md.y * this.sensitivity;
586
- this.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.pitch));
587
- }
588
- this._forward.set(-Math.sin(this.yaw), 0, -Math.cos(this.yaw)).normalize();
589
- this._right.set(-this._forward.z, 0, this._forward.x);
590
- let inputX = 0;
591
- let inputZ = 0;
592
- if (input.isPressed("move_forward")) inputZ += 1;
593
- if (input.isPressed("move_backward")) inputZ -= 1;
594
- if (input.isPressed("move_left")) inputX -= 1;
595
- if (input.isPressed("move_right")) inputX += 1;
596
- this._moveDir.set(0, 0, 0);
597
- if (inputX !== 0 || inputZ !== 0) {
598
- this._moveDir.addScaledVector(this._forward, inputZ);
599
- this._moveDir.addScaledVector(this._right, inputX);
600
- this._moveDir.normalize();
601
- }
602
- let vVel = this.verticalVelocity;
603
- const grounded = this.isGroundedState;
604
- if (grounded) {
605
- vVel = 0;
606
- if (input.isJustPressed("jump")) {
607
- vVel = this.jumpVelocity;
608
- }
609
- }
610
- vVel += -this.gravity * dt;
611
- if (vVel > 0) {
612
- controller.disableSnapToGround();
613
- } else {
614
- controller.enableSnapToGround(0.3);
615
- }
616
- controller.computeColliderMovement(
617
- collider,
618
- {
619
- x: this._moveDir.x * this.moveSpeed * dt,
620
- y: vVel * dt,
621
- z: this._moveDir.z * this.moveSpeed * dt
622
- },
623
- ctx.rapier.QueryFilterFlags.EXCLUDE_SENSORS
624
- );
625
- const corrected = controller.computedMovement();
626
- const nowGrounded = controller.computedGrounded();
627
- const pos = body.translation();
628
- const newPos = {
629
- x: pos.x + corrected.x,
630
- y: pos.y + corrected.y,
631
- z: pos.z + corrected.z
632
- };
633
- body.setNextKinematicTranslation(newPos);
634
- if (newPos.y < FALL_RESET_Y) {
635
- body.setNextKinematicTranslation({ x: 0, y: 2, z: 0 });
636
- vVel = 0;
637
- }
638
- if (nowGrounded && vVel < 0) vVel = 0;
639
- this.verticalVelocity = vVel;
640
- this.isGroundedState = nowGrounded;
641
- }
642
- dispose(ctx) {
643
- if (this.controller) {
644
- ctx.rapierWorld.removeCharacterController(this.controller);
645
- this.controller = null;
646
- }
647
- if (this.onPointerLockChange) {
648
- document.removeEventListener("pointerlockchange", this.onPointerLockChange);
649
- this.onPointerLockChange = null;
650
- }
651
- if (this.onCanvasClick && this.canvas) {
652
- this.canvas.removeEventListener("click", this.onCanvasClick);
653
- }
654
- this.onCanvasClick = null;
655
- this.canvas = null;
656
- if (document.pointerLockElement) document.exitPointerLock();
657
- }
658
- /** Current body translation — used by ball spawning. */
659
- getPosition() {
660
- return this.rigidBody?.translation() ?? { x: 0, y: 0, z: 0 };
661
- }
662
- /** Eye height above the body origin. */
663
- get playerHeight() {
664
- return this.height;
665
- }
666
- /** Whether the character is currently on the ground. */
667
- isGrounded() {
668
- return this.isGroundedState;
669
- }
670
- /** Whether the pointer is currently locked to the canvas. */
671
- get pointerLocked() {
672
- return this.pointerLockedState;
673
- }
674
- };
675
- var FPSCamera = class extends GameComponent4 {
676
- constructor() {
677
- super(...arguments);
678
- this.sensitivity = 2e-3;
679
- this.controllerRef = null;
680
- }
681
- static {
682
- this.phase = "preRender";
683
- }
684
- static {
685
- this.schema = z4.object({
686
- sensitivity: z4.number().default(2e-3).describe("Mouse look sensitivity (radians per pixel)")
687
- });
688
- }
689
- init(ctx) {
690
- ctx.camera.rotation.order = "YXZ";
691
- this.controllerRef = ctx.components.getComponents(this.object3D).find((c) => c instanceof FPSController) ?? null;
692
- }
693
- update(_dt, ctx) {
694
- const fps = this.controllerRef;
695
- if (!fps) return;
696
- const pos = fps.getPosition();
697
- ctx.camera.position.set(pos.x, pos.y + fps.playerHeight, pos.z);
698
- ctx.camera.rotation.set(fps.pitch, fps.yaw, 0);
699
- }
700
- dispose(ctx) {
701
- ctx.camera.rotation.order = "XYZ";
702
- this.controllerRef = null;
703
- }
704
- };
705
- var THROW_MIN_SPEED = 15;
706
- var THROW_MAX_SPEED = 45;
707
- var THROW_CHARGE_TAU = 1e3;
708
- var FPSBallLauncher = class extends GameComponent4 {
709
- constructor() {
710
- super(...arguments);
711
- this.balls = [];
712
- this.playerRef = null;
713
- this.nextBallIdx = 0;
714
- this.mouseDownTime = 0;
715
- this.charging = false;
716
- this.wasGrounded = true;
717
- this.footstepTimer = 0;
718
- this._throwDir = new THREE2.Vector3();
719
- }
720
- static {
721
- this.phase = "postPhysics";
722
- }
723
- /** Whether the throw is currently charging — read by the HUD's charge bar. */
724
- get isCharging() {
725
- return this.charging;
726
- }
727
- /** ms since the charge began (0 when not charging) — read by the HUD. */
728
- get chargeHoldTime() {
729
- return this.charging ? performance.now() - this.mouseDownTime : 0;
730
- }
731
- /** Charge fraction (0-100) on the same exponential curve as the throw speed
732
- * — read directly by the HUD's charge bar. */
733
- get chargePercent() {
734
- return (1 - Math.exp(-this.chargeHoldTime / THROW_CHARGE_TAU)) * 100;
735
- }
736
- fireBall(speed) {
737
- const fps = this.playerRef;
738
- if (!fps) return;
739
- const ball = this.balls[this.nextBallIdx % this.balls.length];
740
- if (!ball) return;
741
- this.nextBallIdx++;
742
- this.getCamera().getWorldDirection(this._throwDir);
743
- const pos = fps.getPosition();
744
- const spawnX = pos.x + this._throwDir.x * 0.8;
745
- const spawnY = pos.y + fps.playerHeight + this._throwDir.y * 0.8;
746
- const spawnZ = pos.z + this._throwDir.z * 0.8;
747
- ball.body.setTranslation({ x: spawnX, y: spawnY, z: spawnZ }, true);
748
- ball.body.setRotation({ x: 0, y: 0, z: 0, w: 1 }, true);
749
- ball.body.setLinvel(
750
- {
751
- x: this._throwDir.x * speed,
752
- y: this._throwDir.y * speed,
753
- z: this._throwDir.z * speed
754
- },
755
- true
756
- );
757
- ball.body.setAngvel({ x: 0, y: 0, z: 0 }, true);
758
- ball.body.wakeUp();
759
- ball.mesh.visible = true;
760
- }
761
- update(dt, ctx) {
762
- this.sceneInstance.update(dt);
763
- for (const ball of this.balls) {
764
- if (!ball.mesh.visible) continue;
765
- if (ball.mesh.position.y < FALL_RESET_Y) {
766
- ball.body.sleep();
767
- ball.mesh.visible = false;
768
- }
769
- }
770
- if (ctx.input.isJustPressed("fps_throw") && this.playerRef?.pointerLocked) {
771
- this.mouseDownTime = performance.now();
772
- this.charging = true;
773
- this.chargeSound.play();
774
- }
775
- if (ctx.input.isJustReleased("fps_throw") && this.charging) {
776
- this.charging = false;
777
- this.chargeSound.stop?.();
778
- const holdTime = performance.now() - this.mouseDownTime;
779
- const speed = THROW_MIN_SPEED + (THROW_MAX_SPEED - THROW_MIN_SPEED) * (1 - Math.exp(-holdTime / THROW_CHARGE_TAU));
780
- this.fireBall(speed);
781
- this.throwWhoosh.play();
782
- }
783
- const grounded = this.playerRef?.isGrounded() ?? false;
784
- if (!this.wasGrounded && grounded) this.landSound.play();
785
- this.wasGrounded = grounded;
786
- const moving = ctx.input.isPressed("move_forward") || ctx.input.isPressed("move_backward") || ctx.input.isPressed("move_left") || ctx.input.isPressed("move_right");
787
- if (grounded && moving) {
788
- this.footstepTimer -= dt;
789
- if (this.footstepTimer <= 0) {
790
- this.stepSound.play();
791
- this.footstepTimer = 0.3;
792
- }
793
- } else {
794
- this.footstepTimer = 0;
795
- }
796
- if (ctx.input.isJustPressed("jump") && grounded) {
797
- this.jmpSound.play();
798
- }
799
- }
800
- };
801
-
802
- // examples/fps/src/procedural-audio.ts
803
- function createNoiseBuffer(ctx, duration = 1) {
804
- const length = ctx.sampleRate * duration;
805
- const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
806
- const data = buffer.getChannelData(0);
807
- for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
808
- return buffer;
809
- }
810
- function footstep(ctx, dest, options) {
811
- const pitch = options?.pitch ?? 250;
812
- const volume = options?.volume ?? 0.3;
813
- const variation = options?.variation ?? 0.2;
814
- const noiseBuffer = createNoiseBuffer(ctx, 0.2);
815
- const out = ctx.createGain();
816
- out.gain.value = volume;
817
- out.connect(dest);
818
- return {
819
- play() {
820
- const now = ctx.currentTime;
821
- const src = ctx.createBufferSource();
822
- src.buffer = noiseBuffer;
823
- const filter = ctx.createBiquadFilter();
824
- filter.type = "bandpass";
825
- filter.frequency.value = pitch * (1 + (Math.random() - 0.5) * 2 * variation);
826
- filter.Q.value = 2;
827
- const env = ctx.createGain();
828
- env.gain.setValueAtTime(0, now);
829
- env.gain.linearRampToValueAtTime(1, now + 7e-3);
830
- env.gain.exponentialRampToValueAtTime(1e-3, now + 0.05);
831
- src.connect(filter);
832
- filter.connect(env);
833
- env.connect(out);
834
- src.start(now);
835
- src.stop(now + 0.06);
836
- },
837
- dispose() {
838
- out.disconnect();
839
- }
840
- };
841
- }
842
- function whoosh(ctx, dest, options) {
843
- const pitch = options?.pitch ?? 600;
844
- const volume = options?.volume ?? 0.3;
845
- const duration = options?.duration ?? 0.2;
846
- const speed = options?.speed ?? 1;
847
- const noiseBuffer = createNoiseBuffer(ctx, 0.5);
848
- const out = ctx.createGain();
849
- out.gain.value = volume;
850
- out.connect(dest);
851
- return {
852
- play() {
853
- const now = ctx.currentTime;
854
- const src = ctx.createBufferSource();
855
- src.buffer = noiseBuffer;
856
- const filter = ctx.createBiquadFilter();
857
- filter.type = "bandpass";
858
- filter.Q.value = 2;
859
- filter.frequency.setValueAtTime(pitch * speed * 2, now);
860
- filter.frequency.exponentialRampToValueAtTime(Math.max(pitch * 0.5, 20), now + duration);
861
- const peak = duration * 0.3;
862
- const env = ctx.createGain();
863
- env.gain.setValueAtTime(1e-3, now);
864
- env.gain.linearRampToValueAtTime(1, now + peak);
865
- env.gain.exponentialRampToValueAtTime(1e-3, now + duration);
866
- src.connect(filter);
867
- filter.connect(env);
868
- env.connect(out);
869
- src.start(now);
870
- src.stop(now + duration + 0.01);
871
- },
872
- dispose() {
873
- out.disconnect();
874
- }
875
- };
876
- }
877
- function jump(ctx, dest, options) {
878
- const pitch = options?.pitch ?? 150;
879
- const volume = options?.volume ?? 0.3;
880
- const duration = options?.duration ?? 0.15;
881
- const out = ctx.createGain();
882
- out.gain.value = volume;
883
- out.connect(dest);
884
- return {
885
- play() {
886
- const now = ctx.currentTime;
887
- const osc = ctx.createOscillator();
888
- osc.type = "triangle";
889
- osc.frequency.setValueAtTime(pitch, now);
890
- osc.frequency.exponentialRampToValueAtTime(pitch * 2, now + duration);
891
- const env = ctx.createGain();
892
- env.gain.setValueAtTime(0.8, now);
893
- env.gain.exponentialRampToValueAtTime(1e-3, now + duration);
894
- osc.connect(env);
895
- env.connect(out);
896
- osc.start(now);
897
- osc.stop(now + duration + 0.01);
898
- },
899
- dispose() {
900
- out.disconnect();
901
- }
902
- };
903
- }
904
- function hit(ctx, dest, options) {
905
- const pitch = options?.pitch ?? 400;
906
- const volume = options?.volume ?? 0.5;
907
- const duration = options?.duration ?? 0.1;
908
- const brightness = options?.brightness ?? 1;
909
- const noiseBuffer = createNoiseBuffer(ctx, 0.5);
910
- const out = ctx.createGain();
911
- out.gain.value = volume;
912
- out.connect(dest);
913
- return {
914
- play() {
915
- const now = ctx.currentTime;
916
- const src = ctx.createBufferSource();
917
- src.buffer = noiseBuffer;
918
- const filter = ctx.createBiquadFilter();
919
- filter.type = "bandpass";
920
- filter.frequency.value = pitch;
921
- filter.Q.value = brightness;
922
- const env = ctx.createGain();
923
- env.gain.setValueAtTime(1, now);
924
- env.gain.exponentialRampToValueAtTime(1e-3, now + duration);
925
- src.connect(filter);
926
- filter.connect(env);
927
- env.connect(out);
928
- src.start(now);
929
- src.stop(now + duration + 0.01);
930
- },
931
- dispose() {
932
- out.disconnect();
933
- }
934
- };
935
- }
936
- function chargeUp(ctx, dest, options) {
937
- const startFreq = options?.startFreq ?? 100;
938
- const endFreq = options?.endFreq ?? 800;
939
- const volume = options?.volume ?? 0.3;
940
- const duration = options?.duration ?? 2;
941
- const out = ctx.createGain();
942
- out.gain.value = 0;
943
- out.connect(dest);
944
- let osc = null;
945
- return {
946
- play() {
947
- if (osc) return;
948
- const now = ctx.currentTime;
949
- osc = ctx.createOscillator();
950
- osc.type = "sawtooth";
951
- osc.frequency.setValueAtTime(startFreq, now);
952
- osc.frequency.exponentialRampToValueAtTime(endFreq, now + duration);
953
- osc.connect(out);
954
- osc.start(now);
955
- out.gain.setValueAtTime(1e-3, now);
956
- out.gain.exponentialRampToValueAtTime(volume, now + 0.05);
957
- },
958
- stop() {
959
- if (!osc) return;
960
- const now = ctx.currentTime;
961
- out.gain.setTargetAtTime(0, now, 0.02);
962
- const ref = osc;
963
- osc = null;
964
- setTimeout(() => {
965
- try {
966
- ref.stop();
967
- } catch {
968
- }
969
- ref.disconnect();
970
- }, 100);
971
- },
972
- dispose() {
973
- if (osc) {
974
- try {
975
- osc.stop();
976
- } catch {
977
- }
978
- osc.disconnect();
979
- osc = null;
980
- }
981
- out.disconnect();
982
- }
983
- };
984
- }
985
-
986
- // examples/fps/src/components/player.ts
987
- import { GameComponent as GameComponent5 } from "@engine/ecs/game-component";
988
- var Player = class extends GameComponent5 {
989
- update() {
990
- }
991
- };
992
-
993
- // examples/fps/src/scripts/registry.ts
994
- var gameComponents3 = {
995
- Player,
996
- FPSController,
997
- FPSCamera
998
- };
999
-
1000
- // examples/fps/src/index.ts
1001
- var SCENE_PATH2 = "scenes/fps.vscn.json";
1002
- var BALL_POOL_SIZE = 30;
1003
- var BALL_RADIUS = 0.2;
1004
- async function setup3(ctx) {
1005
- const { scene, camera, rapierWorld, rapier } = ctx;
1006
- const localRegistry = {
1007
- ...gameComponents3,
1008
- FPSController,
1009
- FPSCamera
1010
- };
1011
- const sceneInstance = await loadScene3(SCENE_PATH2, {
1012
- scene,
1013
- rapierWorld,
1014
- rapier,
1015
- physics: ctx.physics,
1016
- componentRegistry: localRegistry,
1017
- componentManager: ctx.components,
1018
- audioListener: ctx.audio.listener
1019
- });
1020
- const playerObj = queryByComponent(scene, "FPSController")[0];
1021
- const fps = playerObj ? ctx.components.getComponents(playerObj).find((c) => c instanceof FPSController) : void 0;
1022
- const grid = new THREE3.GridHelper(50, 50, 8947848, 7829367);
1023
- grid.position.y = 0.01;
1024
- scene.add(grid);
1025
- const audioCtx = ctx.audio.listener.context;
1026
- const sfx = ctx.audio.buses.sfx;
1027
- const resumeAudio = () => ctx.audio.resume();
1028
- window.addEventListener("click", resumeAudio, { once: true });
1029
- window.addEventListener("keydown", resumeAudio, { once: true });
1030
- const chargeSound = chargeUp(audioCtx, sfx, { volume: 0.15 });
1031
- const throwWhoosh = whoosh(audioCtx, sfx, { volume: 0.25 });
1032
- const stepSound = footstep(audioCtx, sfx, { volume: 0.12 });
1033
- const jmpSound = jump(audioCtx, sfx, { volume: 0.2 });
1034
- const landSound = hit(audioCtx, sfx, { pitch: 200, volume: 0.15, duration: 0.08 });
1035
- const ballGeo = new THREE3.IcosahedronGeometry(BALL_RADIUS, 3);
1036
- const ballMat = new THREE3.MeshLambertMaterial({ color: 14605965 });
1037
- const balls = [];
1038
- for (let i = 0; i < BALL_POOL_SIZE; i++) {
1039
- const mesh = new THREE3.Mesh(ballGeo, ballMat);
1040
- mesh.castShadow = true;
1041
- mesh.receiveShadow = true;
1042
- mesh.visible = false;
1043
- scene.add(mesh);
1044
- const body = rapierWorld.createRigidBody(
1045
- rapier.RigidBodyDesc.dynamic().setTranslation(0, -100, 0).setCcdEnabled(true)
1046
- );
1047
- const collider = rapierWorld.createCollider(
1048
- rapier.ColliderDesc.ball(BALL_RADIUS).setRestitution(0.6).setDensity(3),
1049
- body
1050
- );
1051
- body.sleep();
1052
- ctx.physics.add(mesh, body, collider);
1053
- balls.push({ mesh, body, collider });
1054
- }
1055
- ctx.input.registerAction("fps_throw", [{ type: "mouse_button", button: 0 }]);
1056
- const launcherGroup = new THREE3.Group();
1057
- launcherGroup.name = "FPSBallLauncher";
1058
- scene.add(launcherGroup);
1059
- const launcher = new FPSBallLauncher();
1060
- launcher.sceneInstance = sceneInstance;
1061
- launcher.balls = balls;
1062
- launcher.chargeSound = chargeSound;
1063
- launcher.throwWhoosh = throwWhoosh;
1064
- launcher.stepSound = stepSound;
1065
- launcher.jmpSound = jmpSound;
1066
- launcher.landSound = landSound;
1067
- launcher.playerRef = fps ?? null;
1068
- launcher.getCamera = () => camera;
1069
- ctx.components.attach(launcherGroup, launcher);
1070
- return {
1071
- dispose() {
1072
- ctx.components.detach(launcherGroup);
1073
- scene.remove(launcherGroup);
1074
- sceneInstance.dispose();
1075
- chargeSound.dispose();
1076
- throwWhoosh.dispose();
1077
- stepSound.dispose();
1078
- jmpSound.dispose();
1079
- landSound.dispose();
1080
- for (const ball of balls) ctx.physics.remove(ball.mesh);
1081
- for (const ball of balls) rapierWorld.removeRigidBody(ball.body);
1082
- for (const ball of balls) scene.remove(ball.mesh);
1083
- ballGeo.dispose();
1084
- ballMat.dispose();
1085
- scene.remove(grid);
1086
- grid.geometry.dispose();
1087
- grid.material.dispose();
1088
- }
1089
- };
1090
- }
1091
- var adapter3 = fromSetup3("fps", setup3);
1092
-
1093
- // examples/ik/src/index.ts
1094
- import { loadScene as loadScene4 } from "@engine/scene/scene-loader";
1095
- import * as THREE5 from "three";
1096
-
1097
- // examples/ik/src/scripts/registry.ts
1098
- var gameComponents4 = {};
1099
-
1100
- // examples/ik/src/ik-chain.ts
1101
- import { GameComponent as GameComponent6 } from "@engine/ecs/game-component";
1102
- import * as THREE4 from "three";
1103
- import { CCDIKSolver } from "three/examples/jsm/animation/CCDIKSolver.js";
1104
- import { z as z5 } from "zod";
1105
- var IKChain = class extends GameComponent6 {
1106
- constructor() {
1107
- super(...arguments);
1108
- this.boneNames = [];
1109
- this.targetBoneName = "IKTarget";
1110
- this.targetOffset = [0, 0, 0];
1111
- this.iterations = 20;
1112
- /** Optional live world-space target (set from code to track a moving object). */
1113
- this.getWorldTarget = null;
1114
- this.solver = null;
1115
- this.mesh = null;
1116
- this.targetBone = null;
1117
- this._wt = new THREE4.Vector3();
1118
- }
1119
- static {
1120
- this.phase = "preRender";
1121
- }
1122
- static {
1123
- this.schema = z5.object({
1124
- boneNames: z5.array(z5.string()).default([]).describe("Bone names from chain root \u2192 effector (the tip that reaches the target)"),
1125
- targetBoneName: z5.string().default("IKTarget").describe("Name of the skeleton bone used as the IK goal (created if absent)"),
1126
- targetOffset: z5.array(z5.number()).default([0, 0, 0]).describe("World-space target position when no live target is provided"),
1127
- iterations: z5.number().default(20).describe("CCDIK iterations solved per frame")
1128
- });
1129
- }
1130
- init() {
1131
- const mesh = this.findSkinnedMesh(this.object3D);
1132
- if (!mesh) return;
1133
- this.mesh = mesh;
1134
- const skel = mesh.skeleton;
1135
- const indexOf = (name) => skel.bones.findIndex((b) => b.name === name);
1136
- const chain = this.boneNames.map(indexOf);
1137
- if (chain.length < 2 || chain.some((i) => i < 0)) return;
1138
- const effector = chain[chain.length - 1];
1139
- const links = chain.slice(0, -1).reverse().map((index) => ({ index }));
1140
- let tIdx = indexOf(this.targetBoneName);
1141
- if (tIdx < 0) tIdx = this.appendTargetBone(mesh);
1142
- this.targetBone = skel.bones[tIdx] ?? null;
1143
- this.solver = new CCDIKSolver(mesh, [
1144
- { target: tIdx, effector, links, iteration: this.iterations }
1145
- ]);
1146
- }
1147
- update() {
1148
- if (!this.solver || !this.targetBone || !this.mesh) return;
1149
- if (this.getWorldTarget) this._wt.copy(this.getWorldTarget());
1150
- else this._wt.set(this.targetOffset[0], this.targetOffset[1], this.targetOffset[2]);
1151
- this.mesh.updateWorldMatrix(true, false);
1152
- this.mesh.worldToLocal(this._wt);
1153
- this.targetBone.position.copy(this._wt);
1154
- this.mesh.updateMatrixWorld(true);
1155
- this.solver.update();
1156
- }
1157
- findSkinnedMesh(root) {
1158
- let found = null;
1159
- root.traverse((o) => {
1160
- if (!found && o.isSkinnedMesh) found = o;
1161
- });
1162
- return found;
1163
- }
1164
- /** Append a target bone to the skeleton + grow the skin's bone buffers. */
1165
- appendTargetBone(mesh) {
1166
- const skel = mesh.skeleton;
1167
- const bone = new THREE4.Bone();
1168
- bone.name = this.targetBoneName;
1169
- mesh.add(bone);
1170
- skel.bones.push(bone);
1171
- skel.boneInverses.push(new THREE4.Matrix4());
1172
- skel.boneMatrices = new Float32Array(skel.bones.length * 16);
1173
- if (skel.boneTexture) {
1174
- skel.boneTexture.dispose();
1175
- skel.boneTexture = null;
1176
- }
1177
- skel.computeBoneTexture();
1178
- return skel.bones.length - 1;
1179
- }
1180
- };
1181
-
1182
- // examples/ik/src/orb-mover.ts
1183
- import { GameComponent as GameComponent7 } from "@engine/ecs/game-component";
1184
- var OrbMover = class extends GameComponent7 {
1185
- constructor() {
1186
- super(...arguments);
1187
- this.t = 0;
1188
- }
1189
- static {
1190
- this.phase = "gameLogic";
1191
- }
1192
- update(dt, _ctx) {
1193
- this.t += dt;
1194
- this.object3D.position.set(
1195
- Math.cos(this.t * 0.9) * 5,
1196
- 4 + Math.sin(this.t * 1.7) * 1.2,
1197
- Math.sin(this.t * 0.9) * 5
1198
- );
1199
- }
1200
- };
1201
-
1202
- // examples/ik/src/index.ts
1203
- import { fromSetup as fromSetup4 } from "@engine/adapter";
1204
- var SCENE_PATH3 = "scenes/ik.vscn.json";
1205
- function buildArm() {
1206
- const base = new THREE5.Bone();
1207
- base.name = "base";
1208
- const seg1 = new THREE5.Bone();
1209
- seg1.name = "seg1";
1210
- seg1.position.y = 3;
1211
- base.add(seg1);
1212
- const seg2 = new THREE5.Bone();
1213
- seg2.name = "seg2";
1214
- seg2.position.y = 3;
1215
- seg1.add(seg2);
1216
- const hand = new THREE5.Bone();
1217
- hand.name = "hand";
1218
- hand.position.y = 2;
1219
- seg2.add(hand);
1220
- const targetBone = new THREE5.Bone();
1221
- targetBone.name = "IKTarget";
1222
- const bones = [base, seg1, seg2, hand, targetBone];
1223
- const skeleton = new THREE5.Skeleton(bones);
1224
- const H = 8;
1225
- const geo = new THREE5.CylinderGeometry(0.55, 0.7, H, 12, 32);
1226
- geo.translate(0, H / 2, 0);
1227
- const pos = geo.attributes["position"];
1228
- const skinIndices = [];
1229
- const skinWeights = [];
1230
- for (let i = 0; i < pos.count; i++) {
1231
- const y = pos.getY(i);
1232
- const b = y < 3 ? 0 : y < 6 ? 1 : 2;
1233
- skinIndices.push(b, 0, 0, 0);
1234
- skinWeights.push(1, 0, 0, 0);
1235
- }
1236
- geo.setAttribute("skinIndex", new THREE5.Uint16BufferAttribute(skinIndices, 4));
1237
- geo.setAttribute("skinWeight", new THREE5.Float32BufferAttribute(skinWeights, 4));
1238
- const skinned = new THREE5.SkinnedMesh(
1239
- geo,
1240
- new THREE5.MeshStandardMaterial({ color: 9085120, metalness: 0.5, roughness: 0.4 })
1241
- );
1242
- skinned.castShadow = true;
1243
- skinned.frustumCulled = false;
1244
- skinned.add(base);
1245
- skinned.add(targetBone);
1246
- skinned.bind(skeleton);
1247
- const jointMat = new THREE5.MeshStandardMaterial({
1248
- color: 3358810,
1249
- metalness: 0.4,
1250
- roughness: 0.5
1251
- });
1252
- for (const j of [seg1, seg2, hand]) {
1253
- const sph = new THREE5.Mesh(new THREE5.SphereGeometry(0.62, 16, 16), jointMat);
1254
- sph.castShadow = true;
1255
- j.add(sph);
1256
- }
1257
- const entity = new THREE5.Group();
1258
- entity.name = "IKArm";
1259
- entity.position.set(0, 0.5, 0);
1260
- entity.add(skinned);
1261
- return { entity, skinned, hand };
1262
- }
1263
- async function setup4(ctx) {
1264
- const { scene, systems } = ctx;
1265
- const sceneInstance = await loadScene4(SCENE_PATH3, {
1266
- scene,
1267
- rapierWorld: ctx.rapierWorld,
1268
- rapier: ctx.rapier,
1269
- physics: ctx.physics,
1270
- componentRegistry: gameComponents4,
1271
- componentManager: ctx.components,
1272
- audioListener: ctx.audio.listener
1273
- });
1274
- const { entity, hand } = buildArm();
1275
- scene.add(entity);
1276
- const orb = new THREE5.Mesh(
1277
- new THREE5.SphereGeometry(0.5, 20, 20),
1278
- new THREE5.MeshStandardMaterial({ color: 16765503, emissive: 16756736, emissiveIntensity: 1.4 })
1279
- );
1280
- orb.castShadow = true;
1281
- scene.add(orb);
1282
- const ik = new IKChain();
1283
- ik.boneNames = ["base", "seg1", "seg2", "hand"];
1284
- ik.targetBoneName = "IKTarget";
1285
- ik.iterations = 16;
1286
- const orbWorld = new THREE5.Vector3();
1287
- ik.getWorldTarget = () => orb.getWorldPosition(orbWorld);
1288
- ctx.components.attach(entity, ik);
1289
- const orbMover = new OrbMover();
1290
- ctx.components.attach(orb, orbMover);
1291
- const sceneTick = (dt) => sceneInstance.update(dt);
1292
- systems.add("postPhysics", sceneTick);
1293
- ctx.camera.position.set(11, 8, 13);
1294
- ctx.camera.lookAt(0, 4, 0);
1295
- const handWorld = new THREE5.Vector3();
1296
- window["__ikState"] = () => {
1297
- hand.getWorldPosition(handWorld);
1298
- orb.getWorldPosition(orbWorld);
1299
- return {
1300
- hand: { x: handWorld.x, y: handWorld.y, z: handWorld.z },
1301
- target: { x: orbWorld.x, y: orbWorld.y, z: orbWorld.z },
1302
- dist: handWorld.distanceTo(orbWorld)
1303
- };
1304
- };
1305
- return {
1306
- dispose() {
1307
- systems.remove("postPhysics", sceneTick);
1308
- ctx.components.detach(entity);
1309
- ctx.components.detach(orb);
1310
- scene.remove(entity);
1311
- entity.traverse((child) => {
1312
- if (!(child instanceof THREE5.Mesh)) return;
1313
- child.geometry.dispose();
1314
- const material = child.material;
1315
- if (Array.isArray(material)) {
1316
- for (const m of material) m.dispose();
1317
- } else {
1318
- material.dispose();
1319
- }
1320
- });
1321
- scene.remove(orb);
1322
- orb.geometry.dispose();
1323
- orb.material.dispose();
1324
- sceneInstance.dispose();
1325
- delete window["__ikState"];
1326
- }
1327
- };
1328
- }
1329
- var adapter4 = fromSetup4("ik", setup4);
1330
-
1331
- // examples/learn-joints/src/index.ts
1332
- import { fromSetup as fromSetup5 } from "@engine/adapter";
1333
- import { loadScene as loadScene5 } from "@engine/scene/scene-loader";
1334
- var SCENE_PATH4 = "scenes/learn-joints.vscn.json";
1335
- async function setup5(ctx) {
1336
- const sceneInstance = await loadScene5(SCENE_PATH4, {
1337
- scene: ctx.scene,
1338
- rapierWorld: ctx.rapierWorld,
1339
- rapier: ctx.rapier,
1340
- physics: ctx.physics
1341
- });
1342
- ctx.camera.position.set(0, 4.5, 12);
1343
- ctx.camera.lookAt(0, 3.5, 0);
1344
- return {
1345
- dispose() {
1346
- sceneInstance.dispose();
1347
- }
1348
- };
1349
- }
1350
- var adapter5 = fromSetup5("learn-joints", setup5);
1351
-
1352
- // examples/learn-matfiles/src/index.ts
1353
- import { fromSetup as fromSetup6 } from "@engine/adapter";
1354
- import { loadScene as loadScene6 } from "@engine/scene/scene-loader";
1355
- var SCENE_PATH5 = "scenes/learn-matfiles.vscn.json";
1356
- async function setup6(ctx) {
1357
- const sceneInstance = await loadScene6(SCENE_PATH5, {
1358
- scene: ctx.scene,
1359
- rapierWorld: ctx.rapierWorld,
1360
- rapier: ctx.rapier,
1361
- physics: ctx.physics
1362
- });
1363
- ctx.camera.position.set(0, 4, 11);
1364
- ctx.camera.lookAt(0, 1.5, 0);
1365
- return {
1366
- dispose() {
1367
- sceneInstance.dispose();
1368
- }
1369
- };
1370
- }
1371
- var adapter6 = fromSetup6("learn-matfiles", setup6);
1372
-
1373
- // examples/learn-splines/src/index.ts
1374
- import { fromSetup as fromSetup7 } from "@engine/adapter";
1375
- import { loadScene as loadScene7 } from "@engine/scene/scene-loader";
1376
-
1377
- // examples/learn-splines/src/components.ts
1378
- import { GameComponent as GameComponent8 } from "@engine/ecs/game-component";
1379
- import { getUserData } from "@engine/scene/user-data";
1380
- import * as THREE6 from "three";
1381
- import { z as z6 } from "zod";
1382
- var SplineFollow = class extends GameComponent8 {
1383
- constructor() {
1384
- super(...arguments);
1385
- this.speed = 0.15;
1386
- this.t = 0;
1387
- // The engine only guarantees a THREE.Curve (userData.splineCurve's declared
1388
- // type in @engine/scene/user-data) — CatmullRomCurve3 is today's concrete
1389
- // builder, but only base-Curve methods (getPoints/getPointAt) are used below.
1390
- this.curve = null;
1391
- this.marker = null;
1392
- this.line = null;
1393
- }
1394
- static {
1395
- this.phase = "gameLogic";
1396
- }
1397
- static {
1398
- this.schema = z6.object({
1399
- speed: z6.number().default(0.15).describe("Fraction of the path traversed per second")
1400
- });
1401
- }
1402
- init(_ctx) {
1403
- const curve = getUserData(this.object3D, "splineCurve");
1404
- if (!curve) return;
1405
- this.curve = curve;
1406
- const pts = curve.getPoints(160);
1407
- const geo = new THREE6.BufferGeometry().setFromPoints(pts);
1408
- this.line = new THREE6.Line(geo, new THREE6.LineBasicMaterial({ color: "#4aa3ff" }));
1409
- this.object3D.add(this.line);
1410
- this.marker = new THREE6.Mesh(
1411
- new THREE6.SphereGeometry(0.45, 20, 20),
1412
- new THREE6.MeshStandardMaterial({ color: "#ffcc33", emissive: "#aa7700", emissiveIntensity: 0.7 })
1413
- );
1414
- this.object3D.add(this.marker);
1415
- }
1416
- update(dt, _ctx) {
1417
- if (!this.curve || !this.marker) return;
1418
- this.t = (this.t + dt * this.speed) % 1;
1419
- this.curve.getPointAt(this.t, this.marker.position);
1420
- }
1421
- dispose() {
1422
- this.line?.geometry.dispose();
1423
- this.line?.material?.dispose();
1424
- this.marker?.geometry.dispose();
1425
- this.marker?.material?.dispose();
1426
- }
1427
- };
1428
-
1429
- // examples/learn-splines/src/index.ts
1430
- var SCENE_PATH6 = "scenes/learn-splines.vscn.json";
1431
- var registry = { SplineFollow };
1432
- async function setup7(ctx) {
1433
- const sceneInstance = await loadScene7(SCENE_PATH6, {
1434
- scene: ctx.scene,
1435
- rapierWorld: ctx.rapierWorld,
1436
- rapier: ctx.rapier,
1437
- physics: ctx.physics,
1438
- componentManager: ctx.components,
1439
- componentRegistry: registry
1440
- });
1441
- ctx.camera.position.set(0, 8, 12);
1442
- ctx.camera.lookAt(0, 1, 0);
1443
- return {
1444
- dispose() {
1445
- sceneInstance.dispose();
1446
- }
1447
- };
1448
- }
1449
- var adapter7 = fromSetup7("learn-splines", setup7);
1450
-
1451
- // examples/learn-triggers/src/index.ts
1452
- import { fromSetup as fromSetup8 } from "@engine/adapter";
1453
- import { loadScene as loadScene8 } from "@engine/scene/scene-loader";
1454
-
1455
- // examples/learn-triggers/src/scripts/components/scene-camera.ts
1456
- import { z as z7 } from "zod";
1457
- import { GameComponent as GameComponent9 } from "@engine/ecs/game-component";
1458
- var SceneCamera2 = class extends GameComponent9 {
1459
- constructor() {
1460
- super(...arguments);
1461
- this.x = 3;
1462
- this.y = 3;
1463
- this.z = 3;
1464
- this.lookAtX = 0;
1465
- this.lookAtY = 0.5;
1466
- this.lookAtZ = 0;
1467
- }
1468
- static {
1469
- this.phase = "preRender";
1470
- }
1471
- static {
1472
- this.schema = z7.object({
1473
- x: z7.number().default(3).describe("Camera position X"),
1474
- y: z7.number().default(3).describe("Camera position Y"),
1475
- z: z7.number().default(3).describe("Camera position Z"),
1476
- lookAtX: z7.number().default(0).describe("Look-at target X"),
1477
- lookAtY: z7.number().default(0.5).describe("Look-at target Y"),
1478
- lookAtZ: z7.number().default(0).describe("Look-at target Z")
1479
- });
1480
- }
1481
- init(ctx) {
1482
- ctx.camera.position.set(this.x, this.y, this.z);
1483
- ctx.camera.lookAt(this.lookAtX, this.lookAtY, this.lookAtZ);
1484
- }
1485
- update() {
1486
- }
1487
- };
1488
-
1489
- // examples/learn-triggers/src/components.ts
1490
- import { GameComponent as GameComponent10 } from "@engine/ecs/game-component";
1491
- import * as THREE7 from "three";
1492
- import { z as z8 } from "zod";
1493
- var RED = new THREE7.Color("#cc3344");
1494
- var GREEN = new THREE7.Color("#33dd77");
1495
- function matOf(o) {
1496
- const m = o.material;
1497
- return m instanceof THREE7.MeshStandardMaterial ? m : null;
1498
- }
1499
- var TriggerZone = class extends GameComponent10 {
1500
- constructor() {
1501
- super(...arguments);
1502
- this.inside = 0;
1503
- }
1504
- static {
1505
- this.phase = "gameLogic";
1506
- }
1507
- init(_ctx) {
1508
- const m = matOf(this.object3D);
1509
- if (m) {
1510
- m.color.copy(RED);
1511
- m.emissive.copy(RED).multiplyScalar(0.25);
1512
- }
1513
- }
1514
- // The base GameComponent.update is abstract — a sensor zone has no per-frame
1515
- // logic, but must still define it (its behavior is in the trigger callbacks).
1516
- update(_dt, _ctx) {
1517
- }
1518
- // `other` is a raw node — Rapier hands us whatever entered the sensor, not
1519
- // "the Mover" specifically. Discriminate the partner by asking the
1520
- // component manager whether it's a `Mover` (`ctx.components.getComponent`,
1521
- // issue #99), not by matching a display name (`other.name === 'Mover'`
1522
- // would silently break the moment someone renames the entity in the
1523
- // editor). Anything without a `Mover` attached is ignored.
1524
- onTriggerEnter(other, ctx) {
1525
- if (!ctx.components.getComponent(other, Mover)) return;
1526
- this.inside++;
1527
- const m = matOf(this.object3D);
1528
- if (m) {
1529
- m.color.copy(GREEN);
1530
- m.emissive.copy(GREEN).multiplyScalar(0.6);
1531
- }
1532
- }
1533
- onTriggerExit(other, ctx) {
1534
- if (!ctx.components.getComponent(other, Mover)) return;
1535
- this.inside = Math.max(0, this.inside - 1);
1536
- if (this.inside === 0) {
1537
- const m = matOf(this.object3D);
1538
- if (m) {
1539
- m.color.copy(RED);
1540
- m.emissive.copy(RED).multiplyScalar(0.25);
1541
- }
1542
- }
1543
- }
1544
- };
1545
- var Mover = class extends GameComponent10 {
1546
- constructor() {
1547
- super(...arguments);
1548
- this.axis = "x";
1549
- this.range = 9;
1550
- this.speed = 1.5;
1551
- this.t = 0;
1552
- this.base = new THREE7.Vector3();
1553
- }
1554
- static {
1555
- this.phase = "gameLogic";
1556
- }
1557
- static {
1558
- this.schema = z8.object({
1559
- axis: z8.enum(["x", "z"]).default("x").describe("Axis to oscillate along"),
1560
- range: z8.number().default(4).describe("Half travel distance (world units)"),
1561
- speed: z8.number().default(1.5).describe("Oscillation speed")
1562
- });
1563
- }
1564
- init(_ctx) {
1565
- this.base.copy(this.object3D.position);
1566
- this.rigidBody?.setGravityScale(0, true);
1567
- }
1568
- update(dt, _ctx) {
1569
- this.t += dt;
1570
- const body = this.rigidBody;
1571
- if (!body) return;
1572
- const prog = Math.max(0, Math.min(1, this.t / 8));
1573
- const targetX = this.axis === "x" ? this.base.x + prog * this.range : this.base.x;
1574
- const targetZ = this.axis === "z" ? this.base.z + prog * this.range : this.base.z;
1575
- const pos = body.translation();
1576
- body.setLinvel(
1577
- { x: (targetX - pos.x) * 6, y: (this.base.y - pos.y) * 6, z: (targetZ - pos.z) * 6 },
1578
- true
1579
- );
1580
- }
1581
- };
1582
-
1583
- // examples/learn-triggers/src/index.ts
1584
- var SCENE_PATH7 = "scenes/learn-triggers.vscn.json";
1585
- var registry2 = { TriggerZone, Mover, SceneCamera: SceneCamera2 };
1586
- async function setup8(ctx) {
1587
- const sceneInstance = await loadScene8(SCENE_PATH7, {
1588
- scene: ctx.scene,
1589
- rapierWorld: ctx.rapierWorld,
1590
- rapier: ctx.rapier,
1591
- physics: ctx.physics,
1592
- componentManager: ctx.components,
1593
- componentRegistry: registry2
1594
- });
1595
- return {
1596
- dispose() {
1597
- sceneInstance.dispose();
1598
- }
1599
- };
1600
- }
1601
- var adapter8 = fromSetup8("learn-triggers", setup8);
1602
-
1603
- // examples/littlest-tokyo/src/index.tsx
1604
- import * as THREE8 from "three";
1605
- import { OrbitControls } from "three/addons/controls/OrbitControls.js";
1606
- import { fromSetup as fromSetup9 } from "@engine/adapter";
1607
- import { GameComponent as GameComponent11 } from "@engine/ecs/game-component";
1608
- import { shallow as shallow2, useGame as useGame2, useGameState as useGameState2 } from "@engine/react/game-state";
1609
- import { gltfLoader, resolveUrl } from "@engine/loader";
1610
- import { parseSceneFile } from "@engine/scene/parse";
1611
- import { loadSceneFromData } from "@engine/scene/scene-loader";
1612
- import { applyScenePostProcessing } from "@engine/setup/setup-renderer";
1613
-
1614
- // examples/littlest-tokyo/src/ui/Button.tsx
1615
- import { jsx as jsx8 } from "react/jsx-runtime";
1616
-
1617
- // examples/littlest-tokyo/src/ui/KeyHint.tsx
1618
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1619
-
1620
- // examples/littlest-tokyo/src/ui/Panel.tsx
1621
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1622
-
1623
- // examples/littlest-tokyo/src/ui/Slider.tsx
1624
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1625
-
1626
- // examples/littlest-tokyo/src/ui/StatusDot.tsx
1627
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1628
-
1629
- // examples/littlest-tokyo/src/ui/Toggle.tsx
1630
- import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1631
-
1632
- // examples/littlest-tokyo/src/index.tsx
1633
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1634
- var TokyoSettings = class extends GameComponent11 {
1635
- constructor() {
1636
- super(...arguments);
1637
- this.exposure = 1;
1638
- this.animSpeed = 1;
1639
- this.tiltFocus = 0.4;
1640
- this.tiltFeather = 0.3;
1641
- this.bloomIntensity = 0.2;
1642
- this.saturation = 0.15;
1643
- this.setExposure = () => {
1644
- };
1645
- this.setAnimSpeed = () => {
1646
- };
1647
- this.setTiltFocus = () => {
1648
- };
1649
- this.setTiltFeather = () => {
1650
- };
1651
- this.setBloomIntensity = () => {
1652
- };
1653
- this.setSaturation = () => {
1654
- };
1655
- }
1656
- update() {
1657
- }
1658
- };
1659
- async function setup9(ctx) {
1660
- const { scene, camera, composer, rapierWorld, rapier } = ctx;
1661
- const renderer = composer.getRenderer();
1662
- const res = await fetch(resolveUrl("scenes/littlest-tokyo.vscn.json"));
1663
- const sceneData = parseSceneFile(await res.json());
1664
- const tokyoEntity = sceneData.entities.find((e) => e.id === "tokyo");
1665
- sceneData.entities = sceneData.entities.filter((e) => e.id !== "tokyo");
1666
- const sceneInstance = await loadSceneFromData(sceneData, {
1667
- scene,
1668
- rapierWorld,
1669
- rapier,
1670
- physics: ctx.physics
1671
- });
1672
- const gltf = await new Promise(
1673
- (resolve, reject) => {
1674
- gltfLoader.load(
1675
- "models/LittlestTokyo.glb",
1676
- (result) => resolve({ scene: result.scene, animations: result.animations }),
1677
- void 0,
1678
- reject
1679
- );
1680
- }
1681
- );
1682
- const tokyoModel = gltf.scene;
1683
- const scale = tokyoEntity?.transform?.scale ?? [0.01, 0.01, 0.01];
1684
- tokyoModel.scale.set(scale[0], scale[1], scale[2]);
1685
- tokyoModel.traverse((child) => {
1686
- if (child instanceof THREE8.Mesh) {
1687
- child.castShadow = true;
1688
- child.receiveShadow = true;
1689
- }
1690
- });
1691
- scene.add(tokyoModel);
1692
- const mixer = new THREE8.AnimationMixer(tokyoModel);
1693
- for (const clip of gltf.animations) {
1694
- mixer.clipAction(clip).play();
1695
- }
1696
- const env = JSON.parse(JSON.stringify(sceneData.environment ?? {}));
1697
- applyScenePostProcessing(composer, renderer, scene, camera, env);
1698
- camera.position.set(5, 3, 8);
1699
- camera.lookAt(0, 0.5, 0);
1700
- const controls = new OrbitControls(camera, renderer.domElement);
1701
- controls.target.set(0, 0.5, 0);
1702
- controls.maxPolarAngle = Math.PI * 0.45;
1703
- controls.minDistance = 3;
1704
- controls.maxDistance = 20;
1705
- controls.enableDamping = true;
1706
- controls.dampingFactor = 0.05;
1707
- controls.update();
1708
- const gameLogic = (dt) => {
1709
- sceneInstance.update(dt);
1710
- mixer.update(dt);
1711
- controls.update();
1712
- };
1713
- ctx.systems.add("gameLogic", gameLogic);
1714
- const settingsNode = new THREE8.Group();
1715
- settingsNode.name = "TokyoSettings";
1716
- scene.add(settingsNode);
1717
- const settings = new TokyoSettings();
1718
- settings.exposure = env.toneMapping?.exposure ?? 1;
1719
- settings.animSpeed = 1;
1720
- settings.tiltFocus = env.postProcessing?.tiltShift?.focusArea ?? 0.4;
1721
- settings.tiltFeather = env.postProcessing?.tiltShift?.feather ?? 0.3;
1722
- settings.bloomIntensity = env.postProcessing?.bloom?.intensity ?? 0.2;
1723
- settings.saturation = env.postProcessing?.hueSaturation?.saturation ?? 0.15;
1724
- ctx.components.attach(settingsNode, settings);
1725
- function applySettings() {
1726
- if (env.toneMapping) env.toneMapping.exposure = settings.exposure;
1727
- if (env.postProcessing?.tiltShift) {
1728
- env.postProcessing.tiltShift.focusArea = settings.tiltFocus;
1729
- env.postProcessing.tiltShift.feather = settings.tiltFeather;
1730
- }
1731
- if (env.postProcessing?.bloom) env.postProcessing.bloom.intensity = settings.bloomIntensity;
1732
- if (env.postProcessing?.hueSaturation) {
1733
- env.postProcessing.hueSaturation.saturation = settings.saturation;
1734
- }
1735
- applyScenePostProcessing(composer, renderer, scene, camera, env);
1736
- }
1737
- settings.setExposure = (value) => {
1738
- settings.exposure = value;
1739
- applySettings();
1740
- };
1741
- settings.setAnimSpeed = (value) => {
1742
- settings.animSpeed = value;
1743
- mixer.timeScale = value;
1744
- };
1745
- settings.setTiltFocus = (value) => {
1746
- settings.tiltFocus = value;
1747
- applySettings();
1748
- };
1749
- settings.setTiltFeather = (value) => {
1750
- settings.tiltFeather = value;
1751
- applySettings();
1752
- };
1753
- settings.setBloomIntensity = (value) => {
1754
- settings.bloomIntensity = value;
1755
- applySettings();
1756
- };
1757
- settings.setSaturation = (value) => {
1758
- settings.saturation = value;
1759
- applySettings();
1760
- };
1761
- return {
1762
- dispose() {
1763
- ctx.systems.remove("gameLogic", gameLogic);
1764
- mixer.stopAllAction();
1765
- controls.dispose();
1766
- sceneInstance.dispose();
1767
- scene.remove(tokyoModel);
1768
- ctx.components.detach(settingsNode);
1769
- scene.remove(settingsNode);
1770
- }
1771
- };
1772
- }
1773
- var adapter9 = fromSetup9("littlest-tokyo", setup9);
1774
-
1775
- // examples/rts/src/index.ts
1776
- import { fromSetup as fromSetup10 } from "@engine/adapter";
1777
- import { loadScene as loadScene9 } from "@engine/scene/scene-loader";
1778
- import { queryByComponent as queryByComponent3 } from "@engine/scene/scene-query";
1779
- import { getUserData as getUserData3 } from "@engine/scene/user-data";
1780
- import * as THREE10 from "three";
1781
- import { SelectionBox } from "three/addons/interactive/SelectionBox.js";
1782
-
1783
- // examples/rts/src/components.ts
1784
- import {
1785
- initNavigation,
1786
- NavMeshManager
1787
- } from "@engine/ai/navigation";
1788
- import {
1789
- bindXStateAnimation
1790
- } from "@engine/animation/xstate-animation-binding";
1791
- import { GameComponent as GameComponent12 } from "@engine/ecs/game-component";
1792
- import { queryByComponent as queryByComponent2 } from "@engine/scene/scene-query";
1793
- import { getUserData as getUserData2 } from "@engine/scene/user-data";
1794
- import * as THREE9 from "three";
1795
- import { assign, createActor, setup as xstateSetup } from "xstate";
1796
- import { z as z9 } from "zod";
1797
- var FACE_LERP = 0.15;
1798
- var MOVE_SPEED_FACE_THRESHOLD = 0.2;
1799
- var unitAnimationMachine = xstateSetup({
1800
- types: {
1801
- context: {},
1802
- events: {}
1803
- }
1804
- }).createMachine({
1805
- id: "rts-unit-animation",
1806
- initial: "idle",
1807
- context: { speed: 0 },
1808
- on: {
1809
- UPDATE: { actions: assign({ speed: ({ event }) => event.speed }) }
1810
- },
1811
- states: {
1812
- idle: {
1813
- meta: { animation: { clip: "idle", loop: true, crossfade: { duration: 0.2 } } },
1814
- always: { guard: ({ context }) => context.speed > 0.1, target: "locomotion" }
1815
- },
1816
- locomotion: {
1817
- meta: {
1818
- animation: {
1819
- blendTree: {
1820
- type: "1D",
1821
- parameter: "speed",
1822
- children: [
1823
- { clip: "walk", threshold: 1 },
1824
- { clip: "run", threshold: 5 }
1825
- ]
1826
- },
1827
- crossfade: { duration: 0.2 }
1828
- }
1829
- },
1830
- always: { guard: ({ context }) => context.speed <= 0.1, target: "idle" }
1831
- }
1832
- }
1833
- });
1834
- var RTSUnit = class extends GameComponent12 {
1835
- constructor() {
1836
- super(...arguments);
1837
- // Config (defaulted + validated through the schema)
1838
- this.radius = 0.35;
1839
- this.height = 1.8;
1840
- this.maxSpeed = 3.5;
1841
- this.maxAcceleration = 20;
1842
- this.separationWeight = 2;
1843
- this.collisionQueryRange = 2;
1844
- this.pathOptimizationRange = 5;
1845
- // Runtime state
1846
- /** Crowd agent handle — assigned by RTSDirector after the navmesh is built. */
1847
- this.agent = null;
1848
- this.selected = false;
1849
- this.ring = null;
1850
- this.ringGeo = null;
1851
- this.ringMat = null;
1852
- this.animActor = null;
1853
- this.animBinding = null;
1854
- this.lastSpeed = 0;
1855
- this._targetQuat = new THREE9.Quaternion();
1856
- this._up = new THREE9.Vector3(0, 1, 0);
1857
- }
1858
- static {
1859
- this.phase = "gameLogic";
1860
- }
1861
- static {
1862
- this.schema = z9.object({
1863
- radius: z9.number().default(0.35).describe("Crowd agent collision radius (m)"),
1864
- height: z9.number().default(1.8).describe("Crowd agent height (m)"),
1865
- maxSpeed: z9.number().default(3.5).describe("Maximum movement speed (m/s)"),
1866
- maxAcceleration: z9.number().default(20).describe("Maximum acceleration (m/s^2)"),
1867
- separationWeight: z9.number().default(2).describe("How strongly the agent avoids crowding its neighbors"),
1868
- collisionQueryRange: z9.number().default(2).describe("Distance (m) within which neighbors are considered for avoidance"),
1869
- pathOptimizationRange: z9.number().default(5).describe("Distance (m) over which the path is optimized")
1870
- });
1871
- }
1872
- init(_ctx) {
1873
- const ringGeo = new THREE9.RingGeometry(0.35, 0.45, 24);
1874
- ringGeo.rotateX(-Math.PI / 2);
1875
- const ringMat = new THREE9.MeshBasicMaterial({
1876
- color: 4521864,
1877
- transparent: true,
1878
- opacity: 0.7,
1879
- depthWrite: false
1880
- });
1881
- const ring = new THREE9.Mesh(ringGeo, ringMat);
1882
- ring.position.y = 0.05;
1883
- ring.visible = false;
1884
- this.object3D.add(ring);
1885
- this.ring = ring;
1886
- this.ringGeo = ringGeo;
1887
- this.ringMat = ringMat;
1888
- const mixer = getUserData2(this.object3D, "_animMixer");
1889
- const clips = getUserData2(this.object3D, "_animClips");
1890
- if (mixer && clips) {
1891
- this.animActor = createActor(unitAnimationMachine).start();
1892
- this.animBinding = bindXStateAnimation(this.animActor, mixer, clips);
1893
- }
1894
- }
1895
- update(dt, _ctx) {
1896
- const agent = this.agent;
1897
- if (!agent) return;
1898
- const pos = agent.position();
1899
- this.object3D.position.set(pos.x, pos.y, pos.z);
1900
- const vel = agent.velocity();
1901
- const speed = Math.hypot(vel.x, vel.z);
1902
- this.lastSpeed = speed;
1903
- if (speed > MOVE_SPEED_FACE_THRESHOLD) {
1904
- const targetAngle = Math.atan2(vel.x, vel.z) + Math.PI;
1905
- this._targetQuat.setFromAxisAngle(this._up, targetAngle);
1906
- this.object3D.quaternion.slerp(this._targetQuat, FACE_LERP);
1907
- }
1908
- this.animActor?.send({ type: "UPDATE", speed });
1909
- this.animBinding?.tick(dt);
1910
- }
1911
- dispose(_ctx) {
1912
- if (this.ring) {
1913
- this.ring.parent?.remove(this.ring);
1914
- this.ring = null;
1915
- }
1916
- this.ringGeo?.dispose();
1917
- this.ringMat?.dispose();
1918
- this.ringGeo = null;
1919
- this.ringMat = null;
1920
- this.agent = null;
1921
- this.animBinding?.dispose();
1922
- this.animBinding = null;
1923
- this.animActor?.stop();
1924
- this.animActor = null;
1925
- }
1926
- /** Toggle selection state + ring visibility. */
1927
- setSelected(selected) {
1928
- this.selected = selected;
1929
- if (this.ring) this.ring.visible = selected;
1930
- }
1931
- /** Last computed horizontal speed (m/s) — used by RTSDirector's footstep audio. */
1932
- getSpeed() {
1933
- return this.lastSpeed;
1934
- }
1935
- };
1936
- var RTSDirector = class extends GameComponent12 {
1937
- constructor() {
1938
- super(...arguments);
1939
- this.maxAgents = 16;
1940
- // Wired by setup() immediately after construction (not schema-driven — this
1941
- // is code-attached infra, not authored scene data): scene tick + per-unit
1942
- // footstep audio + move-marker fade-out, folded in from the old
1943
- // `gameLogicAudio` closure.
1944
- this.sceneInstance = null;
1945
- this.stepSound = null;
1946
- this.moveMarker = null;
1947
- this.moveMarkerMat = null;
1948
- /** Set by index.ts's move-order handler to start the marker fade-out. */
1949
- this.markerFadeTime = 0;
1950
- this.nav = null;
1951
- /** Unit components this director manages (populated in init). */
1952
- this.units = [];
1953
- this.unitStepTimers = /* @__PURE__ */ new Map();
1954
- }
1955
- static {
1956
- this.phase = "gameLogic";
1957
- }
1958
- static {
1959
- this.schema = z9.object({
1960
- maxAgents: z9.number().default(16).describe("Maximum number of crowd agents")
1961
- });
1962
- }
1963
- async init(ctx) {
1964
- await initNavigation();
1965
- const navMeshSources = [];
1966
- ctx.scene.traverse((child) => {
1967
- if (child instanceof THREE9.Mesh && child.userData["navRole"]) {
1968
- navMeshSources.push(child);
1969
- }
1970
- });
1971
- const nav = new NavMeshManager();
1972
- const cellSize = 0.3;
1973
- const agentRadius = 0.35;
1974
- nav.buildFromMeshes(navMeshSources, {
1975
- cellSize,
1976
- walkableRadius: Math.ceil(agentRadius / cellSize)
1977
- });
1978
- const crowd = nav.createCrowd(this.maxAgents);
1979
- this.nav = nav;
1980
- for (const obj of queryByComponent2(ctx.scene, "RTSUnit")) {
1981
- const unit = ctx.components.getComponents(obj).find((c) => c instanceof RTSUnit);
1982
- if (!unit) continue;
1983
- const pos = obj.position;
1984
- unit.agent = crowd.addAgent(
1985
- { x: pos.x, y: pos.y, z: pos.z },
1986
- {
1987
- radius: unit.radius,
1988
- height: unit.height,
1989
- maxSpeed: unit.maxSpeed,
1990
- maxAcceleration: unit.maxAcceleration,
1991
- separationWeight: unit.separationWeight,
1992
- collisionQueryRange: unit.collisionQueryRange,
1993
- pathOptimizationRange: unit.pathOptimizationRange,
1994
- updateFlags: 7,
1995
- obstacleAvoidanceType: 0,
1996
- queryFilterType: 0
1997
- }
1998
- );
1999
- this.units.push(unit);
2000
- }
2001
- }
2002
- update(dt, ctx) {
2003
- this.nav?.updateCrowd(dt);
2004
- this.sceneInstance?.update(dt);
2005
- if (ctx.input.isJustPressed("rts_deselect")) this.clearSelection();
2006
- for (const unit of this.units) {
2007
- const speed = unit.getSpeed();
2008
- if (speed > 0.5) {
2009
- const timer = (this.unitStepTimers.get(unit) ?? 0) - dt;
2010
- if (timer <= 0) {
2011
- this.stepSound?.play();
2012
- this.unitStepTimers.set(unit, 0.4);
2013
- } else {
2014
- this.unitStepTimers.set(unit, timer);
2015
- }
2016
- } else {
2017
- this.unitStepTimers.set(unit, 0);
2018
- }
2019
- }
2020
- if (this.markerFadeTime > 0 && this.moveMarker && this.moveMarkerMat) {
2021
- this.markerFadeTime -= dt;
2022
- this.moveMarkerMat.opacity = Math.max(0, this.markerFadeTime) * 0.8;
2023
- if (this.markerFadeTime <= 0) this.moveMarker.visible = false;
2024
- }
2025
- }
2026
- dispose(ctx) {
2027
- if (this.nav) {
2028
- this.nav.dispose(ctx.scene);
2029
- this.nav = null;
2030
- }
2031
- this.units.length = 0;
2032
- this.unitStepTimers.clear();
2033
- }
2034
- // --- Selection / order API (iterates the managed unit components) ---
2035
- /** All units this director manages. */
2036
- getUnits() {
2037
- return this.units;
2038
- }
2039
- /** Currently selected units. */
2040
- getSelected() {
2041
- return this.units.filter((u) => u.selected);
2042
- }
2043
- /** Deselect every unit. */
2044
- clearSelection() {
2045
- for (const unit of this.units) unit.setSelected(false);
2046
- }
2047
- /** Select a single unit. */
2048
- selectUnit(unit) {
2049
- unit.setSelected(true);
2050
- }
2051
- /** Issue a move order to a unit's crowd agent. */
2052
- requestMoveTarget(unit, target) {
2053
- unit.agent?.requestMoveTarget(target);
2054
- }
2055
- };
2056
- var RTSCamera = class extends GameComponent12 {
2057
- constructor() {
2058
- super(...arguments);
2059
- this.panSpeed = 15;
2060
- this.yaw = Math.PI / 4;
2061
- // Ground-plane pan basis (derived from `yaw` in init()).
2062
- this.panRightX = 0;
2063
- this.panRightZ = 0;
2064
- this.panUpX = 0;
2065
- this.panUpZ = 0;
2066
- // Authored starting position (read once in init()) + accumulated pan offset.
2067
- this.baseX = 0;
2068
- this.baseZ = 0;
2069
- this.panX = 0;
2070
- this.panZ = 0;
2071
- }
2072
- static {
2073
- this.phase = "preRender";
2074
- }
2075
- static {
2076
- this.schema = z9.object({
2077
- panSpeed: z9.number().default(15).describe("Ground-plane camera pan speed (m/s)"),
2078
- yaw: z9.number().default(Math.PI / 4).describe(
2079
- "Isometric yaw (radians) used to derive the ground-plane pan-direction basis \u2014 must match this camera entity's authored rotation for pan directions to feel right"
2080
- )
2081
- });
2082
- }
2083
- init(_ctx) {
2084
- this.baseX = this.object3D.position.x;
2085
- this.baseZ = this.object3D.position.z;
2086
- this.panRightX = Math.cos(this.yaw);
2087
- this.panRightZ = -Math.sin(this.yaw);
2088
- this.panUpX = Math.sin(this.yaw);
2089
- this.panUpZ = Math.cos(this.yaw);
2090
- }
2091
- update(dt, ctx) {
2092
- let kbX = 0;
2093
- let kbZ = 0;
2094
- if (ctx.input.isPressed("rts_pan_forward")) kbZ -= 1;
2095
- if (ctx.input.isPressed("rts_pan_backward")) kbZ += 1;
2096
- if (ctx.input.isPressed("rts_pan_left")) kbX -= 1;
2097
- if (ctx.input.isPressed("rts_pan_right")) kbX += 1;
2098
- const screenX = Math.sign(kbX);
2099
- const screenZ = Math.sign(kbZ);
2100
- this.panX += (screenX * this.panRightX + screenZ * this.panUpX) * this.panSpeed * dt;
2101
- this.panZ += (screenX * this.panRightZ + screenZ * this.panUpZ) * this.panSpeed * dt;
2102
- this.object3D.position.x = this.baseX + this.panX;
2103
- this.object3D.position.z = this.baseZ + this.panZ;
2104
- }
2105
- };
2106
-
2107
- // examples/rts/src/procedural-audio.ts
2108
- function createNoiseBuffer2(ctx, duration = 1) {
2109
- const length = ctx.sampleRate * duration;
2110
- const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
2111
- const data = buffer.getChannelData(0);
2112
- for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
2113
- return buffer;
2114
- }
2115
- function footstep2(ctx, dest, options) {
2116
- const pitch = options?.pitch ?? 250;
2117
- const volume = options?.volume ?? 0.3;
2118
- const variation = options?.variation ?? 0.2;
2119
- const noiseBuffer = createNoiseBuffer2(ctx, 0.2);
2120
- const out = ctx.createGain();
2121
- out.gain.value = volume;
2122
- out.connect(dest);
2123
- return {
2124
- play() {
2125
- const now = ctx.currentTime;
2126
- const src = ctx.createBufferSource();
2127
- src.buffer = noiseBuffer;
2128
- const filter = ctx.createBiquadFilter();
2129
- filter.type = "bandpass";
2130
- filter.frequency.value = pitch * (1 + (Math.random() - 0.5) * 2 * variation);
2131
- filter.Q.value = 2;
2132
- const env = ctx.createGain();
2133
- env.gain.setValueAtTime(0, now);
2134
- env.gain.linearRampToValueAtTime(1, now + 7e-3);
2135
- env.gain.exponentialRampToValueAtTime(1e-3, now + 0.05);
2136
- src.connect(filter);
2137
- filter.connect(env);
2138
- env.connect(out);
2139
- src.start(now);
2140
- src.stop(now + 0.06);
2141
- },
2142
- dispose() {
2143
- out.disconnect();
2144
- }
2145
- };
2146
- }
2147
- function uiClick(ctx, dest, options) {
2148
- const pitch = options?.pitch ?? 800;
2149
- const volume = options?.volume ?? 0.2;
2150
- const out = ctx.createGain();
2151
- out.gain.value = volume;
2152
- out.connect(dest);
2153
- return {
2154
- play() {
2155
- const now = ctx.currentTime;
2156
- const osc = ctx.createOscillator();
2157
- osc.type = "sine";
2158
- osc.frequency.value = pitch;
2159
- const env = ctx.createGain();
2160
- env.gain.setValueAtTime(1, now);
2161
- env.gain.exponentialRampToValueAtTime(1e-3, now + 0.04);
2162
- osc.connect(env);
2163
- env.connect(out);
2164
- osc.start(now);
2165
- osc.stop(now + 0.05);
2166
- },
2167
- dispose() {
2168
- out.disconnect();
2169
- }
2170
- };
2171
- }
2172
- function uiSuccess(ctx, dest, options) {
2173
- const pitch = options?.pitch ?? 500;
2174
- const volume = options?.volume ?? 0.3;
2175
- const out = ctx.createGain();
2176
- out.gain.value = volume;
2177
- out.connect(dest);
2178
- return {
2179
- play() {
2180
- const now = ctx.currentTime;
2181
- const osc1 = ctx.createOscillator();
2182
- osc1.type = "sine";
2183
- osc1.frequency.value = pitch;
2184
- const env1 = ctx.createGain();
2185
- env1.gain.setValueAtTime(1, now);
2186
- env1.gain.exponentialRampToValueAtTime(1e-3, now + 0.15);
2187
- osc1.connect(env1);
2188
- env1.connect(out);
2189
- osc1.start(now);
2190
- osc1.stop(now + 0.16);
2191
- const osc2 = ctx.createOscillator();
2192
- osc2.type = "sine";
2193
- osc2.frequency.value = pitch * (5 / 4);
2194
- const env2 = ctx.createGain();
2195
- env2.gain.setValueAtTime(1e-3, now);
2196
- env2.gain.setValueAtTime(1, now + 0.1);
2197
- env2.gain.exponentialRampToValueAtTime(1e-3, now + 0.25);
2198
- osc2.connect(env2);
2199
- env2.connect(out);
2200
- osc2.start(now);
2201
- osc2.stop(now + 0.26);
2202
- },
2203
- dispose() {
2204
- out.disconnect();
2205
- }
2206
- };
2207
- }
2208
-
2209
- // examples/rts/src/scripts/registry.ts
2210
- var gameComponents5 = {
2211
- RTSUnit,
2212
- RTSCamera
2213
- };
2214
-
2215
- // examples/rts/src/index.ts
2216
- var SCENE_PATH8 = "scenes/rts.vscn.json";
2217
- var SELECTION_DRAG_THRESHOLD = 5;
2218
- var ORTHO_SIZE_DEFAULT = 18;
2219
- var ORTHO_SIZE_MIN = 8;
2220
- var ORTHO_SIZE_MAX = 36;
2221
- async function setup10(ctx) {
2222
- const { scene, camera } = ctx;
2223
- const canvas = ctx.composer.getRenderer().domElement;
2224
- const localRegistry = {
2225
- ...gameComponents5,
2226
- RTSUnit,
2227
- RTSDirector,
2228
- RTSCamera
2229
- };
2230
- const sceneInstance = await loadScene9(SCENE_PATH8, {
2231
- scene,
2232
- rapierWorld: ctx.rapierWorld,
2233
- rapier: ctx.rapier,
2234
- physics: ctx.physics,
2235
- componentRegistry: localRegistry,
2236
- componentManager: ctx.components,
2237
- audioListener: ctx.audio.listener
2238
- });
2239
- const directorGroup = new THREE10.Group();
2240
- directorGroup.name = "RTSDirector";
2241
- scene.add(directorGroup);
2242
- const director = new RTSDirector();
2243
- ctx.components.attach(directorGroup, director);
2244
- const unitPairs = [];
2245
- for (const obj of queryByComponent3(scene, "RTSUnit")) {
2246
- const unit = ctx.components.getComponents(obj).find((c) => c instanceof RTSUnit);
2247
- if (unit) unitPairs.push({ obj, unit });
2248
- }
2249
- const grid = new THREE10.GridHelper(40, 40, 4880938, 3828250);
2250
- grid.position.y = 0.01;
2251
- scene.add(grid);
2252
- const audioCtx = ctx.audio.listener.context;
2253
- const sfx = ctx.audio.buses.sfx;
2254
- const resumeAudio = () => ctx.audio.resume();
2255
- window.addEventListener("click", resumeAudio, { once: true });
2256
- window.addEventListener("keydown", resumeAudio, { once: true });
2257
- const clickSound = uiClick(audioCtx, sfx, { volume: 0.15 });
2258
- const confirmSound = uiSuccess(audioCtx, sfx, { volume: 0.2 });
2259
- const stepSound = footstep2(audioCtx, sfx, { volume: 0.08, pitch: 300 });
2260
- director.sceneInstance = sceneInstance;
2261
- director.stepSound = stepSound;
2262
- const cameraEntity = queryByComponent3(scene, "RTSCamera")[0];
2263
- const maybeOrthoCamera = getUserData3(cameraEntity, "_camera");
2264
- if (!maybeOrthoCamera) {
2265
- throw new Error("rts: scene is missing its RTSCamera-carrying orthographic camera entity");
2266
- }
2267
- const orthoCamera = maybeOrthoCamera;
2268
- let orthoSize = ORTHO_SIZE_DEFAULT;
2269
- function updateOrthoFrustum() {
2270
- const a = window.innerWidth / window.innerHeight;
2271
- orthoCamera.left = -orthoSize * a / 2;
2272
- orthoCamera.right = orthoSize * a / 2;
2273
- orthoCamera.top = orthoSize / 2;
2274
- orthoCamera.bottom = -orthoSize / 2;
2275
- orthoCamera.updateProjectionMatrix();
2276
- }
2277
- updateOrthoFrustum();
2278
- function onResize() {
2279
- updateOrthoFrustum();
2280
- }
2281
- function onWheel(e) {
2282
- e.preventDefault();
2283
- const factor = Math.exp(e.deltaY * 1e-3);
2284
- orthoSize = Math.max(ORTHO_SIZE_MIN, Math.min(ORTHO_SIZE_MAX, orthoSize * factor));
2285
- updateOrthoFrustum();
2286
- }
2287
- window.addEventListener("resize", onResize);
2288
- canvas.addEventListener("wheel", onWheel, { passive: false });
2289
- ctx.input.registerAction("rts_pan_forward", [
2290
- { type: "key", code: "KeyW" },
2291
- { type: "key", code: "ArrowUp" }
2292
- ]);
2293
- ctx.input.registerAction("rts_pan_backward", [
2294
- { type: "key", code: "KeyS" },
2295
- { type: "key", code: "ArrowDown" }
2296
- ]);
2297
- ctx.input.registerAction("rts_pan_left", [
2298
- { type: "key", code: "KeyA" },
2299
- { type: "key", code: "ArrowLeft" }
2300
- ]);
2301
- ctx.input.registerAction("rts_pan_right", [
2302
- { type: "key", code: "KeyD" },
2303
- { type: "key", code: "ArrowRight" }
2304
- ]);
2305
- ctx.input.registerAction("rts_deselect", [{ type: "key", code: "Escape" }]);
2306
- const composerPasses = ctx.composer.passes;
2307
- for (const pass of composerPasses) {
2308
- pass.mainCamera = orthoCamera;
2309
- }
2310
- canvas.style.cursor = "default";
2311
- let isDragging = false;
2312
- let dragStartX = 0;
2313
- let dragStartY = 0;
2314
- const selectionBox = new SelectionBox(orthoCamera, scene);
2315
- const selectRect = document.createElement("div");
2316
- selectRect.style.cssText = "position:fixed;border:1px solid #55aaff;background:rgba(75,160,255,0.3);pointer-events:none;display:none;z-index:1000;";
2317
- document.body.appendChild(selectRect);
2318
- const meshToUnit = /* @__PURE__ */ new Map();
2319
- for (const { obj, unit } of unitPairs) {
2320
- obj.traverse((child) => {
2321
- if (child instanceof THREE10.Mesh && child !== unit.ring) {
2322
- meshToUnit.set(child, unit);
2323
- }
2324
- });
2325
- }
2326
- function toNDC(clientX, clientY) {
2327
- const rect = canvas.getBoundingClientRect();
2328
- return {
2329
- x: (clientX - rect.left) / rect.width * 2 - 1,
2330
- y: -((clientY - rect.top) / rect.height) * 2 + 1
2331
- };
2332
- }
2333
- function applyBoxSelection(startX, startY, endX, endY) {
2334
- const start = toNDC(startX, startY);
2335
- const end = toNDC(endX, endY);
2336
- selectionBox.startPoint.set(start.x, start.y, 0.5);
2337
- selectionBox.endPoint.set(end.x, end.y, 0.5);
2338
- const selected = selectionBox.select();
2339
- director.clearSelection();
2340
- const selectedUnits = /* @__PURE__ */ new Set();
2341
- for (const obj of selected) {
2342
- const unit = meshToUnit.get(obj);
2343
- if (unit) selectedUnits.add(unit);
2344
- }
2345
- for (const unit of selectedUnits) {
2346
- director.selectUnit(unit);
2347
- clickSound.play();
2348
- }
2349
- }
2350
- function updateSelectRect(clientX, clientY) {
2351
- const left = Math.min(dragStartX, clientX);
2352
- const top = Math.min(dragStartY, clientY);
2353
- selectRect.style.display = "block";
2354
- selectRect.style.left = `${left}px`;
2355
- selectRect.style.top = `${top}px`;
2356
- selectRect.style.width = `${Math.abs(clientX - dragStartX)}px`;
2357
- selectRect.style.height = `${Math.abs(clientY - dragStartY)}px`;
2358
- }
2359
- function findUnitFromObject(obj) {
2360
- let current = obj;
2361
- while (current) {
2362
- const unit = meshToUnit.get(current);
2363
- if (unit) return unit;
2364
- for (const pair of unitPairs) {
2365
- if (pair.obj === current) return pair.unit;
2366
- }
2367
- current = current.parent;
2368
- }
2369
- return void 0;
2370
- }
2371
- const _raycaster = new THREE10.Raycaster();
2372
- let groundMesh = null;
2373
- scene.traverse((child) => {
2374
- if (child instanceof THREE10.Mesh && !groundMesh) {
2375
- const geo = child.geometry;
2376
- if (geo) {
2377
- geo.computeBoundingBox();
2378
- const box = geo.boundingBox;
2379
- const sizeX = box.max.x - box.min.x;
2380
- const sizeZ = box.max.z - box.min.z;
2381
- if (sizeX >= 30 && sizeZ >= 30) groundMesh = child;
2382
- }
2383
- }
2384
- });
2385
- const _moveMarkerGeo = new THREE10.RingGeometry(0.2, 0.35, 16);
2386
- _moveMarkerGeo.rotateX(-Math.PI / 2);
2387
- const _moveMarkerMat = new THREE10.MeshBasicMaterial({
2388
- color: 16763972,
2389
- transparent: true,
2390
- opacity: 0.8,
2391
- depthWrite: false
2392
- });
2393
- const moveMarker = new THREE10.Mesh(_moveMarkerGeo, _moveMarkerMat);
2394
- moveMarker.position.y = 0.05;
2395
- moveMarker.visible = false;
2396
- scene.add(moveMarker);
2397
- director.moveMarker = moveMarker;
2398
- director.moveMarkerMat = _moveMarkerMat;
2399
- function handleMoveCommand(e) {
2400
- const selected = director.getSelected();
2401
- if (selected.length === 0) return;
2402
- const ndc = toNDC(e.clientX, e.clientY);
2403
- _raycaster.setFromCamera(new THREE10.Vector2(ndc.x, ndc.y), orthoCamera);
2404
- const hits = groundMesh ? _raycaster.intersectObject(groundMesh) : _raycaster.intersectObjects(scene.children, true);
2405
- if (hits.length === 0) return;
2406
- const pt = hits[0].point;
2407
- const count = selected.length;
2408
- const cols = Math.ceil(Math.sqrt(count));
2409
- const spacing = 1.2;
2410
- for (let i = 0; i < count; i++) {
2411
- const col = i % cols;
2412
- const row = Math.floor(i / cols);
2413
- const offsetX = (col - (cols - 1) / 2) * spacing;
2414
- const offsetZ = (row - (Math.ceil(count / cols) - 1) / 2) * spacing;
2415
- director.requestMoveTarget(selected[i], {
2416
- x: pt.x + offsetX,
2417
- y: 0,
2418
- z: pt.z + offsetZ
2419
- });
2420
- }
2421
- confirmSound.play();
2422
- moveMarker.position.set(pt.x, 0.05, pt.z);
2423
- moveMarker.visible = true;
2424
- director.markerFadeTime = 1;
2425
- }
2426
- function onPointerDown(e) {
2427
- if (e.button === 2) {
2428
- handleMoveCommand(e);
2429
- return;
2430
- }
2431
- if (e.button !== 0) return;
2432
- isDragging = false;
2433
- dragStartX = e.clientX;
2434
- dragStartY = e.clientY;
2435
- }
2436
- function onPointerMove(e) {
2437
- if (e.buttons !== 1) return;
2438
- if (!isDragging && Math.hypot(e.clientX - dragStartX, e.clientY - dragStartY) > SELECTION_DRAG_THRESHOLD) {
2439
- isDragging = true;
2440
- }
2441
- if (isDragging) {
2442
- updateSelectRect(e.clientX, e.clientY);
2443
- applyBoxSelection(dragStartX, dragStartY, e.clientX, e.clientY);
2444
- }
2445
- }
2446
- function onPointerUp(e) {
2447
- if (e.button !== 0) return;
2448
- selectRect.style.display = "none";
2449
- if (isDragging) {
2450
- applyBoxSelection(dragStartX, dragStartY, e.clientX, e.clientY);
2451
- } else {
2452
- const ndc = toNDC(e.clientX, e.clientY);
2453
- _raycaster.setFromCamera(new THREE10.Vector2(ndc.x, ndc.y), orthoCamera);
2454
- const hits = _raycaster.intersectObjects(scene.children, true);
2455
- director.clearSelection();
2456
- for (const hit3 of hits) {
2457
- const unit = findUnitFromObject(hit3.object);
2458
- if (unit) {
2459
- director.selectUnit(unit);
2460
- clickSound.play();
2461
- break;
2462
- }
2463
- }
2464
- }
2465
- isDragging = false;
2466
- }
2467
- function onContextMenu(e) {
2468
- e.preventDefault();
2469
- }
2470
- canvas.addEventListener("pointerdown", onPointerDown);
2471
- canvas.addEventListener("pointermove", onPointerMove);
2472
- canvas.addEventListener("pointerup", onPointerUp);
2473
- canvas.addEventListener("contextmenu", onContextMenu);
2474
- return {
2475
- dispose() {
2476
- ctx.components.detach(directorGroup);
2477
- scene.remove(directorGroup);
2478
- sceneInstance.dispose();
2479
- window.removeEventListener("resize", onResize);
2480
- canvas.removeEventListener("wheel", onWheel);
2481
- canvas.removeEventListener("pointerdown", onPointerDown);
2482
- canvas.removeEventListener("pointermove", onPointerMove);
2483
- canvas.removeEventListener("pointerup", onPointerUp);
2484
- canvas.removeEventListener("contextmenu", onContextMenu);
2485
- canvas.style.cursor = "";
2486
- selectRect.remove();
2487
- for (const pass of composerPasses) {
2488
- pass.mainCamera = camera;
2489
- }
2490
- clickSound.dispose();
2491
- confirmSound.dispose();
2492
- stepSound.dispose();
2493
- scene.remove(moveMarker);
2494
- _moveMarkerGeo.dispose();
2495
- _moveMarkerMat.dispose();
2496
- scene.remove(grid);
2497
- }
2498
- };
2499
- }
2500
- var adapter10 = fromSetup10("rts", setup10);
2501
-
2502
- // examples/starfighter/src/index.ts
2503
- import { fromSetup as fromSetup11 } from "@engine/adapter";
2504
- import { loadScene as loadScene10 } from "@engine/scene/scene-loader";
2505
- import { applyScenePostProcessing as applyScenePostProcessing2 } from "@engine/setup/setup-renderer";
2506
-
2507
- // examples/starfighter/src/components.ts
2508
- import * as THREE11 from "three";
2509
- import { z as z10 } from "zod";
2510
- import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
2511
- import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
2512
- import { GameComponent as GameComponent13 } from "@engine/ecs/game-component";
2513
- import { gltfLoader as gltfLoader2, textureLoader, loadingManager } from "@engine/loader";
2514
- var flight = { scroll: 0 };
2515
- var StarfighterController = class extends GameComponent13 {
2516
- constructor() {
2517
- super(...arguments);
2518
- this.speed = 90;
2519
- this.steer = 55;
2520
- this.disposables = [];
2521
- // Virtual steering accumulators, integrated from mouse delta each frame. All
2522
- // input flows through the engine InputManager (ctx.input) — never raw window
2523
- // listeners — so the editor host can focus-gate it (raw listeners bypass the
2524
- // gate and leak into the game while you work in the editor).
2525
- this.mouseX = 0;
2526
- this.mouseY = 0;
2527
- this.headingX = 0;
2528
- this.headingY = 0;
2529
- this.prevX = 0;
2530
- this.bank = 0;
2531
- this.pitch = 0;
2532
- this.t = 0;
2533
- }
2534
- static {
2535
- this.phase = "gameLogic";
2536
- }
2537
- static {
2538
- this.schema = z10.object({
2539
- speed: z10.number().default(90).describe("Forward cruise speed (units/sec)"),
2540
- steer: z10.number().default(55).describe("Max lateral steering reach")
2541
- });
2542
- }
2543
- track(x) {
2544
- this.disposables.push(x);
2545
- return x;
2546
- }
2547
- init(ctx) {
2548
- const ship = this.object3D;
2549
- const metalTex = (file, srgb = false) => {
2550
- const tx = textureLoader.load(`textures/metal/${file}`);
2551
- tx.wrapS = tx.wrapT = THREE11.RepeatWrapping;
2552
- if (srgb) tx.colorSpace = THREE11.SRGBColorSpace;
2553
- return this.track(tx);
2554
- };
2555
- const hullMat = this.track(
2556
- new THREE11.MeshStandardMaterial({
2557
- map: metalTex("Metal038_2K_Color.jpg", true),
2558
- normalMap: metalTex("Metal038_2K_NormalGL.jpg"),
2559
- roughnessMap: metalTex("Metal038_2K_Roughness.jpg"),
2560
- metalnessMap: metalTex("Metal038_2K_Metalness.jpg"),
2561
- metalness: 1,
2562
- roughness: 1,
2563
- envMapIntensity: 1.2,
2564
- color: 10134706
2565
- })
2566
- );
2567
- const accentMat = this.track(new THREE11.MeshStandardMaterial({ color: 1119516, metalness: 0.9, roughness: 0.35 }));
2568
- const glassMat = this.track(
2569
- new THREE11.MeshStandardMaterial({ color: 660512, metalness: 0.1, roughness: 0.05, emissive: 1718874, emissiveIntensity: 0.4, envMapIntensity: 1.5 })
2570
- );
2571
- this.engineMat = this.track(new THREE11.MeshStandardMaterial({ color: 3399167, emissive: 3399167, emissiveIntensity: 2.2 }));
2572
- const parts = [];
2573
- const push = (geo, x = 0, y = 0, z13 = 0, rx = 0, ry = 0, rz = 0) => {
2574
- geo.rotateX(rx);
2575
- geo.rotateY(ry);
2576
- geo.rotateZ(rz);
2577
- geo.translate(x, y, z13);
2578
- parts.push(geo.index ? geo.toNonIndexed() : geo);
2579
- };
2580
- push(new THREE11.CapsuleGeometry(0.9, 3.2, 8, 20), 0, 0, 0.4, Math.PI / 2, 0, 0);
2581
- push(new THREE11.ConeGeometry(0.9, 2.4, 20), 0, 0, 3.3, Math.PI / 2, 0, 0);
2582
- const wl = new THREE11.BoxGeometry(5.6, 0.18, 1.8);
2583
- wl.rotateY(-0.5);
2584
- wl.rotateZ(0.06);
2585
- wl.translate(-1.2, -0.1, -0.6);
2586
- parts.push(wl.toNonIndexed());
2587
- const wr = new THREE11.BoxGeometry(5.6, 0.18, 1.8);
2588
- wr.rotateY(0.5);
2589
- wr.rotateZ(-0.06);
2590
- wr.translate(1.2, -0.1, -0.6);
2591
- parts.push(wr.toNonIndexed());
2592
- push(new THREE11.BoxGeometry(0.16, 1.7, 1.6), 0, 0.85, -1.9);
2593
- push(new THREE11.CylinderGeometry(0.28, 0.28, 2.2, 12), -3.6, -0.1, -0.5, Math.PI / 2, 0, 0);
2594
- push(new THREE11.CylinderGeometry(0.28, 0.28, 2.2, 12), 3.6, -0.1, -0.5, Math.PI / 2, 0, 0);
2595
- const hullGeo = this.track(mergeGeometries(parts, false));
2596
- ship.add(new THREE11.Mesh(hullGeo, hullMat));
2597
- const bellGeo = this.track(new THREE11.CylinderGeometry(0.55, 0.7, 1.1, 16));
2598
- const glowGeo = this.track(new THREE11.CircleGeometry(0.5, 16));
2599
- for (const sx of [-0.9, 0.9]) {
2600
- const bell = new THREE11.Mesh(bellGeo, accentMat);
2601
- bell.rotation.x = Math.PI / 2;
2602
- bell.position.set(sx, -0.05, -2.4);
2603
- ship.add(bell);
2604
- const glow = new THREE11.Mesh(glowGeo, this.engineMat);
2605
- glow.position.set(sx, -0.05, -2.95);
2606
- glow.rotation.y = Math.PI;
2607
- ship.add(glow);
2608
- }
2609
- const canopyGeo = this.track(new THREE11.SphereGeometry(0.6, 16, 12, 0, Math.PI * 2, 0, Math.PI * 0.6));
2610
- const canopy = new THREE11.Mesh(canopyGeo, glassMat);
2611
- canopy.scale.set(0.8, 0.7, 1.6);
2612
- canopy.position.set(0, 0.5, 1.2);
2613
- ship.add(canopy);
2614
- this.engineLight = new THREE11.PointLight(3399167, 6, 40, 2);
2615
- this.engineLight.position.set(0, 0, -3);
2616
- ship.add(this.engineLight);
2617
- const n = 6e3;
2618
- const pos = new Float32Array(n * 3);
2619
- const col = new Float32Array(n * 3);
2620
- const c = new THREE11.Color();
2621
- for (let i = 0; i < n; i++) {
2622
- const u = Math.random() * 2 - 1;
2623
- const th = Math.random() * Math.PI * 2;
2624
- const r = Math.sqrt(1 - u * u);
2625
- pos[i * 3] = Math.cos(th) * r * 4500;
2626
- pos[i * 3 + 1] = u * 4500;
2627
- pos[i * 3 + 2] = Math.sin(th) * r * 4500;
2628
- c.setHSL(0.55 + Math.random() * 0.12, 0.5, 0.5 + Math.random() * 0.5);
2629
- col[i * 3] = c.r;
2630
- col[i * 3 + 1] = c.g;
2631
- col[i * 3 + 2] = c.b;
2632
- }
2633
- const sg = this.track(new THREE11.BufferGeometry());
2634
- sg.setAttribute("position", new THREE11.BufferAttribute(pos, 3));
2635
- sg.setAttribute("color", new THREE11.BufferAttribute(col, 3));
2636
- const sm = this.track(new THREE11.PointsMaterial({ size: 2.4, sizeAttenuation: false, vertexColors: true, transparent: true, depthWrite: false }));
2637
- this.starfield = new THREE11.Points(sg, sm);
2638
- this.starfield.frustumCulled = false;
2639
- ctx.scene.add(this.starfield);
2640
- const renderer = ctx.composer.getRenderer();
2641
- const pmrem = new THREE11.PMREMGenerator(renderer);
2642
- pmrem.compileEquirectangularShader();
2643
- new RGBELoader(loadingManager).load("textures/dikhololo_night_2k.hdr", (hdr) => {
2644
- hdr.mapping = THREE11.EquirectangularReflectionMapping;
2645
- ctx.scene.environment = pmrem.fromEquirectangular(hdr).texture;
2646
- hdr.dispose();
2647
- pmrem.dispose();
2648
- });
2649
- }
2650
- update(dt, ctx) {
2651
- this.t += dt;
2652
- const ship = this.object3D;
2653
- const input = ctx.input;
2654
- const kx = (input.isPressed("move_left") ? -1 : 0) + (input.isPressed("move_right") ? 1 : 0);
2655
- const ky = (input.isPressed("move_forward") ? 1 : 0) + (input.isPressed("move_backward") ? -1 : 0);
2656
- const md = input.getMouseDelta();
2657
- this.mouseX = THREE11.MathUtils.clamp(this.mouseX + md.x * 3e-3, -1, 1);
2658
- this.mouseY = THREE11.MathUtils.clamp(this.mouseY + md.y * 3e-3, -1, 1);
2659
- const tx = this.mouseX * this.steer + kx * this.steer;
2660
- const ty = -this.mouseY * (this.steer * 0.58) + ky * (this.steer * 0.58);
2661
- this.headingX += (tx - this.headingX) * (1 - Math.exp(-dt * 1.6));
2662
- this.headingY += (ty - this.headingY) * (1 - Math.exp(-dt * 1.6));
2663
- flight.scroll += this.speed * dt;
2664
- ship.position.set(this.headingX, this.headingY + 2, 0);
2665
- const desiredBank = THREE11.MathUtils.clamp((this.headingX - this.prevX) * 6, -0.6, 0.6);
2666
- this.prevX = this.headingX;
2667
- this.bank += (desiredBank - this.bank) * (1 - Math.exp(-dt * 4));
2668
- this.pitch += (this.headingY * -4e-3 - this.pitch) * (1 - Math.exp(-dt * 3));
2669
- ship.rotation.set(this.pitch, 0, this.bank);
2670
- const flick = 3.4 + Math.sin(this.t * 40) * 0.4 + Math.random() * 0.3;
2671
- this.engineMat.emissiveIntensity = flick;
2672
- this.engineLight.intensity = 4 + flick;
2673
- }
2674
- dispose(ctx) {
2675
- if (this.starfield) ctx.scene.remove(this.starfield);
2676
- if (ctx.scene.environment) {
2677
- ctx.scene.environment.dispose();
2678
- ctx.scene.environment = null;
2679
- }
2680
- for (const d of this.disposables) d.dispose();
2681
- }
2682
- };
2683
- var AsteroidField = class extends GameComponent13 {
2684
- constructor() {
2685
- super(...arguments);
2686
- this.perKind = 240;
2687
- this.radius = 120;
2688
- this.length = 2600;
2689
- this.meshes = [];
2690
- this.insts = [];
2691
- this.dummy = new THREE11.Object3D();
2692
- }
2693
- static {
2694
- this.phase = "gameLogic";
2695
- }
2696
- static {
2697
- this.schema = z10.object({
2698
- perKind: z10.number().default(240).describe("Instances per rock kind (\xD73 kinds)"),
2699
- radius: z10.number().default(120).describe("Tube radius around the flight path"),
2700
- length: z10.number().default(2600).describe("Depth of the field before it wraps")
2701
- });
2702
- }
2703
- async init() {
2704
- const kinds = ["moon_rock_02", "moon_rock_04", "moon_rock_06"];
2705
- await Promise.all(
2706
- kinds.map(
2707
- (name, kind) => new Promise((resolve) => {
2708
- gltfLoader2.load(`models/rocks/${name}/${name}_2k.gltf`, (g) => {
2709
- let src;
2710
- g.scene.traverse((o) => {
2711
- if (o.isMesh) src = o;
2712
- });
2713
- if (!src) return resolve();
2714
- src.material.envMapIntensity = 1;
2715
- const inst = new THREE11.InstancedMesh(src.geometry, src.material, this.perKind);
2716
- inst.instanceMatrix.setUsage(THREE11.DynamicDrawUsage);
2717
- inst.frustumCulled = false;
2718
- for (let i = 0; i < this.perKind; i++) {
2719
- const ang = Math.random() * Math.PI * 2;
2720
- const rad = 18 + Math.random() * this.radius;
2721
- const base = new THREE11.Vector3(Math.cos(ang) * rad, (Math.random() - 0.5) * this.radius, Math.random() * this.length);
2722
- const scale = (1.4 + Math.random() * 6) * (kind === 2 ? 1.6 : 1);
2723
- const rot = new THREE11.Euler(Math.random() * 7, Math.random() * 7, Math.random() * 7);
2724
- const spin = new THREE11.Vector3((Math.random() - 0.5) * 0.4, (Math.random() - 0.5) * 0.4, (Math.random() - 0.5) * 0.4);
2725
- this.insts.push({ mesh: inst, i, base, scale, rot, spin });
2726
- }
2727
- this.object3D.add(inst);
2728
- this.meshes.push(inst);
2729
- resolve();
2730
- });
2731
- })
2732
- )
2733
- );
2734
- }
2735
- update(dt) {
2736
- for (const rk of this.insts) {
2737
- rk.rot.x += rk.spin.x * dt;
2738
- rk.rot.y += rk.spin.y * dt;
2739
- rk.rot.z += rk.spin.z * dt;
2740
- let z13 = rk.base.z - flight.scroll;
2741
- if (z13 < -60) {
2742
- rk.base.z += this.length;
2743
- z13 = rk.base.z - flight.scroll;
2744
- }
2745
- this.dummy.position.set(rk.base.x, rk.base.y, z13);
2746
- this.dummy.rotation.copy(rk.rot);
2747
- this.dummy.scale.setScalar(rk.scale);
2748
- this.dummy.updateMatrix();
2749
- rk.mesh.setMatrixAt(rk.i, this.dummy.matrix);
2750
- }
2751
- for (const m of this.meshes) m.instanceMatrix.needsUpdate = true;
2752
- }
2753
- dispose() {
2754
- for (const m of this.meshes) {
2755
- this.object3D.remove(m);
2756
- m.dispose();
2757
- }
2758
- this.meshes = [];
2759
- this.insts = [];
2760
- }
2761
- };
2762
- var StarfighterChaseCamera = class extends GameComponent13 {
2763
- constructor() {
2764
- super(...arguments);
2765
- this.smooth = 3;
2766
- this.desired = new THREE11.Vector3();
2767
- }
2768
- static {
2769
- this.phase = "preRender";
2770
- }
2771
- static {
2772
- this.schema = z10.object({
2773
- smooth: z10.number().default(3).describe("Camera follow responsiveness")
2774
- });
2775
- }
2776
- update(dt, ctx) {
2777
- const p = this.object3D.position;
2778
- this.desired.set(p.x * 0.7 - 6, p.y + 3, p.z - 10);
2779
- ctx.camera.position.lerp(this.desired, 1 - Math.exp(-dt * this.smooth));
2780
- ctx.camera.lookAt(p.x * 0.7 + 1, p.y + 1, p.z + 5);
2781
- }
2782
- };
2783
-
2784
- // examples/starfighter/src/scripts/registry.ts
2785
- var componentRegistry2 = {
2786
- StarfighterController,
2787
- AsteroidField,
2788
- StarfighterChaseCamera
2789
- };
2790
-
2791
- // examples/starfighter/src/index.ts
2792
- var SCENE_PATH9 = "scenes/starfighter.vscn.json";
2793
- async function setup11(ctx) {
2794
- const renderer = ctx.composer.getRenderer();
2795
- const sceneInstance = await loadScene10(SCENE_PATH9, {
2796
- scene: ctx.scene,
2797
- rapierWorld: ctx.rapierWorld,
2798
- rapier: ctx.rapier,
2799
- physics: ctx.physics,
2800
- componentRegistry: componentRegistry2,
2801
- componentManager: ctx.components,
2802
- audioListener: ctx.audio.listener,
2803
- renderer,
2804
- particleRenderer: ctx.particles.batchedRenderer
2805
- });
2806
- applyScenePostProcessing2(ctx.composer, renderer, ctx.scene, ctx.camera, sceneInstance.environment);
2807
- const sceneTick = (dt) => sceneInstance.update(dt);
2808
- ctx.systems.add("postPhysics", sceneTick);
2809
- return {
2810
- dispose() {
2811
- sceneInstance.dispose();
2812
- }
2813
- };
2814
- }
2815
- var adapter11 = fromSetup11("starfighter", setup11);
2816
-
2817
- // examples/third-person/src/index.ts
2818
- import { fromSetup as fromSetup12 } from "@engine/adapter";
2819
- import { loadScene as loadScene11 } from "@engine/scene/scene-loader";
2820
- import { queryByComponent as queryByComponent5 } from "@engine/scene/scene-query";
2821
- import * as THREE13 from "three";
2822
-
2823
- // examples/third-person/src/components.ts
2824
- import {
2825
- bindXStateAnimation as bindXStateAnimation2
2826
- } from "@engine/animation/xstate-animation-binding";
2827
- import { GameComponent as GameComponent14 } from "@engine/ecs/game-component";
2828
- import { queryByComponent as queryByComponent4 } from "@engine/scene/scene-query";
2829
- import { getUserData as getUserData4 } from "@engine/scene/user-data";
2830
- import * as THREE12 from "three";
2831
- import { assign as assign2, createActor as createActor2, setup as xstateSetup2 } from "xstate";
2832
- import { z as z11 } from "zod";
2833
- var ANIM_SPEED_SCALE = 1.25;
2834
- var characterAnimationMachine = xstateSetup2({
2835
- types: {
2836
- context: {},
2837
- events: {}
2838
- }
2839
- }).createMachine({
2840
- id: "character-animation",
2841
- initial: "idle",
2842
- context: { speed: 0, grounded: true },
2843
- on: {
2844
- UPDATE: {
2845
- actions: assign2({
2846
- speed: ({ event }) => event.speed,
2847
- grounded: ({ event }) => event.grounded
2848
- })
2849
- }
2850
- },
2851
- states: {
2852
- idle: {
2853
- meta: { animation: { clip: "idle", loop: true, crossfade: { duration: 0.2 } } },
2854
- on: { JUMP: "airborne" },
2855
- always: { guard: ({ context }) => context.speed > 0.1, target: "locomotion" }
2856
- },
2857
- locomotion: {
2858
- meta: {
2859
- animation: {
2860
- blendTree: {
2861
- type: "1D",
2862
- parameter: "speed",
2863
- children: [
2864
- { clip: "walk", threshold: 1 },
2865
- { clip: "run", threshold: 5 }
2866
- ]
2867
- },
2868
- crossfade: { duration: 0.2 }
2869
- }
2870
- },
2871
- on: { JUMP: "airborne" },
2872
- always: { guard: ({ context }) => context.speed <= 0.1, target: "idle" }
2873
- },
2874
- airborne: {
2875
- meta: { animation: { clip: "jump", loop: false, crossfade: { duration: 0.1 } } },
2876
- always: [
2877
- {
2878
- guard: ({ context }) => context.grounded && context.speed > 0.1,
2879
- target: "locomotion"
2880
- },
2881
- { guard: ({ context }) => context.grounded, target: "idle" }
2882
- ]
2883
- }
2884
- }
2885
- });
2886
- var CharacterController = class extends GameComponent14 {
2887
- constructor() {
2888
- super(...arguments);
2889
- // Config (defaulted + validated through the schema)
2890
- this.moveSpeed = 4;
2891
- this.jumpVelocity = 6;
2892
- this.gravity = 15;
2893
- this.turnSpeed = 0.15;
2894
- this.offset = 0.02;
2895
- this.maxSlopeAngle = Math.PI / 4;
2896
- this.autostepMaxHeight = 0.4;
2897
- this.autostepMinWidth = 0.2;
2898
- this.snapToGround = 0.3;
2899
- this.applyImpulses = true;
2900
- this.mass = 1;
2901
- // Runtime state (plain instance fields; old `facingAngles` Map collapses to one)
2902
- this.isGroundedState = true;
2903
- this.verticalVelocity = 0;
2904
- this.facingAngle = 0;
2905
- this.correctedSpeed = 0;
2906
- this.jumpSequence = 0;
2907
- // Runtime handles
2908
- this.controller = null;
2909
- this.cameraRef = null;
2910
- // Reusable math
2911
- this._forward = new THREE12.Vector3();
2912
- this._right = new THREE12.Vector3();
2913
- this._moveDir = new THREE12.Vector3();
2914
- this._euler = new THREE12.Euler();
2915
- this._quat = new THREE12.Quaternion();
2916
- }
2917
- static {
2918
- this.phase = "prePhysics";
2919
- }
2920
- static {
2921
- this.schema = z11.object({
2922
- moveSpeed: z11.number().default(4).describe("Horizontal movement speed (m/s)"),
2923
- jumpVelocity: z11.number().default(6).describe("Upward velocity applied on jump (m/s)"),
2924
- gravity: z11.number().default(15).describe("Downward gravity magnitude (m/s^2)"),
2925
- turnSpeed: z11.number().default(0.15).describe("Facing rotation lerp factor toward the movement direction (0-1)"),
2926
- offset: z11.number().default(0.02).describe("KinematicCharacterController collide-and-slide skin width / offset (m)"),
2927
- maxSlopeAngle: z11.number().default(Math.PI / 4).describe("Maximum slope climb angle (radians)"),
2928
- autostepMaxHeight: z11.number().default(0.4).describe("Maximum step height the character can auto-step over (m)"),
2929
- autostepMinWidth: z11.number().default(0.2).describe("Minimum step width required for auto-stepping (m)"),
2930
- snapToGround: z11.number().default(0.3).describe("Snap-to-ground distance keeping the character on the floor (m)"),
2931
- applyImpulses: z11.boolean().default(true).describe("Whether the character pushes dynamic bodies it collides with"),
2932
- mass: z11.number().default(1).describe("Character mass used when pushing dynamic bodies (kg)")
2933
- });
2934
- }
2935
- init(ctx) {
2936
- if (!this.rigidBody || !this.collider) return;
2937
- const ctrl = ctx.rapierWorld.createCharacterController(this.offset);
2938
- ctrl.setUp({ x: 0, y: 1, z: 0 });
2939
- ctrl.setMaxSlopeClimbAngle(this.maxSlopeAngle);
2940
- ctrl.enableAutostep(this.autostepMaxHeight, this.autostepMinWidth, true);
2941
- ctrl.enableSnapToGround(this.snapToGround);
2942
- ctrl.setSlideEnabled(true);
2943
- ctrl.setApplyImpulsesToDynamicBodies(this.applyImpulses);
2944
- ctrl.setCharacterMass(this.mass);
2945
- this.controller = ctrl;
2946
- this.isGroundedState = true;
2947
- this.cameraRef = ctx.components.getComponents(this.object3D).find((c) => c instanceof CameraTarget) ?? null;
2948
- }
2949
- update(dt, ctx) {
2950
- const body = this.rigidBody;
2951
- const collider = this.collider;
2952
- const controller = this.controller;
2953
- if (!body || !collider || !controller) return;
2954
- const input = ctx.input;
2955
- const yaw = this.cameraRef?.yaw ?? 0;
2956
- this._forward.set(-Math.sin(yaw), 0, -Math.cos(yaw)).normalize();
2957
- this._right.set(-this._forward.z, 0, this._forward.x);
2958
- let inputX = 0;
2959
- let inputZ = 0;
2960
- if (input.isPressed("move_forward")) inputZ += 1;
2961
- if (input.isPressed("move_backward")) inputZ -= 1;
2962
- if (input.isPressed("move_left")) inputX -= 1;
2963
- if (input.isPressed("move_right")) inputX += 1;
2964
- this._moveDir.set(0, 0, 0);
2965
- if (inputX !== 0 || inputZ !== 0) {
2966
- this._moveDir.addScaledVector(this._forward, inputZ);
2967
- this._moveDir.addScaledVector(this._right, inputX);
2968
- this._moveDir.normalize();
2969
- }
2970
- const horizontalSpeed = this._moveDir.length() * this.moveSpeed;
2971
- let vVel = this.verticalVelocity;
2972
- const grounded = this.isGroundedState;
2973
- if (grounded) {
2974
- vVel = 0;
2975
- if (input.isJustPressed("jump")) {
2976
- vVel = this.jumpVelocity;
2977
- this.jumpSequence++;
2978
- }
2979
- }
2980
- vVel += -this.gravity * dt;
2981
- if (vVel > 0) {
2982
- controller.disableSnapToGround();
2983
- } else {
2984
- controller.enableSnapToGround(this.snapToGround);
2985
- }
2986
- controller.computeColliderMovement(
2987
- collider,
2988
- {
2989
- x: this._moveDir.x * this.moveSpeed * dt,
2990
- y: vVel * dt,
2991
- z: this._moveDir.z * this.moveSpeed * dt
2992
- },
2993
- ctx.rapier.QueryFilterFlags.EXCLUDE_SENSORS
2994
- );
2995
- const corrected = controller.computedMovement();
2996
- const nowGrounded = controller.computedGrounded();
2997
- const pos = body.translation();
2998
- body.setNextKinematicTranslation({
2999
- x: pos.x + corrected.x,
3000
- y: pos.y + corrected.y,
3001
- z: pos.z + corrected.z
3002
- });
3003
- if (horizontalSpeed > 0.1) {
3004
- const targetAngle = Math.atan2(this._moveDir.x, this._moveDir.z);
3005
- let diff = targetAngle - this.facingAngle;
3006
- while (diff > Math.PI) diff -= Math.PI * 2;
3007
- while (diff < -Math.PI) diff += Math.PI * 2;
3008
- this.facingAngle += diff * this.turnSpeed;
3009
- this._euler.set(0, this.facingAngle, 0);
3010
- this._quat.setFromEuler(this._euler);
3011
- body.setNextKinematicRotation({
3012
- x: this._quat.x,
3013
- y: this._quat.y,
3014
- z: this._quat.z,
3015
- w: this._quat.w
3016
- });
3017
- }
3018
- if (nowGrounded && vVel < 0) vVel = 0;
3019
- this.verticalVelocity = vVel;
3020
- this.isGroundedState = nowGrounded;
3021
- this.correctedSpeed = horizontalSpeed * ANIM_SPEED_SCALE;
3022
- }
3023
- dispose(ctx) {
3024
- if (this.controller) {
3025
- ctx.rapierWorld.removeCharacterController(this.controller);
3026
- this.controller = null;
3027
- }
3028
- this.cameraRef = null;
3029
- }
3030
- /** Whether the character is currently on the ground (used by audio). */
3031
- isGrounded() {
3032
- return this.isGroundedState;
3033
- }
3034
- };
3035
- var CharacterAnimation = class extends GameComponent14 {
3036
- constructor() {
3037
- super(...arguments);
3038
- this.controller = null;
3039
- this.actor = null;
3040
- this.binding = null;
3041
- this.seenJumpSequence = 0;
3042
- }
3043
- static {
3044
- this.phase = "animation";
3045
- }
3046
- init(ctx) {
3047
- this.controller = ctx.components.getComponents(this.object3D).find((component) => component instanceof CharacterController) ?? null;
3048
- const mixer = getUserData4(this.object3D, "_animMixer");
3049
- const clips = getUserData4(this.object3D, "_animClips");
3050
- if (!this.controller || !mixer || !clips) {
3051
- throw new Error(
3052
- "CharacterAnimation requires a sibling CharacterController and scene-loaded animation clips."
3053
- );
3054
- }
3055
- this.actor = createActor2(characterAnimationMachine).start();
3056
- this.binding = bindXStateAnimation2(this.actor, mixer, clips);
3057
- this.seenJumpSequence = this.controller.jumpSequence;
3058
- }
3059
- update(dt) {
3060
- const controller = this.controller;
3061
- const actor = this.actor;
3062
- if (!controller || !actor) return;
3063
- if (controller.jumpSequence !== this.seenJumpSequence) {
3064
- this.seenJumpSequence = controller.jumpSequence;
3065
- actor.send({ type: "JUMP" });
3066
- }
3067
- actor.send({
3068
- type: "UPDATE",
3069
- speed: controller.correctedSpeed,
3070
- grounded: controller.isGroundedState
3071
- });
3072
- this.binding?.tick(dt);
3073
- }
3074
- dispose() {
3075
- this.binding?.dispose();
3076
- this.binding = null;
3077
- this.actor?.stop();
3078
- this.actor = null;
3079
- this.controller = null;
3080
- }
3081
- };
3082
- var CameraTarget = class extends GameComponent14 {
3083
- constructor() {
3084
- super(...arguments);
3085
- // Config (defaulted + validated through the schema)
3086
- this.distance = 6;
3087
- this.lookAtHeight = 1.2;
3088
- this.sensitivity = 3e-3;
3089
- this.minPitch = 0.1;
3090
- this.maxPitch = 1.4;
3091
- // Runtime orbit state (plain instance fields)
3092
- this.yaw = 0;
3093
- this.pitch = 0.4;
3094
- this.pointerLocked = false;
3095
- // DOM wiring
3096
- this.canvas = null;
3097
- this.onPointerLockChange = null;
3098
- this.onCanvasClick = null;
3099
- this._offset = new THREE12.Vector3();
3100
- }
3101
- static {
3102
- this.phase = "preRender";
3103
- }
3104
- static {
3105
- this.schema = z11.object({
3106
- distance: z11.number().default(6).describe("Camera distance from the target (m)"),
3107
- lookAtHeight: z11.number().default(1.2).describe("Height above the target the camera looks at (m)"),
3108
- sensitivity: z11.number().default(3e-3).describe("Mouse / look-stick sensitivity (radians/pixel)"),
3109
- minPitch: z11.number().default(0.1).describe("Minimum camera pitch in radians"),
3110
- maxPitch: z11.number().default(1.4).describe("Maximum camera pitch in radians")
3111
- });
3112
- }
3113
- init(ctx) {
3114
- const canvas = ctx.composer.getRenderer().domElement;
3115
- this.canvas = canvas;
3116
- this.onPointerLockChange = () => {
3117
- this.pointerLocked = document.pointerLockElement === canvas;
3118
- };
3119
- this.onCanvasClick = () => {
3120
- if (!this.pointerLocked) canvas.requestPointerLock();
3121
- };
3122
- document.addEventListener("pointerlockchange", this.onPointerLockChange);
3123
- canvas.addEventListener("click", this.onCanvasClick);
3124
- this.positionCamera(ctx);
3125
- }
3126
- update(_dt, ctx) {
3127
- const md = ctx.input.getMouseDelta();
3128
- if (md.x !== 0 || md.y !== 0) {
3129
- this.yaw -= md.x * this.sensitivity;
3130
- this.pitch = Math.max(
3131
- this.minPitch,
3132
- Math.min(this.maxPitch, this.pitch + md.y * this.sensitivity)
3133
- );
3134
- }
3135
- this.positionCamera(ctx);
3136
- }
3137
- positionCamera(ctx) {
3138
- const targetPos = this.object3D.position;
3139
- this._offset.set(
3140
- this.distance * Math.cos(this.pitch) * Math.sin(this.yaw),
3141
- this.distance * Math.sin(this.pitch),
3142
- this.distance * Math.cos(this.pitch) * Math.cos(this.yaw)
3143
- );
3144
- ctx.camera.position.copy(targetPos).add(this._offset);
3145
- ctx.camera.lookAt(targetPos.x, targetPos.y + this.lookAtHeight, targetPos.z);
3146
- }
3147
- dispose(_ctx) {
3148
- if (this.onPointerLockChange) {
3149
- document.removeEventListener("pointerlockchange", this.onPointerLockChange);
3150
- this.onPointerLockChange = null;
3151
- }
3152
- if (this.onCanvasClick && this.canvas) {
3153
- this.canvas.removeEventListener("click", this.onCanvasClick);
3154
- }
3155
- this.onCanvasClick = null;
3156
- this.canvas = null;
3157
- if (document.pointerLockElement) document.exitPointerLock();
3158
- }
3159
- };
3160
- var AIFollow = class extends GameComponent14 {
3161
- constructor() {
3162
- super(...arguments);
3163
- this.speed = 2;
3164
- this.range = 15;
3165
- this.stopDistance = 1.5;
3166
- this.playerObj = null;
3167
- }
3168
- static {
3169
- this.phase = "gameLogic";
3170
- }
3171
- static {
3172
- this.schema = z11.object({
3173
- speed: z11.number().default(2).describe("Horizontal chase speed (m/s)"),
3174
- range: z11.number().default(15).describe("Max distance at which the enemy will chase (m)"),
3175
- stopDistance: z11.number().default(1.5).describe("Distance at which the enemy stops chasing (m)")
3176
- });
3177
- }
3178
- init(ctx) {
3179
- this.playerObj = queryByComponent4(ctx.scene, "Player")[0] ?? null;
3180
- }
3181
- update(_dt, _ctx) {
3182
- const body = this.rigidBody;
3183
- const player = this.playerObj;
3184
- if (!body || !player) return;
3185
- const ex = this.object3D.position.x;
3186
- const ez = this.object3D.position.z;
3187
- const dx = player.position.x - ex;
3188
- const dz = player.position.z - ez;
3189
- const dist = Math.sqrt(dx * dx + dz * dz);
3190
- if (dist > this.range || dist < this.stopDistance) return;
3191
- const nx = dx / dist;
3192
- const nz = dz / dist;
3193
- const vel = body.linvel();
3194
- body.setLinvel({ x: nx * this.speed, y: vel.y, z: nz * this.speed }, true);
3195
- }
3196
- };
3197
-
3198
- // examples/third-person/src/procedural-audio.ts
3199
- function createNoiseBuffer3(ctx, duration = 1) {
3200
- const length = ctx.sampleRate * duration;
3201
- const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
3202
- const data = buffer.getChannelData(0);
3203
- for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
3204
- return buffer;
3205
- }
3206
- function footstep3(ctx, dest, options) {
3207
- const pitch = options?.pitch ?? 250;
3208
- const volume = options?.volume ?? 0.3;
3209
- const variation = options?.variation ?? 0.2;
3210
- const noiseBuffer = createNoiseBuffer3(ctx, 0.2);
3211
- const out = ctx.createGain();
3212
- out.gain.value = volume;
3213
- out.connect(dest);
3214
- return {
3215
- play() {
3216
- const now = ctx.currentTime;
3217
- const src = ctx.createBufferSource();
3218
- src.buffer = noiseBuffer;
3219
- const filter = ctx.createBiquadFilter();
3220
- filter.type = "bandpass";
3221
- filter.frequency.value = pitch * (1 + (Math.random() - 0.5) * 2 * variation);
3222
- filter.Q.value = 2;
3223
- const env = ctx.createGain();
3224
- env.gain.setValueAtTime(0, now);
3225
- env.gain.linearRampToValueAtTime(1, now + 7e-3);
3226
- env.gain.exponentialRampToValueAtTime(1e-3, now + 0.05);
3227
- src.connect(filter);
3228
- filter.connect(env);
3229
- env.connect(out);
3230
- src.start(now);
3231
- src.stop(now + 0.06);
3232
- },
3233
- dispose() {
3234
- out.disconnect();
3235
- }
3236
- };
3237
- }
3238
- function jump2(ctx, dest, options) {
3239
- const pitch = options?.pitch ?? 150;
3240
- const volume = options?.volume ?? 0.3;
3241
- const duration = options?.duration ?? 0.15;
3242
- const out = ctx.createGain();
3243
- out.gain.value = volume;
3244
- out.connect(dest);
3245
- return {
3246
- play() {
3247
- const now = ctx.currentTime;
3248
- const osc = ctx.createOscillator();
3249
- osc.type = "triangle";
3250
- osc.frequency.setValueAtTime(pitch, now);
3251
- osc.frequency.exponentialRampToValueAtTime(pitch * 2, now + duration);
3252
- const env = ctx.createGain();
3253
- env.gain.setValueAtTime(0.8, now);
3254
- env.gain.exponentialRampToValueAtTime(1e-3, now + duration);
3255
- osc.connect(env);
3256
- env.connect(out);
3257
- osc.start(now);
3258
- osc.stop(now + duration + 0.01);
3259
- },
3260
- dispose() {
3261
- out.disconnect();
3262
- }
3263
- };
3264
- }
3265
- function hit2(ctx, dest, options) {
3266
- const pitch = options?.pitch ?? 400;
3267
- const volume = options?.volume ?? 0.5;
3268
- const duration = options?.duration ?? 0.1;
3269
- const brightness = options?.brightness ?? 1;
3270
- const noiseBuffer = createNoiseBuffer3(ctx, 0.5);
3271
- const out = ctx.createGain();
3272
- out.gain.value = volume;
3273
- out.connect(dest);
3274
- return {
3275
- play() {
3276
- const now = ctx.currentTime;
3277
- const src = ctx.createBufferSource();
3278
- src.buffer = noiseBuffer;
3279
- const filter = ctx.createBiquadFilter();
3280
- filter.type = "bandpass";
3281
- filter.frequency.value = pitch;
3282
- filter.Q.value = brightness;
3283
- const env = ctx.createGain();
3284
- env.gain.setValueAtTime(1, now);
3285
- env.gain.exponentialRampToValueAtTime(1e-3, now + duration);
3286
- src.connect(filter);
3287
- filter.connect(env);
3288
- env.connect(out);
3289
- src.start(now);
3290
- src.stop(now + duration + 0.01);
3291
- },
3292
- dispose() {
3293
- out.disconnect();
3294
- }
3295
- };
3296
- }
3297
-
3298
- // examples/third-person/src/components/player.ts
3299
- import { GameComponent as GameComponent15 } from "@engine/ecs/game-component";
3300
- var Player2 = class extends GameComponent15 {
3301
- update() {
3302
- }
3303
- };
3304
-
3305
- // examples/third-person/src/scripts/registry.ts
3306
- var gameComponents6 = {
3307
- Player: Player2,
3308
- CharacterController,
3309
- CharacterAnimation,
3310
- CameraTarget,
3311
- AIFollow
3312
- };
3313
-
3314
- // examples/third-person/src/index.ts
3315
- var SCENE_PATH10 = "scenes/third-person.vscn.json";
3316
- async function setup12(ctx) {
3317
- const { scene, rapierWorld, rapier, input, systems } = ctx;
3318
- const localRegistry = {
3319
- ...gameComponents6,
3320
- CharacterController,
3321
- CharacterAnimation,
3322
- CameraTarget,
3323
- AIFollow
3324
- };
3325
- const sceneInstance = await loadScene11(SCENE_PATH10, {
3326
- scene,
3327
- rapierWorld,
3328
- rapier,
3329
- physics: ctx.physics,
3330
- componentRegistry: localRegistry,
3331
- componentManager: ctx.components,
3332
- audioListener: ctx.audio.listener
3333
- });
3334
- const playerObj = queryByComponent5(scene, "CharacterController")[0];
3335
- const character = playerObj ? ctx.components.getComponents(playerObj).find((c) => c instanceof CharacterController) : void 0;
3336
- const grid = new THREE13.GridHelper(40, 40, 8947848, 7829367);
3337
- grid.position.y = 0.01;
3338
- scene.add(grid);
3339
- const resumeAudio = () => ctx.audio.resume();
3340
- window.addEventListener("click", resumeAudio, { once: true });
3341
- window.addEventListener("keydown", resumeAudio, { once: true });
3342
- const audioCtx = ctx.audio.listener.context;
3343
- const sfx = ctx.audio.buses.sfx;
3344
- const stepSound = footstep3(audioCtx, sfx, { volume: 0.4 });
3345
- const jmpSound = jump2(audioCtx, sfx, { volume: 0.5 });
3346
- const landSound = hit2(audioCtx, sfx, { pitch: 200, volume: 0.45, duration: 0.08 });
3347
- let footstepTimer = 0;
3348
- let wasGrounded = true;
3349
- function audioTick(dt) {
3350
- sceneInstance.update(dt);
3351
- const grounded = character?.isGrounded() ?? false;
3352
- if (!wasGrounded && grounded) landSound.play();
3353
- wasGrounded = grounded;
3354
- const moving = input.isPressed("move_forward") || input.isPressed("move_backward") || input.isPressed("move_left") || input.isPressed("move_right");
3355
- if (grounded && moving) {
3356
- footstepTimer -= dt;
3357
- if (footstepTimer <= 0) {
3358
- stepSound.play();
3359
- footstepTimer = 0.3;
3360
- }
3361
- } else {
3362
- footstepTimer = 0;
3363
- }
3364
- if (input.isJustPressed("jump") && grounded) {
3365
- jmpSound.play();
3366
- }
3367
- }
3368
- systems.add("postPhysics", audioTick);
3369
- return {
3370
- dispose() {
3371
- systems.remove("postPhysics", audioTick);
3372
- sceneInstance.dispose();
3373
- window.removeEventListener("click", resumeAudio);
3374
- window.removeEventListener("keydown", resumeAudio);
3375
- stepSound.dispose();
3376
- jmpSound.dispose();
3377
- landSound.dispose();
3378
- scene.remove(grid);
3379
- grid.geometry.dispose();
3380
- grid.material.dispose();
3381
- }
3382
- };
3383
- }
3384
- var adapter12 = fromSetup12("third-person", setup12);
3385
-
3386
- // examples/vehicle/src/index.ts
3387
- import { fromSetup as fromSetup13 } from "@engine/adapter";
3388
- import { loadScene as loadScene12 } from "@engine/scene/scene-loader";
3389
- import { queryByComponent as queryByComponent6 } from "@engine/scene/scene-query";
3390
- import * as THREE15 from "three";
3391
-
3392
- // examples/vehicle/src/components.ts
3393
- import * as THREE14 from "three";
3394
- import { z as z12 } from "zod";
3395
- import { GameComponent as GameComponent16 } from "@engine/ecs/game-component";
3396
- var ENGINE_STEP = 1;
3397
- var STEER_LERP = 0.25;
3398
- var BRAKE_STEP = 0.05;
3399
- var wheelPositions = [
3400
- { x: -1, y: 0, z: -1.5 },
3401
- // front-left
3402
- { x: 1, y: 0, z: -1.5 },
3403
- // front-right
3404
- { x: -1, y: 0, z: 1.5 },
3405
- // rear-left
3406
- { x: 1, y: 0, z: 1.5 }
3407
- // rear-right
3408
- ];
3409
- var VehicleController = class extends GameComponent16 {
3410
- constructor() {
3411
- super(...arguments);
3412
- // Config (defaulted + validated through the schema)
3413
- this.wheelRadius = 0.3;
3414
- this.wheelWidth = 0.4;
3415
- this.suspensionRest = 0.8;
3416
- this.suspensionStiffness = 24;
3417
- this.frictionSlip = 1e3;
3418
- this.maxEngineForce = 30;
3419
- this.maxSteer = Math.PI / 4;
3420
- this.maxBrake = 1;
3421
- // Runtime state
3422
- this.vehicle = null;
3423
- this.wheelMeshes = [];
3424
- this.wheelGeo = null;
3425
- this.wheelMat = null;
3426
- this.accelValue = 0;
3427
- this.brakeValue = 0;
3428
- this.braking = false;
3429
- }
3430
- static {
3431
- this.phase = "prePhysics";
3432
- }
3433
- static {
3434
- this.schema = z12.object({
3435
- wheelRadius: z12.number().default(0.3).describe("Wheel collider/visual radius in meters"),
3436
- wheelWidth: z12.number().default(0.4).describe("Wheel visual width (cylinder length) in meters"),
3437
- suspensionRest: z12.number().default(0.8).describe("Suspension rest length in meters"),
3438
- suspensionStiffness: z12.number().default(24).describe("Suspension spring stiffness"),
3439
- frictionSlip: z12.number().default(1e3).describe("Tire friction slip coefficient"),
3440
- maxEngineForce: z12.number().default(30).describe("Maximum engine force applied to drive wheels"),
3441
- maxSteer: z12.number().default(Math.PI / 4).describe("Maximum steering angle in radians"),
3442
- maxBrake: z12.number().default(1).describe("Maximum brake force")
3443
- });
3444
- }
3445
- init(ctx) {
3446
- const chassis = this.rigidBody;
3447
- if (!chassis) return;
3448
- const vehicle = ctx.rapierWorld.createVehicleController(chassis);
3449
- this.vehicle = vehicle;
3450
- const wheelGeo = new THREE14.CylinderGeometry(
3451
- this.wheelRadius,
3452
- this.wheelRadius,
3453
- this.wheelWidth,
3454
- 16
3455
- );
3456
- wheelGeo.rotateZ(Math.PI / 2);
3457
- const wheelMat = new THREE14.MeshStandardMaterial({ color: 2236962, roughness: 0.9 });
3458
- this.wheelGeo = wheelGeo;
3459
- this.wheelMat = wheelMat;
3460
- for (let i = 0; i < 4; i++) {
3461
- const wp = wheelPositions[i];
3462
- vehicle.addWheel(
3463
- wp,
3464
- { x: 0, y: -1, z: 0 },
3465
- // suspension direction (down)
3466
- { x: -1, y: 0, z: 0 },
3467
- // axle direction (left)
3468
- this.suspensionRest,
3469
- this.wheelRadius
3470
- );
3471
- vehicle.setWheelSuspensionStiffness(i, this.suspensionStiffness);
3472
- vehicle.setWheelFrictionSlip(i, this.frictionSlip);
3473
- const wMesh = new THREE14.Mesh(wheelGeo, wheelMat);
3474
- wMesh.castShadow = true;
3475
- wMesh.position.set(wp.x, wp.y, wp.z);
3476
- this.object3D.add(wMesh);
3477
- this.wheelMeshes.push(wMesh);
3478
- }
3479
- }
3480
- update(dt, ctx) {
3481
- const vehicle = this.vehicle;
3482
- const chassis = this.rigidBody;
3483
- if (!vehicle || !chassis) return;
3484
- const input = ctx.input;
3485
- if (input.isPressed("attack")) {
3486
- chassis.setTranslation({ x: 0, y: 2, z: 0 }, true);
3487
- chassis.setRotation({ x: 0, y: 0, z: 0, w: 1 }, true);
3488
- chassis.setLinvel({ x: 0, y: 0, z: 0 }, true);
3489
- chassis.setAngvel({ x: 0, y: 0, z: 0 }, true);
3490
- this.accelValue = 0;
3491
- this.brakeValue = 0;
3492
- this.braking = false;
3493
- return;
3494
- }
3495
- const fwd = input.isPressed("move_forward");
3496
- const back = input.isPressed("move_backward");
3497
- if (fwd) {
3498
- this.accelValue = Math.max(this.accelValue - ENGINE_STEP, -this.maxEngineForce);
3499
- } else if (back) {
3500
- this.accelValue = Math.min(this.accelValue + ENGINE_STEP, this.maxEngineForce);
3501
- } else {
3502
- this.accelValue = Math.abs(this.accelValue * 0.95) < 0.1 ? 0 : this.accelValue * 0.95;
3503
- if (chassis.isSleeping()) chassis.wakeUp();
3504
- }
3505
- let steerTarget = 0;
3506
- if (input.isPressed("move_left")) steerTarget = this.maxSteer;
3507
- if (input.isPressed("move_right")) steerTarget = -this.maxSteer;
3508
- const currentSteer = vehicle.wheelSteering(0) ?? 0;
3509
- const steering = THREE14.MathUtils.lerp(currentSteer, steerTarget, STEER_LERP);
3510
- let brake = 0;
3511
- if (input.isPressed("jump")) {
3512
- brake = this.brakeValue + BRAKE_STEP;
3513
- if (brake > this.maxBrake) brake = this.maxBrake;
3514
- }
3515
- this.brakeValue = brake;
3516
- this.braking = input.isPressed("jump") && this.brakeValue > 0;
3517
- vehicle.setWheelEngineForce(0, this.accelValue);
3518
- vehicle.setWheelEngineForce(1, this.accelValue);
3519
- vehicle.setWheelEngineForce(2, 0);
3520
- vehicle.setWheelEngineForce(3, 0);
3521
- vehicle.setWheelSteering(0, steering);
3522
- vehicle.setWheelSteering(1, steering);
3523
- const wheelBrake = input.isPressed("jump") ? brake : 0;
3524
- for (let i = 0; i < 4; i++) {
3525
- vehicle.setWheelBrake(i, wheelBrake);
3526
- }
3527
- vehicle.updateVehicle(dt);
3528
- }
3529
- dispose(ctx) {
3530
- if (this.vehicle) {
3531
- ctx.rapierWorld.removeVehicleController(this.vehicle);
3532
- this.vehicle = null;
3533
- }
3534
- for (const wMesh of this.wheelMeshes) {
3535
- wMesh.parent?.remove(wMesh);
3536
- }
3537
- this.wheelMeshes.length = 0;
3538
- this.wheelGeo?.dispose();
3539
- this.wheelMat?.dispose();
3540
- this.wheelGeo = null;
3541
- this.wheelMat = null;
3542
- }
3543
- /** Current vehicle speed — used by audio and HUD. */
3544
- getSpeed() {
3545
- return this.vehicle?.currentVehicleSpeed?.() ?? 0;
3546
- }
3547
- /** Whether brake is currently applied. */
3548
- isBraking() {
3549
- return this.braking;
3550
- }
3551
- /** Normalized speed (0-1) for audio throttle. */
3552
- getNormalizedSpeed() {
3553
- const speed = this.vehicle?.currentVehicleSpeed?.() ?? 0;
3554
- return Math.min(Math.abs(speed) / 20, 1);
3555
- }
3556
- /** Rapier vehicle controller handle — read by the sibling VehicleWheelSync
3557
- * component (its per-wheel steer/suspension/spin state, computed here in
3558
- * prePhysics, is cached on this handle and stays valid for VehicleWheelSync
3559
- * to read back out in postPhysics). */
3560
- get rapierVehicle() {
3561
- return this.vehicle;
3562
- }
3563
- /** Wheel visual meshes (children of this chassis) — read by VehicleWheelSync. */
3564
- get wheels() {
3565
- return this.wheelMeshes;
3566
- }
3567
- };
3568
- var VehicleWheelSync = class extends GameComponent16 {
3569
- constructor() {
3570
- super(...arguments);
3571
- this.controllerRef = null;
3572
- this._up = new THREE14.Vector3(0, 1, 0);
3573
- this._steerQuat = new THREE14.Quaternion();
3574
- this._spinQuat = new THREE14.Quaternion();
3575
- }
3576
- static {
3577
- this.phase = "postPhysics";
3578
- }
3579
- init(ctx) {
3580
- this.controllerRef = ctx.components.getComponents(this.object3D).find((c) => c instanceof VehicleController) ?? null;
3581
- }
3582
- update(_dt, _ctx) {
3583
- const controller = this.controllerRef;
3584
- const vehicle = controller?.rapierVehicle;
3585
- if (!controller || !vehicle) return;
3586
- const wheels = controller.wheels;
3587
- for (let i = 0; i < 4; i++) {
3588
- const wMesh = wheels[i];
3589
- if (!wMesh) continue;
3590
- const wp = wheelPositions[i];
3591
- const axleCs = vehicle.wheelAxleCs(i);
3592
- const connection = vehicle.wheelChassisConnectionPointCs(i)?.y ?? wp.y;
3593
- const suspension = vehicle.wheelSuspensionLength(i) ?? 0;
3594
- const steer = vehicle.wheelSteering(i) ?? 0;
3595
- const spin = vehicle.wheelRotation(i) ?? 0;
3596
- wMesh.position.set(wp.x, connection - suspension, wp.z);
3597
- this._steerQuat.setFromAxisAngle(this._up, steer);
3598
- this._spinQuat.setFromAxisAngle(axleCs, spin);
3599
- wMesh.quaternion.multiplyQuaternions(this._steerQuat, this._spinQuat);
3600
- }
3601
- }
3602
- dispose(_ctx) {
3603
- this.controllerRef = null;
3604
- }
3605
- };
3606
- var ChaseCamera = class extends GameComponent16 {
3607
- constructor() {
3608
- super(...arguments);
3609
- this.offsetX = 0;
3610
- this.offsetY = 5;
3611
- this.offsetZ = 10;
3612
- this.smooth = 0.08;
3613
- this._desiredPos = new THREE14.Vector3();
3614
- this._target = new THREE14.Vector3();
3615
- this._quat = new THREE14.Quaternion();
3616
- }
3617
- static {
3618
- this.phase = "preRender";
3619
- }
3620
- static {
3621
- this.schema = z12.object({
3622
- offsetX: z12.number().default(0).describe("Camera offset from chassis along local X"),
3623
- offsetY: z12.number().default(5).describe("Camera offset from chassis along local Y"),
3624
- offsetZ: z12.number().default(10).describe("Camera offset from chassis along local Z"),
3625
- smooth: z12.number().default(0.08).describe("Camera position lerp factor (0-1)")
3626
- });
3627
- }
3628
- update(_dt, ctx) {
3629
- const chassis = this.rigidBody;
3630
- if (!chassis) return;
3631
- const ct = chassis.translation();
3632
- const cr = chassis.rotation();
3633
- this._target.set(ct.x, ct.y + 1, ct.z);
3634
- this._desiredPos.set(this.offsetX, this.offsetY, this.offsetZ);
3635
- this._quat.set(cr.x, cr.y, cr.z, cr.w);
3636
- this._desiredPos.applyQuaternion(this._quat);
3637
- this._desiredPos.add(this._target);
3638
- ctx.camera.position.lerp(this._desiredPos, this.smooth);
3639
- ctx.camera.lookAt(this._target);
3640
- }
3641
- };
3642
-
3643
- // examples/vehicle/src/procedural-audio.ts
3644
- function engineDrone(ctx, dest, options) {
3645
- const baseFreq = options?.baseFreq ?? 60;
3646
- const maxVol = options?.volume ?? 0.4;
3647
- const out = ctx.createGain();
3648
- out.gain.value = 0;
3649
- out.connect(dest);
3650
- const shaper = ctx.createWaveShaper();
3651
- const curve = new Float32Array(256);
3652
- for (let i = 0; i < 256; i++) {
3653
- const x = (i / 128 - 1) * 1.5;
3654
- curve[i] = Math.tanh(x);
3655
- }
3656
- shaper.curve = curve;
3657
- shaper.connect(out);
3658
- let osc = null;
3659
- return {
3660
- play() {
3661
- if (osc) return;
3662
- osc = ctx.createOscillator();
3663
- osc.type = "sawtooth";
3664
- osc.frequency.value = baseFreq;
3665
- osc.connect(shaper);
3666
- osc.start();
3667
- out.gain.setTargetAtTime(0.1, ctx.currentTime, 0.05);
3668
- },
3669
- stop() {
3670
- if (!osc) return;
3671
- out.gain.setTargetAtTime(0, ctx.currentTime, 0.05);
3672
- const ref = osc;
3673
- osc = null;
3674
- setTimeout(() => {
3675
- try {
3676
- ref.stop();
3677
- } catch {
3678
- }
3679
- ref.disconnect();
3680
- }, 200);
3681
- },
3682
- /** Set throttle 0–1. Adjusts pitch (1x–3x base) and volume. */
3683
- setThrottle(t) {
3684
- if (!osc) return;
3685
- const clamped = Math.max(0, Math.min(1, t));
3686
- osc.frequency.setTargetAtTime(baseFreq + clamped * baseFreq * 2, ctx.currentTime, 0.05);
3687
- out.gain.setTargetAtTime(0.1 + clamped * maxVol, ctx.currentTime, 0.05);
3688
- },
3689
- dispose() {
3690
- if (osc) {
3691
- try {
3692
- osc.stop();
3693
- } catch {
3694
- }
3695
- osc.disconnect();
3696
- osc = null;
3697
- }
3698
- shaper.disconnect();
3699
- out.disconnect();
3700
- }
3701
- };
3702
- }
3703
- function brakeSqueal(ctx, dest, options) {
3704
- const pitch = options?.pitch ?? 2e3;
3705
- const maxVol = options?.volume ?? 0.3;
3706
- const out = ctx.createGain();
3707
- out.gain.value = 0;
3708
- out.connect(dest);
3709
- let osc = null;
3710
- let lfo = null;
3711
- let lfoGain = null;
3712
- return {
3713
- play() {
3714
- if (osc) return;
3715
- osc = ctx.createOscillator();
3716
- osc.type = "sine";
3717
- osc.frequency.value = pitch;
3718
- lfo = ctx.createOscillator();
3719
- lfo.type = "sine";
3720
- lfo.frequency.value = 30;
3721
- lfoGain = ctx.createGain();
3722
- lfoGain.gain.value = 0.3;
3723
- lfo.connect(lfoGain);
3724
- lfoGain.connect(out.gain);
3725
- osc.connect(out);
3726
- osc.start();
3727
- lfo.start();
3728
- },
3729
- stop() {
3730
- if (!osc) return;
3731
- out.gain.setTargetAtTime(0, ctx.currentTime, 0.02);
3732
- const refs = { osc, lfo, lfoGain };
3733
- osc = null;
3734
- lfo = null;
3735
- lfoGain = null;
3736
- setTimeout(() => {
3737
- try {
3738
- refs.osc.stop();
3739
- } catch {
3740
- }
3741
- refs.osc.disconnect();
3742
- try {
3743
- refs.lfo?.stop();
3744
- } catch {
3745
- }
3746
- refs.lfo?.disconnect();
3747
- refs.lfoGain?.disconnect();
3748
- }, 100);
3749
- },
3750
- /** Set brake intensity 0–1. */
3751
- setIntensity(i) {
3752
- out.gain.setTargetAtTime(Math.max(0, Math.min(1, i)) * maxVol, ctx.currentTime, 0.02);
3753
- },
3754
- dispose() {
3755
- if (osc) {
3756
- try {
3757
- osc.stop();
3758
- } catch {
3759
- }
3760
- osc.disconnect();
3761
- }
3762
- if (lfo) {
3763
- try {
3764
- lfo.stop();
3765
- } catch {
3766
- }
3767
- lfo.disconnect();
3768
- }
3769
- lfoGain?.disconnect();
3770
- osc = null;
3771
- lfo = null;
3772
- lfoGain = null;
3773
- out.disconnect();
3774
- }
3775
- };
3776
- }
3777
-
3778
- // examples/vehicle/src/scripts/registry.ts
3779
- var gameComponents7 = {
3780
- VehicleController,
3781
- VehicleWheelSync,
3782
- ChaseCamera
3783
- };
3784
-
3785
- // examples/vehicle/src/index.ts
3786
- var SCENE_PATH11 = "scenes/vehicle.vscn.json";
3787
- async function setup13(ctx) {
3788
- const { scene, rapierWorld, rapier, systems } = ctx;
3789
- const localRegistry = {
3790
- ...gameComponents7,
3791
- VehicleController,
3792
- VehicleWheelSync,
3793
- ChaseCamera
3794
- };
3795
- const sceneInstance = await loadScene12(SCENE_PATH11, {
3796
- scene,
3797
- rapierWorld,
3798
- rapier,
3799
- physics: ctx.physics,
3800
- componentRegistry: localRegistry,
3801
- componentManager: ctx.components,
3802
- audioListener: ctx.audio.listener
3803
- });
3804
- const vehicleObj = queryByComponent6(scene, "VehicleController")[0];
3805
- const veh = vehicleObj ? ctx.components.getComponents(vehicleObj).find((c) => c instanceof VehicleController) : void 0;
3806
- const grid = new THREE15.GridHelper(100, 100, 11184810, 10066329);
3807
- grid.position.y = 0.01;
3808
- scene.add(grid);
3809
- const audioCtx = ctx.audio.listener.context;
3810
- const sfx = ctx.audio.buses.sfx;
3811
- const resumeAudio = () => ctx.audio.resume();
3812
- window.addEventListener("click", resumeAudio, { once: true });
3813
- window.addEventListener("keydown", resumeAudio, { once: true });
3814
- const engine = engineDrone(audioCtx, sfx, { volume: 0.3 });
3815
- const brakeSound = brakeSqueal(audioCtx, sfx, { volume: 0.2 });
3816
- engine.play();
3817
- let brakeActive = false;
3818
- function audioSystem(dt) {
3819
- sceneInstance.update(dt);
3820
- if (!veh) return;
3821
- engine.setThrottle(veh.getNormalizedSpeed());
3822
- const speed = veh.getSpeed();
3823
- if (veh.isBraking() && Math.abs(speed) > 2) {
3824
- if (!brakeActive) {
3825
- brakeSound.play();
3826
- brakeActive = true;
3827
- }
3828
- brakeSound.setIntensity(Math.min(1, Math.abs(speed) / 20));
3829
- } else if (brakeActive) {
3830
- brakeSound.stop();
3831
- brakeActive = false;
3832
- }
3833
- }
3834
- systems.add("postPhysics", audioSystem);
3835
- return {
3836
- dispose() {
3837
- systems.remove("postPhysics", audioSystem);
3838
- sceneInstance.dispose();
3839
- engine.dispose();
3840
- brakeSound.dispose();
3841
- scene.remove(grid);
3842
- grid.geometry.dispose();
3843
- grid.material.dispose();
3844
- }
3845
- };
3846
- }
3847
- var adapter13 = fromSetup13("vehicle", setup13);
3848
-
3849
- // packages/editor/src/generated/setup-by-example.generated.ts
3850
- var SETUP_BY_EXAMPLE_ID = {
3851
- "editor-tutorial": setup,
3852
- "feature-scenes": setup2,
3853
- fps: setup3,
3854
- ik: setup4,
3855
- "learn-joints": setup5,
3856
- "learn-matfiles": setup6,
3857
- "learn-splines": setup7,
3858
- "learn-triggers": setup8,
3859
- "littlest-tokyo": setup9,
3860
- rts: setup10,
3861
- starfighter: setup11,
3862
- "third-person": setup12,
3863
- vehicle: setup13
3864
- };
3865
-
3866
- // packages/editor/src/browser-play-entry.ts
3867
- var DEFAULT_SCENE = "scenes/default.vscn.json";
3868
- var components = gameComponents;
3869
- async function setup14(ctx, editor) {
3870
- if (editor?.exampleId) {
3871
- setAssetPrefix(`/examples/${editor.exampleId}/`);
3872
- const exampleSetup = SETUP_BY_EXAMPLE_ID[editor.exampleId];
3873
- if (exampleSetup) {
3874
- return exampleSetup(ctx);
3875
- }
3876
- } else {
3877
- setAssetPrefix(editor?.assetPrefix ?? "/");
3878
- }
3879
- const loadOpts = {
3880
- scene: ctx.scene,
3881
- rapierWorld: ctx.rapierWorld,
3882
- rapier: ctx.rapier,
3883
- physics: ctx.physics,
3884
- audioListener: ctx.audio.listener,
3885
- componentRegistry,
3886
- componentManager: ctx.components,
3887
- particleRenderer: ctx.particles.batchedRenderer,
3888
- renderer: ctx.composer.getRenderer()
3889
- };
3890
- if (editor?.viewportCamera) {
3891
- ctx.camera.position.fromArray(editor.viewportCamera.position);
3892
- ctx.camera.quaternion.fromArray(editor.viewportCamera.quaternion);
3893
- } else {
3894
- ctx.camera.position.set(3, 3, 3);
3895
- ctx.camera.lookAt(0, 0.5, 0);
3896
- }
3897
- const sceneInstance = editor ? await loadSceneFromData2(editor.sceneData, loadOpts) : await loadScene13(DEFAULT_SCENE, loadOpts);
3898
- if (sceneInstance.environment?.postProcessing) {
3899
- applyScenePostProcessing3(
3900
- ctx.composer,
3901
- ctx.composer.getRenderer(),
3902
- ctx.scene,
3903
- ctx.camera,
3904
- sceneInstance.environment
3905
- );
3906
- }
3907
- ctx.systems.add("postPhysics", (dt) => sceneInstance.update(dt));
3908
- return {
3909
- dispose() {
3910
- sceneInstance.dispose();
3911
- }
3912
- };
3913
- }
3914
- export {
3915
- components,
3916
- setup14 as setup
3917
- };