@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.
- package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
- package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
- package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
- package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
- package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
- package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
- package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
- package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
- package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
- package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
- package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
- package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
- package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
- package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
- package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
- package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
- package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
- package/dist/assets/index-DiUacMXR.css +1 -0
- package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
- package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
- package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
- package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
- package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/packaged.mjs +27125 -1075
- package/package.json +2 -2
- package/server/asset-history-snapshots.ts +189 -0
- package/server/asset-library-routes.ts +404 -53
- package/server/catalog-import-recipe.ts +97 -0
- package/server/catalog-reimport.ts +141 -0
- package/server/cloud-asset-catalog.ts +71 -5
- package/server/dev.ts +1 -1
- package/server/editor-server.ts +106 -51
- package/server/editor-sse.ts +5 -0
- package/server/generative-execution-context.ts +45 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/packaged.ts +1 -1
- package/server/project-hmr-files.ts +3 -5
- package/server/project-output-writer.ts +440 -36
- package/server/project-tools.ts +529 -0
- package/server/server-utils.ts +14 -11
- package/src/EditorContext.tsx +4 -4
- package/src/action-registry.ts +12 -0
- package/src/asset-editor-context.tsx +9 -0
- package/src/asset-events.ts +3 -15
- package/src/asset-selection.ts +3 -0
- package/src/asset-workflow/asset-capabilities.ts +40 -0
- package/src/asset-workflow/asset-import-jobs.ts +108 -0
- package/src/asset-workflow/asset-library-query.ts +8 -1
- package/src/asset-workflow/asset-workflow-quality.ts +2 -0
- package/src/asset-workflow/import-contract.ts +2 -0
- package/src/asset-workflow/model-inspection.ts +92 -1
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +1 -0
- package/src/asset-workflow/project-asset-health.ts +3 -6
- package/src/asset-workflow/project-asset-operations.ts +119 -4
- package/src/command-listener.ts +37 -9
- package/src/components/ApplicationMenus.tsx +13 -15
- package/src/components/AssetBrowser.tsx +301 -26
- package/src/components/AssetImportDetails.tsx +207 -92
- package/src/components/AssetImportJobStatus.tsx +55 -0
- package/src/components/CenterDocuments.tsx +28 -1
- package/src/components/CommandPalette.tsx +2 -2
- package/src/components/DefaultEditorLayout.tsx +4 -0
- package/src/components/InspectorToolSection.tsx +12 -10
- package/src/components/OnlineAssetBrowser.tsx +197 -11
- package/src/components/ToolHost.tsx +8 -8
- package/src/components/ViewportOverlay.tsx +13 -26
- package/src/components/ViewportPanel.tsx +19 -14
- package/src/components/ViewportShadingMenu.tsx +103 -0
- package/src/components/WorkspaceDock.tsx +103 -17
- package/src/components/asset-documents.tsx +38 -11
- package/src/components/asset-editor-persistence.ts +300 -5
- package/src/components/asset-selection-section.tsx +83 -9
- package/src/components/asset-viewers/AudioViewer.tsx +4 -7
- package/src/components/asset-viewers/ModelViewer.tsx +168 -5
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
- package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
- package/src/components/asset-workflow.css +30 -3
- package/src/components/primitives/editor-icons.ts +1 -0
- package/src/components/project-tool-documents.tsx +298 -0
- package/src/components/status-contributions.tsx +38 -0
- package/src/components/tool-documents.tsx +20 -17
- package/src/components/tool-schema-form.tsx +258 -0
- package/src/components/workspace-dock.css +16 -0
- package/src/editor-api.ts +159 -22
- package/src/editor-store.ts +6 -5
- package/src/editor-viewport.ts +12 -7
- package/src/game-viewport-state.ts +28 -0
- package/src/model-thumbnail.ts +97 -9
- package/src/play-mode.ts +30 -3
- package/src/project-tool-discovery.ts +5 -5
- package/src/{project-operations.ts → project-tools.ts} +39 -26
- package/src/scene-sync.ts +0 -67
- package/src/storage/handle-storage.ts +5 -1
- package/src/storage/http-storage.ts +7 -2
- package/src/storage/mem-storage.ts +7 -1
- package/src/storage/types.ts +2 -0
- package/src/theme-css.ts +17 -7
- package/src/tool-loader.ts +152 -241
- package/src/viewport-shading-boundary.ts +15 -0
- package/src/widgets/index.ts +2 -1
- package/src/workspace-dock-controller.ts +8 -0
- package/src/workspace-document-registry.ts +4 -4
- package/src/workspace-state-persistence.ts +36 -10
- package/template/.claude/skills/editor/SKILL.md +13 -3
- package/template/AGENTS.md +100 -83
- package/template/package.json +16 -0
- package/template/public/scenes/default.vscn.json +1 -2
- package/template/scripts/playtest.ts +7 -37
- package/template/server/rooms/game-room.ts +2 -2
- package/template/src/data/README.md +3 -2
- package/template/src/data/tuning.data.json +1 -3
- package/template/src/data/tuning.schema.json +0 -12
- package/template/src/data/tuning.schema.ts +2 -9
- package/template/src/data/tuning.ts +1 -1
- package/template/src/scripts/registry.ts +0 -3
- package/template/src/tools/README.md +151 -16
- package/template/src/tools/autoplay.tool.ts +31 -0
- package/template/src/tools/autoplay.ts +15 -0
- package/template/src/tools/tuning.document.tsx +107 -0
- package/template/src/tools/tuning.tool.ts +24 -0
- package/template/src/tools/tuning.ts +9 -0
- package/template/src/ui/Button.stories.tsx +1 -1
- package/template/tests/acceptance/example.spec.ts +4 -59
- package/template/vgai.game.json +1 -1
- package/dist/assets/index-bAGhEnnO.css +0 -1
- package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
- package/dist/project-scripts.bundle.js +0 -3917
- package/server/project-operations.ts +0 -343
- package/src/components/project-operation-documents.tsx +0 -226
- package/template/src/operations/README.md +0 -88
- package/template/src/scripts/components/data-spinner.ts +0 -86
- package/template/src/scripts/components/spin-lap-math.ts +0 -27
- package/template/src/tools/example.tool.tsx +0 -97
- package/template/src/tools/spin.tool.tsx +0 -101
- 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
|
-
};
|