@jts-studios/web-game-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/FALLBACK_STRATEGY.md +21 -0
- package/PLUGIN_API.md +34 -0
- package/README.md +49 -0
- package/package.json +37 -0
- package/src/adapters/runtime/GameRuntime.js +446 -0
- package/src/app/GameApp.js +6 -0
- package/src/app/camera/CameraController.js +1 -0
- package/src/app/input/InputController.js +1 -0
- package/src/app/input/InputProfileCatalog.js +1 -0
- package/src/app/loop/UpdateLoop.js +1 -0
- package/src/app/state/SceneManager.js +1 -0
- package/src/app/state/SceneSnapshotStore.js +1 -0
- package/src/core/camera/CameraController.js +119 -0
- package/src/core/input/InputController.js +207 -0
- package/src/core/input/InputProfileCatalog.js +29 -0
- package/src/core/loop/FixedStepClock.js +30 -0
- package/src/core/loop/InputReplayTimeline.js +155 -0
- package/src/core/loop/InputReplayTimeline.test.js +43 -0
- package/src/core/loop/UpdateLoop.js +28 -0
- package/src/core/runtime/RuntimeBridge.js +54 -0
- package/src/core/runtime/RuntimeBridge.test.js +25 -0
- package/src/core/state/AssetManifestLoader.js +83 -0
- package/src/core/state/AssetManifestLoader.test.js +59 -0
- package/src/core/state/SceneManager.js +60 -0
- package/src/core/state/SceneSnapshotStore.js +98 -0
- package/src/index.d.ts +27 -0
- package/src/index.js +11 -0
- package/src/main.js +213 -0
- package/src/scenes/createBootstrapScene.js +17 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-03-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- First public package release for `@jts-studios/web-game-engine`.
|
|
10
|
+
- Runtime loop foundation with `FixedStepClock` and `InputReplayTimeline`.
|
|
11
|
+
- Plugin lifecycle support in `GameRuntime` with capability/dependency checks.
|
|
12
|
+
- Scene snapshot utilities and asset preloading with `SceneSnapshotStore` and `AssetManifestLoader`.
|
|
13
|
+
- Export verification and package verify script for release quality gates.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# WebGPU Fallback Strategy
|
|
2
|
+
|
|
3
|
+
`@jts-studios/web-game-engine` targets `@jts-studios/engine` WebGPU rendering.
|
|
4
|
+
|
|
5
|
+
## Runtime Policy
|
|
6
|
+
|
|
7
|
+
- Default policy: `warn`.
|
|
8
|
+
- `strict`: throw when WebGPU is unavailable.
|
|
9
|
+
- `warn`: enter degraded mode and emit diagnostics.
|
|
10
|
+
- `silent`: enter degraded mode without warnings.
|
|
11
|
+
|
|
12
|
+
## Degraded Mode
|
|
13
|
+
|
|
14
|
+
When WebGPU is unavailable, gameplay systems continue updating while rendering is skipped.
|
|
15
|
+
Use `runtime.getFallbackStatus()` to detect this mode.
|
|
16
|
+
|
|
17
|
+
## Recommended App Behavior
|
|
18
|
+
|
|
19
|
+
- Show a clear UI message in degraded mode.
|
|
20
|
+
- Offer browser/device upgrade guidance.
|
|
21
|
+
- Optionally expose a non-WebGPU renderer in the host app.
|
package/PLUGIN_API.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Plugin API
|
|
2
|
+
|
|
3
|
+
`GameRuntime.use(plugin)` accepts plugin objects that implement optional lifecycle hooks.
|
|
4
|
+
|
|
5
|
+
## Required Shape
|
|
6
|
+
|
|
7
|
+
- `name: string` recommended for diagnostics and dependency wiring.
|
|
8
|
+
- `apiVersion?: number` defaults to runtime API version.
|
|
9
|
+
- `capabilities?: string[]` any of: `render-hooks`, `update-hooks`, `scene-state`, `input-profile`.
|
|
10
|
+
- `dependencies?: string[]` plugin names that must be registered first.
|
|
11
|
+
- `priority?: number` lower values run first.
|
|
12
|
+
- `enabled?: boolean` defaults to `true`.
|
|
13
|
+
|
|
14
|
+
## Lifecycle Hooks
|
|
15
|
+
|
|
16
|
+
- `onInit(runtime)` called once after runtime init.
|
|
17
|
+
- `onUpdate(runtime, deltaTime)` called per fixed-step update.
|
|
18
|
+
- `onRender(runtime, alpha)` called after render.
|
|
19
|
+
- `onDestroy(runtime)` called during teardown.
|
|
20
|
+
|
|
21
|
+
## Error Handling
|
|
22
|
+
|
|
23
|
+
Hook exceptions are isolated and routed through `runtime.onPluginError(handler)`.
|
|
24
|
+
Handlers receive:
|
|
25
|
+
|
|
26
|
+
- `plugin`: plugin name
|
|
27
|
+
- `hookName`: lifecycle hook identifier
|
|
28
|
+
- `error`: original exception
|
|
29
|
+
|
|
30
|
+
## Compatibility Guidance
|
|
31
|
+
|
|
32
|
+
- Keep plugin behavior deterministic for replay-heavy projects.
|
|
33
|
+
- Declare `dependencies` and `capabilities` explicitly.
|
|
34
|
+
- Avoid mutating runtime internals not exposed by public APIs.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# web-game-engine
|
|
2
|
+
|
|
3
|
+
This package is a core game-framework layer built on top of `@jts-studios/engine`.
|
|
4
|
+
|
|
5
|
+
`@jts-studios/engine` remains the rendering engine and GPU/rendering foundation.
|
|
6
|
+
|
|
7
|
+
`@jts-studios/web-game-engine` provides reusable gameplay/runtime systems that compose that renderer.
|
|
8
|
+
|
|
9
|
+
## Structure
|
|
10
|
+
|
|
11
|
+
- `src/core/*`: reusable library systems (input, camera, state, loop).
|
|
12
|
+
- `src/adapters/*`: integration glue to concrete runtimes (for example, browser runtime adapter).
|
|
13
|
+
- `src/index.js`: public library entrypoint.
|
|
14
|
+
- `src/app/*`: compatibility wrappers for older import paths.
|
|
15
|
+
|
|
16
|
+
## Included Game Systems
|
|
17
|
+
|
|
18
|
+
- Follow-camera controller with smoothing and dead-zone behavior.
|
|
19
|
+
- Optional world-bounds clamping for camera movement.
|
|
20
|
+
- Scene/state loop scaffolding intended for game-level orchestration.
|
|
21
|
+
- Snapshot store and input profile catalog for runtime workflows.
|
|
22
|
+
|
|
23
|
+
## Public API
|
|
24
|
+
|
|
25
|
+
- `GameRuntime`
|
|
26
|
+
- `CameraController`
|
|
27
|
+
- `InputController`
|
|
28
|
+
- `InputProfileCatalog`
|
|
29
|
+
- `UpdateLoop`
|
|
30
|
+
- `FixedStepClock`
|
|
31
|
+
- `InputReplayTimeline`
|
|
32
|
+
- `RuntimeBridge`
|
|
33
|
+
- `SceneManager`
|
|
34
|
+
- `SceneSnapshotStore`
|
|
35
|
+
- `AssetManifestLoader`
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
- `npm install`
|
|
40
|
+
- `npm run dev`
|
|
41
|
+
- `npm run build`
|
|
42
|
+
- `npm run verify`
|
|
43
|
+
- `npm run release:verify`
|
|
44
|
+
|
|
45
|
+
## Separation Rule
|
|
46
|
+
|
|
47
|
+
- Put rendering primitives and GPU systems in `web-engine`.
|
|
48
|
+
- Put reusable gameplay/entity/physics/combat/quest systems in `web-game-engine`.
|
|
49
|
+
- Put domain-specific implementations (for example, chess rules and chess assets) in their own sibling project folders, such as `chess`.
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jts-studios/web-game-engine",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"PLUGIN_API.md",
|
|
17
|
+
"FALLBACK_STRATEGY.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "vite",
|
|
24
|
+
"build": "vite build",
|
|
25
|
+
"preview": "vite preview",
|
|
26
|
+
"test": "node --test \"src/**/*.test.js\"",
|
|
27
|
+
"check:exports": "node scripts/check-exports.mjs",
|
|
28
|
+
"verify": "npm run check:exports && npm run test && npm run build",
|
|
29
|
+
"release:verify": "npm run verify && npm pack --dry-run"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@jts-studios/engine": "^1.5.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"vite": "^7.3.1"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { Camera, ColorRGBA, Scene, WebGPURenderer2D } from "@jts-studios/engine"
|
|
2
|
+
import { InputController } from "../../core/input/InputController.js"
|
|
3
|
+
import { InputProfileCatalog } from "../../core/input/InputProfileCatalog.js"
|
|
4
|
+
import { CameraController } from "../../core/camera/CameraController.js"
|
|
5
|
+
import { UpdateLoop } from "../../core/loop/UpdateLoop.js"
|
|
6
|
+
import { FixedStepClock } from "../../core/loop/FixedStepClock.js"
|
|
7
|
+
import { InputReplayTimeline } from "../../core/loop/InputReplayTimeline.js"
|
|
8
|
+
import { SceneManager } from "../../core/state/SceneManager.js"
|
|
9
|
+
import { SceneSnapshotStore } from "../../core/state/SceneSnapshotStore.js"
|
|
10
|
+
import { AssetManifestLoader } from "../../core/state/AssetManifestLoader.js"
|
|
11
|
+
import { RuntimeBridge } from "../../core/runtime/RuntimeBridge.js"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runtime adapter that wires web-engine rendering with web-game-engine core systems.
|
|
15
|
+
*/
|
|
16
|
+
export class GameRuntime {
|
|
17
|
+
constructor({ canvas, fixedDelta = 1 / 60, maxSubSteps = 8, pluginApiVersion = 1, fallbackPolicy = "warn" } = {}) {
|
|
18
|
+
this.canvas = canvas
|
|
19
|
+
this.renderer = new WebGPURenderer2D({
|
|
20
|
+
canvas,
|
|
21
|
+
clearColor: new ColorRGBA(0.04, 0.06, 0.1, 1),
|
|
22
|
+
})
|
|
23
|
+
this.camera = new Camera({
|
|
24
|
+
position: { x: 0, y: 0 },
|
|
25
|
+
zoom: 1,
|
|
26
|
+
})
|
|
27
|
+
this.scene = new Scene({
|
|
28
|
+
backgroundColor: new ColorRGBA(0.04, 0.06, 0.1, 1),
|
|
29
|
+
})
|
|
30
|
+
this.sceneManager = new SceneManager()
|
|
31
|
+
this.sceneManager.setScene(this.scene)
|
|
32
|
+
|
|
33
|
+
this.input = new InputController({ element: canvas })
|
|
34
|
+
this.inputProfiles = new InputProfileCatalog()
|
|
35
|
+
this.cameraController = new CameraController(this.camera, this.input)
|
|
36
|
+
this.sceneSnapshots = new SceneSnapshotStore()
|
|
37
|
+
this.assetManifestLoader = new AssetManifestLoader()
|
|
38
|
+
this.updateLoop = new UpdateLoop()
|
|
39
|
+
this.updateLoop.add(this.cameraController)
|
|
40
|
+
this.clock = new FixedStepClock({ fixedDelta, maxSubSteps })
|
|
41
|
+
this.plugins = []
|
|
42
|
+
this.pluginApiVersion = Number.isFinite(pluginApiVersion) ? Math.max(1, Math.floor(pluginApiVersion)) : 1
|
|
43
|
+
this._pluginErrorHandlers = []
|
|
44
|
+
this._activeInputProfileName = ""
|
|
45
|
+
this.replay = new InputReplayTimeline()
|
|
46
|
+
this.bridge = new RuntimeBridge()
|
|
47
|
+
this.fallbackPolicy = fallbackPolicy === "strict" || fallbackPolicy === "silent" ? fallbackPolicy : "warn"
|
|
48
|
+
this._fallbackStatus = {
|
|
49
|
+
active: false,
|
|
50
|
+
reason: "",
|
|
51
|
+
policy: this.fallbackPolicy,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this._frameHandle = 0
|
|
55
|
+
this._running = false
|
|
56
|
+
this._frameInProgress = false
|
|
57
|
+
this._lastFrameTime = 0
|
|
58
|
+
this._onResize = this.#resize.bind(this)
|
|
59
|
+
this._onFrame = this.#frame.bind(this)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async init() {
|
|
63
|
+
try {
|
|
64
|
+
await this.renderer.init()
|
|
65
|
+
const capabilities = this.renderer.getCapabilities?.() ?? null
|
|
66
|
+
const hasWebGpu = capabilities?.supportsWebGPU !== false
|
|
67
|
+
if (!hasWebGpu) {
|
|
68
|
+
this.#activateFallback("webgpu-unavailable")
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.#activateFallback("renderer-init-failed")
|
|
72
|
+
if (this.fallbackPolicy === "strict") {
|
|
73
|
+
throw error
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.input.attach()
|
|
78
|
+
window.addEventListener("resize", this._onResize)
|
|
79
|
+
this.#resize()
|
|
80
|
+
|
|
81
|
+
await this.#invokePluginHookForAll("onInit", this)
|
|
82
|
+
this.bridge.emit("runtime:init", {
|
|
83
|
+
fallback: this.getFallbackStatus(),
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setScene(scene) {
|
|
88
|
+
this.scene = scene
|
|
89
|
+
this.sceneManager.setScene(scene)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
registerState(name, state) {
|
|
93
|
+
this.sceneManager.registerState(name, state)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setState(name, context = null) {
|
|
97
|
+
this.sceneManager.setState(name, context)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
defineInputProfile(name, profile) {
|
|
101
|
+
this.inputProfiles.define(name, profile)
|
|
102
|
+
if (!this._activeInputProfileName) {
|
|
103
|
+
this.applyInputProfile(name)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getInputProfile(name) {
|
|
108
|
+
return this.inputProfiles.get(name)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
listInputProfiles() {
|
|
112
|
+
return this.inputProfiles.list()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
applyInputProfile(name) {
|
|
116
|
+
const profile = this.getInputProfile(name)
|
|
117
|
+
if (!profile) {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this._activeInputProfileName = name
|
|
122
|
+
this.input.applyProfile(profile)
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getActiveInputProfileName() {
|
|
127
|
+
return this._activeInputProfileName ?? null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
saveSceneSnapshot(name, scene = this.sceneManager.getActiveScene() ?? this.scene) {
|
|
131
|
+
this.sceneSnapshots.save(name, scene)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
loadSceneSnapshot(name, { activate = true } = {}) {
|
|
135
|
+
const restored = this.sceneSnapshots.load(name)
|
|
136
|
+
if (!restored) {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (activate) {
|
|
141
|
+
this.setScene(restored)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return restored
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
listSceneSnapshots() {
|
|
148
|
+
return this.sceneSnapshots.list()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
setCameraFollowTarget(target) {
|
|
152
|
+
this.cameraController.setFollowTarget(target)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setCameraBounds(bounds) {
|
|
156
|
+
this.cameraController.setBounds(bounds)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setCameraDeadZone(width, height) {
|
|
160
|
+
this.cameraController.setDeadZone(width, height)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
use(plugin) {
|
|
164
|
+
if (!plugin || typeof plugin !== "object") {
|
|
165
|
+
throw new TypeError("GameRuntime.use requires a plugin object")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const pluginVersion = Number.isFinite(plugin.apiVersion) ? Math.floor(plugin.apiVersion) : this.pluginApiVersion
|
|
169
|
+
if (pluginVersion > this.pluginApiVersion) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`GameRuntime.use rejected plugin '${plugin.name ?? "anonymous"}' (apiVersion ${pluginVersion} > ${this.pluginApiVersion})`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const capabilities = Array.isArray(plugin.capabilities) ? plugin.capabilities : []
|
|
176
|
+
const unsupported = capabilities.filter(
|
|
177
|
+
(capability) =>
|
|
178
|
+
capability !== "render-hooks" &&
|
|
179
|
+
capability !== "update-hooks" &&
|
|
180
|
+
capability !== "scene-state" &&
|
|
181
|
+
capability !== "input-profile",
|
|
182
|
+
)
|
|
183
|
+
if (unsupported.length > 0) {
|
|
184
|
+
throw new Error(`GameRuntime.use rejected plugin with unsupported capabilities: ${unsupported.join(", ")}`)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const pluginName =
|
|
188
|
+
typeof plugin.name === "string" && plugin.name.length > 0 ? plugin.name : `plugin-${this.plugins.length + 1}`
|
|
189
|
+
if (this.plugins.some((entry) => entry?.__pluginName === pluginName)) {
|
|
190
|
+
throw new Error(`GameRuntime.use rejected duplicate plugin name '${pluginName}'`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const dependencies = Array.isArray(plugin.dependencies) ? plugin.dependencies : []
|
|
194
|
+
for (let i = 0; i < dependencies.length; i++) {
|
|
195
|
+
if (!this.plugins.some((entry) => entry?.__pluginName === dependencies[i])) {
|
|
196
|
+
throw new Error(`GameRuntime.use missing dependency '${dependencies[i]}' for plugin '${pluginName}'`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const priority = Number.isFinite(plugin.priority) ? Math.floor(plugin.priority) : 0
|
|
201
|
+
const wrappedPlugin = {
|
|
202
|
+
...plugin,
|
|
203
|
+
__pluginName: pluginName,
|
|
204
|
+
__enabled: plugin.enabled !== false,
|
|
205
|
+
__priority: priority,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.plugins.push(wrappedPlugin)
|
|
209
|
+
this.plugins.sort((a, b) => (a.__priority ?? 0) - (b.__priority ?? 0))
|
|
210
|
+
return this
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
listPlugins() {
|
|
214
|
+
return this.plugins.map((plugin) => ({
|
|
215
|
+
name: plugin.__pluginName,
|
|
216
|
+
enabled: plugin.__enabled,
|
|
217
|
+
priority: plugin.__priority,
|
|
218
|
+
apiVersion: plugin.apiVersion ?? this.pluginApiVersion,
|
|
219
|
+
}))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
setPluginEnabled(name, enabled = true) {
|
|
223
|
+
const plugin = this.plugins.find((entry) => entry.__pluginName === name)
|
|
224
|
+
if (!plugin) {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
plugin.__enabled = !!enabled
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
onPluginError(handler) {
|
|
232
|
+
if (typeof handler === "function") {
|
|
233
|
+
this._pluginErrorHandlers.push(handler)
|
|
234
|
+
}
|
|
235
|
+
return this
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
startReplayRecording() {
|
|
239
|
+
this.replay.startRecording()
|
|
240
|
+
return this
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
stopReplayRecording() {
|
|
244
|
+
return this.replay.stopRecording()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
startReplayPlayback(events) {
|
|
248
|
+
this.replay.startPlayback(events)
|
|
249
|
+
return this
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
stopReplayPlayback() {
|
|
253
|
+
this.replay.stopPlayback()
|
|
254
|
+
return this
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
getReplayDiagnostics(events = null) {
|
|
258
|
+
return this.replay.validate(events ?? this.replay.toJSON())
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
getFallbackStatus() {
|
|
262
|
+
return {
|
|
263
|
+
...this._fallbackStatus,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
onBridgeEvent(listener) {
|
|
268
|
+
return this.bridge.onEvent(listener)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
registerBridgeCommand(name, handler) {
|
|
272
|
+
this.bridge.registerCommand(name, handler)
|
|
273
|
+
return this
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
executeBridgeCommand(name, payload = null) {
|
|
277
|
+
return this.bridge.execute(name, payload, this)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async preloadSnapshotAssets(name, { resolveTextureHandle = null, onProgress = null, signal = null } = {}) {
|
|
281
|
+
const plan = this.sceneSnapshots.createPreloadPlan(name, { resolveTextureHandle })
|
|
282
|
+
if (plan.urls.length === 0) {
|
|
283
|
+
return {
|
|
284
|
+
status: "ready",
|
|
285
|
+
loaded: 0,
|
|
286
|
+
failed: [],
|
|
287
|
+
unresolved: plan.unresolved,
|
|
288
|
+
total: plan.total,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const result = await this.assetManifestLoader.preloadUrls(plan.urls, {
|
|
293
|
+
onProgress,
|
|
294
|
+
signal,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
...result,
|
|
299
|
+
unresolved: plan.unresolved,
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
start() {
|
|
304
|
+
if (this._running) {
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this._running = true
|
|
309
|
+
this._frameInProgress = false
|
|
310
|
+
this._lastFrameTime = 0
|
|
311
|
+
this.clock.reset()
|
|
312
|
+
this._frameHandle = requestAnimationFrame(this._onFrame)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
stop() {
|
|
316
|
+
if (!this._running) {
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this._running = false
|
|
321
|
+
cancelAnimationFrame(this._frameHandle)
|
|
322
|
+
this._frameHandle = 0
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
destroy() {
|
|
326
|
+
this.stop()
|
|
327
|
+
this.input.detach()
|
|
328
|
+
this.updateLoop.clear()
|
|
329
|
+
void this.#invokePluginHookForAll("onDestroy", this)
|
|
330
|
+
window.removeEventListener("resize", this._onResize)
|
|
331
|
+
this.renderer.destroy()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#resize() {
|
|
335
|
+
this.renderer.setSize(window.innerWidth, window.innerHeight, window.devicePixelRatio || 1)
|
|
336
|
+
this.camera.setViewport(this.canvas.width, this.canvas.height)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async #frame(time) {
|
|
340
|
+
if (!this._running) {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (this._frameInProgress) {
|
|
345
|
+
this._frameHandle = requestAnimationFrame(this._onFrame)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
this._frameInProgress = true
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const deltaTime = this._lastFrameTime > 0 ? Math.max(0.0001, (time - this._lastFrameTime) * 0.001) : 1 / 60
|
|
353
|
+
this._lastFrameTime = time
|
|
354
|
+
|
|
355
|
+
const stepped = this.clock.consume(deltaTime)
|
|
356
|
+
for (let i = 0; i < stepped.steps.length; i++) {
|
|
357
|
+
const stepDelta = stepped.steps[i]
|
|
358
|
+
|
|
359
|
+
const replayEvents = this.replay.consume(stepDelta)
|
|
360
|
+
if (replayEvents.length > 0) {
|
|
361
|
+
const latest = replayEvents[replayEvents.length - 1]
|
|
362
|
+
if (latest?.snapshot) {
|
|
363
|
+
this.input.applySnapshot(latest.snapshot)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.replay.record(stepDelta, this.input.captureSnapshot())
|
|
368
|
+
|
|
369
|
+
this.updateLoop.update(stepDelta)
|
|
370
|
+
this.sceneManager.update(stepDelta)
|
|
371
|
+
this.bridge.emit("runtime:update", {
|
|
372
|
+
deltaTime: stepDelta,
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
await this.#invokePluginHookForAll("onUpdate", this, stepDelta)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const activeScene = this.sceneManager.getActiveScene() ?? this.scene
|
|
379
|
+
if (!this._fallbackStatus.active) {
|
|
380
|
+
await this.renderer.render(activeScene, this.camera)
|
|
381
|
+
}
|
|
382
|
+
await this.#invokePluginHookForAll("onRender", this, stepped.alpha)
|
|
383
|
+
this.bridge.emit("runtime:render", {
|
|
384
|
+
alpha: stepped.alpha,
|
|
385
|
+
fallbackActive: this._fallbackStatus.active,
|
|
386
|
+
})
|
|
387
|
+
} finally {
|
|
388
|
+
this._frameInProgress = false
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!this._running) {
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this._frameHandle = requestAnimationFrame(this._onFrame)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async #invokePluginHook(plugin, hookName, ...args) {
|
|
399
|
+
if (!plugin || typeof plugin !== "object") {
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (plugin.__enabled === false) {
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const hook = plugin[hookName]
|
|
408
|
+
if (typeof hook !== "function") {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
await hook(...args)
|
|
414
|
+
} catch (error) {
|
|
415
|
+
for (let i = 0; i < this._pluginErrorHandlers.length; i++) {
|
|
416
|
+
try {
|
|
417
|
+
this._pluginErrorHandlers[i]({
|
|
418
|
+
plugin: plugin.__pluginName ?? plugin.name ?? "anonymous",
|
|
419
|
+
hookName,
|
|
420
|
+
error,
|
|
421
|
+
})
|
|
422
|
+
} catch {
|
|
423
|
+
// Avoid cascading plugin error handler failures.
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async #invokePluginHookForAll(hookName, ...args) {
|
|
430
|
+
for (let i = 0; i < this.plugins.length; i++) {
|
|
431
|
+
await this.#invokePluginHook(this.plugins[i], hookName, ...args)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
#activateFallback(reason) {
|
|
436
|
+
this._fallbackStatus = {
|
|
437
|
+
active: true,
|
|
438
|
+
reason,
|
|
439
|
+
policy: this.fallbackPolicy,
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (this.fallbackPolicy === "warn") {
|
|
443
|
+
console.warn(`[GameRuntime] Falling back to no-render mode: ${reason}`)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CameraController } from "../../core/camera/CameraController.js"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InputController } from "../../core/input/InputController.js"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InputProfileCatalog } from "../../core/input/InputProfileCatalog.js"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UpdateLoop } from "../../core/loop/UpdateLoop.js"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SceneManager } from "../../core/state/SceneManager.js"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SceneSnapshotStore } from "../../core/state/SceneSnapshotStore.js"
|