@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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves scene serializer asset manifests and optionally preloads URL assets.
|
|
3
|
+
*/
|
|
4
|
+
export class AssetManifestLoader {
|
|
5
|
+
resolve(manifest, { resolveTextureHandle = null } = {}) {
|
|
6
|
+
const textures = Array.isArray(manifest?.textures) ? manifest.textures : []
|
|
7
|
+
const resolved = []
|
|
8
|
+
const unresolved = []
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < textures.length; i++) {
|
|
11
|
+
const handle = textures[i]
|
|
12
|
+
if (!handle || typeof handle !== "object") {
|
|
13
|
+
continue
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let url = null
|
|
17
|
+
if (handle.kind === "url" && typeof handle.value === "string") {
|
|
18
|
+
url = handle.value
|
|
19
|
+
} else if (typeof resolveTextureHandle === "function") {
|
|
20
|
+
url = resolveTextureHandle(handle) ?? null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof url === "string" && url.length > 0) {
|
|
24
|
+
resolved.push({ handle, url })
|
|
25
|
+
} else {
|
|
26
|
+
unresolved.push(handle)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
resolved,
|
|
32
|
+
unresolved,
|
|
33
|
+
total: textures.length,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async preloadUrls(urls, { onProgress = null, signal = null } = {}) {
|
|
38
|
+
const queue = Array.isArray(urls) ? urls.filter((url) => typeof url === "string" && url.length > 0) : []
|
|
39
|
+
let loaded = 0
|
|
40
|
+
const failed = []
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < queue.length; i++) {
|
|
43
|
+
if (signal?.aborted) {
|
|
44
|
+
return {
|
|
45
|
+
status: "aborted",
|
|
46
|
+
loaded,
|
|
47
|
+
failed,
|
|
48
|
+
total: queue.length,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const url = queue[i]
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(url, {
|
|
55
|
+
method: "GET",
|
|
56
|
+
cache: "force-cache",
|
|
57
|
+
signal: signal ?? undefined,
|
|
58
|
+
})
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
failed.push({ url, status: response.status })
|
|
61
|
+
} else {
|
|
62
|
+
loaded += 1
|
|
63
|
+
}
|
|
64
|
+
} catch (_error) {
|
|
65
|
+
failed.push({ url, status: 0 })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
onProgress?.({
|
|
69
|
+
loaded,
|
|
70
|
+
total: queue.length,
|
|
71
|
+
failed: failed.length,
|
|
72
|
+
current: url,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
status: "ready",
|
|
78
|
+
loaded,
|
|
79
|
+
failed,
|
|
80
|
+
total: queue.length,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import test from "node:test"
|
|
2
|
+
import assert from "node:assert/strict"
|
|
3
|
+
|
|
4
|
+
import { AssetManifestLoader } from "./AssetManifestLoader.js"
|
|
5
|
+
|
|
6
|
+
test("AssetManifestLoader resolves urls and unresolved handles", () => {
|
|
7
|
+
const loader = new AssetManifestLoader()
|
|
8
|
+
const result = loader.resolve(
|
|
9
|
+
{
|
|
10
|
+
textures: [
|
|
11
|
+
{ kind: "url", value: "a.png" },
|
|
12
|
+
{ kind: "asset", id: "hero" },
|
|
13
|
+
{ kind: "asset", id: "missing" },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
resolveTextureHandle(handle) {
|
|
18
|
+
if (handle?.id === "hero") {
|
|
19
|
+
return "hero.png"
|
|
20
|
+
}
|
|
21
|
+
return null
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert.equal(result.total, 3)
|
|
27
|
+
assert.equal(result.resolved.length, 2)
|
|
28
|
+
assert.equal(result.unresolved.length, 1)
|
|
29
|
+
assert.equal(result.resolved[0].url, "a.png")
|
|
30
|
+
assert.equal(result.resolved[1].url, "hero.png")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("AssetManifestLoader preloadUrls reports failures and progress", async () => {
|
|
34
|
+
const loader = new AssetManifestLoader()
|
|
35
|
+
const progress = []
|
|
36
|
+
|
|
37
|
+
const originalFetch = globalThis.fetch
|
|
38
|
+
globalThis.fetch = async (url) => {
|
|
39
|
+
if (url === "ok.png") {
|
|
40
|
+
return { ok: true, status: 200 }
|
|
41
|
+
}
|
|
42
|
+
return { ok: false, status: 404 }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const result = await loader.preloadUrls(["ok.png", "bad.png"], {
|
|
47
|
+
onProgress(entry) {
|
|
48
|
+
progress.push(entry)
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
assert.equal(result.status, "ready")
|
|
53
|
+
assert.equal(result.loaded, 1)
|
|
54
|
+
assert.equal(result.failed.length, 1)
|
|
55
|
+
assert.equal(progress.length, 2)
|
|
56
|
+
} finally {
|
|
57
|
+
globalThis.fetch = originalFetch
|
|
58
|
+
}
|
|
59
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export class SceneManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._states = new Map()
|
|
4
|
+
this._activeStateName = ""
|
|
5
|
+
this._activeState = null
|
|
6
|
+
this._inlineScene = null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
registerState(name, state) {
|
|
10
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
11
|
+
throw new TypeError("SceneManager.registerState requires a non-empty state name")
|
|
12
|
+
}
|
|
13
|
+
if (!state || typeof state !== "object") {
|
|
14
|
+
throw new TypeError("SceneManager.registerState requires a state object")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this._states.set(name, {
|
|
18
|
+
scene: state.scene ?? null,
|
|
19
|
+
onEnter: typeof state.onEnter === "function" ? state.onEnter : null,
|
|
20
|
+
onExit: typeof state.onExit === "function" ? state.onExit : null,
|
|
21
|
+
onUpdate: typeof state.onUpdate === "function" ? state.onUpdate : null,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setState(name, context = null) {
|
|
26
|
+
if (this._activeState?.onExit) {
|
|
27
|
+
this._activeState.onExit(context)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const state = this._states.get(name)
|
|
31
|
+
if (!state) {
|
|
32
|
+
throw new Error(`Scene state not found: ${name}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this._activeStateName = name
|
|
36
|
+
this._activeState = state
|
|
37
|
+
|
|
38
|
+
if (state.onEnter) {
|
|
39
|
+
state.onEnter(context)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setScene(scene) {
|
|
44
|
+
this._inlineScene = scene
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
update(deltaTime) {
|
|
48
|
+
if (this._activeState?.onUpdate) {
|
|
49
|
+
this._activeState.onUpdate(deltaTime)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getActiveScene() {
|
|
54
|
+
return this._activeState?.scene ?? this._inlineScene
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getActiveStateName() {
|
|
58
|
+
return this._activeStateName
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { SceneSerializer } from "@jts-studios/engine"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gameplay-layer scene snapshot store backed by renderer-scene serialization.
|
|
5
|
+
*/
|
|
6
|
+
export class SceneSnapshotStore {
|
|
7
|
+
constructor() {
|
|
8
|
+
this._snapshots = new Map()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
save(name, scene) {
|
|
12
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
13
|
+
throw new TypeError("SceneSnapshotStore.save requires a non-empty snapshot name")
|
|
14
|
+
}
|
|
15
|
+
const snapshot = SceneSerializer.serialize(scene)
|
|
16
|
+
const validation = SceneSerializer.validate(snapshot)
|
|
17
|
+
if (!validation.valid) {
|
|
18
|
+
throw new Error(`SceneSnapshotStore.save rejected invalid snapshot: ${validation.errors.join("; ")}`)
|
|
19
|
+
}
|
|
20
|
+
this._snapshots.set(name, snapshot)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
load(name) {
|
|
24
|
+
const snapshot = this._snapshots.get(name)
|
|
25
|
+
if (!snapshot) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
return SceneSerializer.deserialize(snapshot)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
has(name) {
|
|
32
|
+
return this._snapshots.has(name)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
validate(name, options = {}) {
|
|
36
|
+
const snapshot = this._snapshots.get(name)
|
|
37
|
+
if (!snapshot) {
|
|
38
|
+
return {
|
|
39
|
+
valid: false,
|
|
40
|
+
strict: !!options.strict,
|
|
41
|
+
errors: ["snapshot not found"],
|
|
42
|
+
warnings: [],
|
|
43
|
+
objectCount: 0,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return SceneSerializer.validate(snapshot, options)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getAssetManifest(name) {
|
|
50
|
+
const snapshot = this._snapshots.get(name)
|
|
51
|
+
if (!snapshot) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
return SceneSerializer.extractAssetManifest(snapshot)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
createPreloadPlan(name, { resolveTextureHandle = null } = {}) {
|
|
58
|
+
const manifest = this.getAssetManifest(name)
|
|
59
|
+
if (!manifest) {
|
|
60
|
+
return {
|
|
61
|
+
urls: [],
|
|
62
|
+
unresolved: [],
|
|
63
|
+
total: 0,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const urls = []
|
|
68
|
+
const unresolved = []
|
|
69
|
+
const textures = Array.isArray(manifest.textures) ? manifest.textures : []
|
|
70
|
+
for (let i = 0; i < textures.length; i++) {
|
|
71
|
+
const handle = textures[i]
|
|
72
|
+
if (handle?.kind === "url" && typeof handle.value === "string" && handle.value.length > 0) {
|
|
73
|
+
urls.push(handle.value)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof resolveTextureHandle === "function") {
|
|
78
|
+
const resolved = resolveTextureHandle(handle)
|
|
79
|
+
if (typeof resolved === "string" && resolved.length > 0) {
|
|
80
|
+
urls.push(resolved)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
unresolved.push(handle)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
urls,
|
|
90
|
+
unresolved,
|
|
91
|
+
total: textures.length,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
list() {
|
|
96
|
+
return Array.from(this._snapshots.keys())
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type PluginCapability = "render-hooks" | "update-hooks" | "scene-state" | "input-profile"
|
|
2
|
+
|
|
3
|
+
export interface GameRuntimePlugin {
|
|
4
|
+
name?: string
|
|
5
|
+
apiVersion?: number
|
|
6
|
+
capabilities?: PluginCapability[]
|
|
7
|
+
dependencies?: string[]
|
|
8
|
+
priority?: number
|
|
9
|
+
enabled?: boolean
|
|
10
|
+
onInit?(runtime: unknown): void | Promise<void>
|
|
11
|
+
onUpdate?(runtime: unknown, deltaTime: number): void | Promise<void>
|
|
12
|
+
onRender?(runtime: unknown, alpha: number): void | Promise<void>
|
|
13
|
+
onDestroy?(runtime: unknown): void | Promise<void>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RuntimeBridgeEvent {
|
|
17
|
+
type: string
|
|
18
|
+
payload?: unknown
|
|
19
|
+
time: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare class RuntimeBridge {
|
|
23
|
+
onEvent(listener: (event: RuntimeBridgeEvent) => void): () => void
|
|
24
|
+
emit(type: string, payload?: unknown): void
|
|
25
|
+
registerCommand(commandName: string, handler: (payload: unknown, context: unknown) => unknown): this
|
|
26
|
+
execute(commandName: string, payload?: unknown, context?: unknown): Promise<unknown>
|
|
27
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { CameraController } from "./core/camera/CameraController.js"
|
|
2
|
+
export { InputController } from "./core/input/InputController.js"
|
|
3
|
+
export { InputProfileCatalog } from "./core/input/InputProfileCatalog.js"
|
|
4
|
+
export { UpdateLoop } from "./core/loop/UpdateLoop.js"
|
|
5
|
+
export { FixedStepClock } from "./core/loop/FixedStepClock.js"
|
|
6
|
+
export { InputReplayTimeline } from "./core/loop/InputReplayTimeline.js"
|
|
7
|
+
export { RuntimeBridge } from "./core/runtime/RuntimeBridge.js"
|
|
8
|
+
export { SceneManager } from "./core/state/SceneManager.js"
|
|
9
|
+
export { SceneSnapshotStore } from "./core/state/SceneSnapshotStore.js"
|
|
10
|
+
export { AssetManifestLoader } from "./core/state/AssetManifestLoader.js"
|
|
11
|
+
export { GameRuntime } from "./adapters/runtime/GameRuntime.js"
|
package/src/main.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { GameRuntime } from "./index.js"
|
|
2
|
+
import { createBootstrapScene } from "./scenes/createBootstrapScene.js"
|
|
3
|
+
|
|
4
|
+
function createDebugPanel(app) {
|
|
5
|
+
const panel = document.createElement("aside")
|
|
6
|
+
panel.style.position = "fixed"
|
|
7
|
+
panel.style.left = "14px"
|
|
8
|
+
panel.style.top = "14px"
|
|
9
|
+
panel.style.width = "320px"
|
|
10
|
+
panel.style.padding = "12px"
|
|
11
|
+
panel.style.border = "1px solid rgba(255, 255, 255, 0.18)"
|
|
12
|
+
panel.style.borderRadius = "10px"
|
|
13
|
+
panel.style.background = "rgba(8, 12, 28, 0.84)"
|
|
14
|
+
panel.style.backdropFilter = "blur(6px)"
|
|
15
|
+
panel.style.color = "#dfe9ff"
|
|
16
|
+
panel.style.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
|
17
|
+
panel.style.zIndex = "1000"
|
|
18
|
+
panel.style.display = "grid"
|
|
19
|
+
panel.style.gap = "8px"
|
|
20
|
+
|
|
21
|
+
const title = document.createElement("div")
|
|
22
|
+
title.textContent = "Web Game Engine Debug"
|
|
23
|
+
title.style.fontWeight = "700"
|
|
24
|
+
title.style.letterSpacing = "0.04em"
|
|
25
|
+
panel.appendChild(title)
|
|
26
|
+
|
|
27
|
+
const snapshotRow = document.createElement("div")
|
|
28
|
+
snapshotRow.style.display = "grid"
|
|
29
|
+
snapshotRow.style.gridTemplateColumns = "1fr auto"
|
|
30
|
+
snapshotRow.style.gap = "6px"
|
|
31
|
+
|
|
32
|
+
const snapshotInput = document.createElement("input")
|
|
33
|
+
snapshotInput.type = "text"
|
|
34
|
+
snapshotInput.value = "bootstrap-initial"
|
|
35
|
+
snapshotInput.placeholder = "snapshot name"
|
|
36
|
+
snapshotInput.style.padding = "6px"
|
|
37
|
+
snapshotInput.style.background = "rgba(255,255,255,0.08)"
|
|
38
|
+
snapshotInput.style.border = "1px solid rgba(255,255,255,0.2)"
|
|
39
|
+
snapshotInput.style.borderRadius = "6px"
|
|
40
|
+
snapshotInput.style.color = "#fff"
|
|
41
|
+
|
|
42
|
+
const snapshotSaveButton = document.createElement("button")
|
|
43
|
+
snapshotSaveButton.type = "button"
|
|
44
|
+
snapshotSaveButton.textContent = "Save"
|
|
45
|
+
snapshotSaveButton.style.padding = "6px 10px"
|
|
46
|
+
snapshotSaveButton.style.cursor = "pointer"
|
|
47
|
+
|
|
48
|
+
snapshotRow.append(snapshotInput, snapshotSaveButton)
|
|
49
|
+
panel.appendChild(snapshotRow)
|
|
50
|
+
|
|
51
|
+
const snapshotControls = document.createElement("div")
|
|
52
|
+
snapshotControls.style.display = "grid"
|
|
53
|
+
snapshotControls.style.gridTemplateColumns = "1fr auto"
|
|
54
|
+
snapshotControls.style.gap = "6px"
|
|
55
|
+
|
|
56
|
+
const snapshotSelect = document.createElement("select")
|
|
57
|
+
snapshotSelect.style.padding = "6px"
|
|
58
|
+
snapshotSelect.style.background = "rgba(255,255,255,0.08)"
|
|
59
|
+
snapshotSelect.style.border = "1px solid rgba(255,255,255,0.2)"
|
|
60
|
+
snapshotSelect.style.borderRadius = "6px"
|
|
61
|
+
snapshotSelect.style.color = "#fff"
|
|
62
|
+
|
|
63
|
+
const snapshotLoadButton = document.createElement("button")
|
|
64
|
+
snapshotLoadButton.type = "button"
|
|
65
|
+
snapshotLoadButton.textContent = "Load"
|
|
66
|
+
snapshotLoadButton.style.padding = "6px 10px"
|
|
67
|
+
snapshotLoadButton.style.cursor = "pointer"
|
|
68
|
+
|
|
69
|
+
snapshotControls.append(snapshotSelect, snapshotLoadButton)
|
|
70
|
+
panel.appendChild(snapshotControls)
|
|
71
|
+
|
|
72
|
+
const profileRow = document.createElement("div")
|
|
73
|
+
profileRow.style.display = "grid"
|
|
74
|
+
profileRow.style.gridTemplateColumns = "1fr auto"
|
|
75
|
+
profileRow.style.gap = "6px"
|
|
76
|
+
|
|
77
|
+
const profileSelect = document.createElement("select")
|
|
78
|
+
profileSelect.style.padding = "6px"
|
|
79
|
+
profileSelect.style.background = "rgba(255,255,255,0.08)"
|
|
80
|
+
profileSelect.style.border = "1px solid rgba(255,255,255,0.2)"
|
|
81
|
+
profileSelect.style.borderRadius = "6px"
|
|
82
|
+
profileSelect.style.color = "#fff"
|
|
83
|
+
|
|
84
|
+
const profileCopyButton = document.createElement("button")
|
|
85
|
+
profileCopyButton.type = "button"
|
|
86
|
+
profileCopyButton.textContent = "Copy JSON"
|
|
87
|
+
profileCopyButton.style.padding = "6px 10px"
|
|
88
|
+
profileCopyButton.style.cursor = "pointer"
|
|
89
|
+
|
|
90
|
+
profileRow.append(profileSelect, profileCopyButton)
|
|
91
|
+
panel.appendChild(profileRow)
|
|
92
|
+
|
|
93
|
+
const info = document.createElement("pre")
|
|
94
|
+
info.style.margin = "0"
|
|
95
|
+
info.style.padding = "8px"
|
|
96
|
+
info.style.maxHeight = "170px"
|
|
97
|
+
info.style.overflow = "auto"
|
|
98
|
+
info.style.whiteSpace = "pre-wrap"
|
|
99
|
+
info.style.background = "rgba(255,255,255,0.05)"
|
|
100
|
+
info.style.borderRadius = "6px"
|
|
101
|
+
panel.appendChild(info)
|
|
102
|
+
|
|
103
|
+
const refreshUI = () => {
|
|
104
|
+
const snapshots = app.listSceneSnapshots()
|
|
105
|
+
const currentSnapshot = snapshotSelect.value
|
|
106
|
+
snapshotSelect.innerHTML = ""
|
|
107
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
108
|
+
const option = document.createElement("option")
|
|
109
|
+
option.value = snapshots[i]
|
|
110
|
+
option.textContent = snapshots[i]
|
|
111
|
+
snapshotSelect.appendChild(option)
|
|
112
|
+
}
|
|
113
|
+
if (snapshots.includes(currentSnapshot)) {
|
|
114
|
+
snapshotSelect.value = currentSnapshot
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const profiles = app.listInputProfiles()
|
|
118
|
+
const currentProfile = profileSelect.value
|
|
119
|
+
profileSelect.innerHTML = ""
|
|
120
|
+
for (let i = 0; i < profiles.length; i++) {
|
|
121
|
+
const option = document.createElement("option")
|
|
122
|
+
option.value = profiles[i]
|
|
123
|
+
option.textContent = profiles[i]
|
|
124
|
+
profileSelect.appendChild(option)
|
|
125
|
+
}
|
|
126
|
+
if (profiles.includes(currentProfile)) {
|
|
127
|
+
profileSelect.value = currentProfile
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const profile = profileSelect.value ? app.getInputProfile(profileSelect.value) : null
|
|
131
|
+
info.textContent = JSON.stringify(
|
|
132
|
+
{
|
|
133
|
+
snapshots,
|
|
134
|
+
profiles,
|
|
135
|
+
selectedProfile: profileSelect.value || null,
|
|
136
|
+
selectedProfileData: profile,
|
|
137
|
+
hint: "Save/Load snapshots + inspect input profiles",
|
|
138
|
+
},
|
|
139
|
+
null,
|
|
140
|
+
2,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
snapshotSaveButton.addEventListener("click", () => {
|
|
145
|
+
const name = snapshotInput.value.trim()
|
|
146
|
+
if (!name) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
app.saveSceneSnapshot(name)
|
|
150
|
+
refreshUI()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
snapshotLoadButton.addEventListener("click", () => {
|
|
154
|
+
const selected = snapshotSelect.value
|
|
155
|
+
if (!selected) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
app.loadSceneSnapshot(selected)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
profileCopyButton.addEventListener("click", async () => {
|
|
162
|
+
const selected = profileSelect.value
|
|
163
|
+
if (!selected) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
const profile = app.getInputProfile(selected)
|
|
167
|
+
if (!profile) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const text = JSON.stringify(profile, null, 2)
|
|
172
|
+
try {
|
|
173
|
+
await navigator.clipboard.writeText(text)
|
|
174
|
+
} catch {
|
|
175
|
+
// Clipboard API can be unavailable depending on browser permissions.
|
|
176
|
+
}
|
|
177
|
+
refreshUI()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
profileSelect.addEventListener("change", refreshUI)
|
|
181
|
+
snapshotSelect.addEventListener("change", refreshUI)
|
|
182
|
+
|
|
183
|
+
refreshUI()
|
|
184
|
+
document.body.appendChild(panel)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function boot() {
|
|
188
|
+
const canvas = document.getElementById("gpu-canvas")
|
|
189
|
+
const app = new GameRuntime({ canvas })
|
|
190
|
+
await app.init()
|
|
191
|
+
app.registerState("bootstrap", {
|
|
192
|
+
scene: createBootstrapScene(),
|
|
193
|
+
})
|
|
194
|
+
app.setState("bootstrap")
|
|
195
|
+
|
|
196
|
+
app.defineInputProfile("default", {
|
|
197
|
+
moveLeft: [{ type: "keyboard", code: "KeyA" }],
|
|
198
|
+
moveRight: [{ type: "keyboard", code: "KeyD" }],
|
|
199
|
+
})
|
|
200
|
+
app.defineInputProfile("camera-pan", {
|
|
201
|
+
pan: [{ type: "keyboard", chord: ["ShiftLeft", "Space"] }],
|
|
202
|
+
zoomIn: [{ type: "keyboard", code: "KeyQ" }],
|
|
203
|
+
zoomOut: [{ type: "keyboard", code: "KeyE" }],
|
|
204
|
+
})
|
|
205
|
+
app.saveSceneSnapshot("bootstrap-initial")
|
|
206
|
+
createDebugPanel(app)
|
|
207
|
+
|
|
208
|
+
app.start()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
boot().catch((error) => {
|
|
212
|
+
console.error(error)
|
|
213
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ColorRGBA, Scene, Sprite, Vec2 } from "@jts-studios/engine"
|
|
2
|
+
|
|
3
|
+
export function createBootstrapScene() {
|
|
4
|
+
const scene = new Scene({
|
|
5
|
+
backgroundColor: new ColorRGBA(0.04, 0.06, 0.1, 1),
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
scene.add(
|
|
9
|
+
new Sprite({
|
|
10
|
+
position: new Vec2(-40, -40),
|
|
11
|
+
size: new Vec2(80, 80),
|
|
12
|
+
color: new ColorRGBA(0.2, 0.8, 0.9, 1),
|
|
13
|
+
}),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
return scene
|
|
17
|
+
}
|