@typeonce/ritmo-devtools 0.6.0 → 0.7.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/README.md +105 -21
- package/dist/Determinism.d.ts +70 -0
- package/dist/Determinism.d.ts.map +1 -0
- package/dist/Determinism.js +137 -0
- package/dist/Determinism.js.map +1 -0
- package/dist/Format.d.ts +6 -1
- package/dist/Format.d.ts.map +1 -1
- package/dist/Format.js +24 -0
- package/dist/Format.js.map +1 -1
- package/dist/Invariant.d.ts +34 -12
- package/dist/Invariant.d.ts.map +1 -1
- package/dist/Invariant.js +6 -19
- package/dist/Invariant.js.map +1 -1
- package/dist/Lint.d.ts +60 -0
- package/dist/Lint.d.ts.map +1 -0
- package/dist/Lint.js +15 -0
- package/dist/Lint.js.map +1 -0
- package/dist/Monitor.d.ts +18 -4
- package/dist/Monitor.d.ts.map +1 -1
- package/dist/Monitor.js +31 -20
- package/dist/Monitor.js.map +1 -1
- package/dist/Replay.d.ts +271 -0
- package/dist/Replay.d.ts.map +1 -0
- package/dist/Replay.js +335 -0
- package/dist/Replay.js.map +1 -0
- package/dist/ReplayNodeSink.d.ts +11 -0
- package/dist/ReplayNodeSink.d.ts.map +1 -0
- package/dist/ReplayNodeSink.js +29 -0
- package/dist/ReplayNodeSink.js.map +1 -0
- package/dist/ReplayVitePlugin.d.ts +41 -0
- package/dist/ReplayVitePlugin.d.ts.map +1 -0
- package/dist/ReplayVitePlugin.js +41 -0
- package/dist/ReplayVitePlugin.js.map +1 -0
- package/dist/ReplayViteSink.d.ts +30 -0
- package/dist/ReplayViteSink.d.ts.map +1 -0
- package/dist/ReplayViteSink.js +9 -0
- package/dist/ReplayViteSink.js.map +1 -0
- package/dist/Session.d.ts +66 -14
- package/dist/Session.d.ts.map +1 -1
- package/dist/Session.js +92 -6
- package/dist/Session.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +57 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/monitor.d.ts +4 -2
- package/dist/internal/monitor.d.ts.map +1 -1
- package/dist/internal/monitor.js +9 -3
- package/dist/internal/monitor.js.map +1 -1
- package/dist/internal/version.d.ts +7 -0
- package/dist/internal/version.d.ts.map +1 -0
- package/dist/internal/version.js +7 -0
- package/dist/internal/version.js.map +1 -0
- package/package.json +5 -2
- package/src/Determinism.ts +190 -0
- package/src/Format.ts +25 -0
- package/src/Invariant.ts +38 -12
- package/src/Lint.ts +72 -0
- package/src/Monitor.ts +48 -25
- package/src/Replay.ts +583 -0
- package/src/ReplayNodeSink.ts +38 -0
- package/src/ReplayVitePlugin.ts +74 -0
- package/src/ReplayViteSink.ts +33 -0
- package/src/Session.ts +153 -23
- package/src/cli.ts +55 -0
- package/src/index.ts +3 -0
- package/src/internal/monitor.ts +12 -5
- package/src/internal/version.ts +6 -0
package/src/Format.ts
CHANGED
|
@@ -119,6 +119,13 @@ export const system = (input: Debug.SystemDescription): string => {
|
|
|
119
119
|
if (input.despawned) lines.push(" reads despawned")
|
|
120
120
|
add("reads relation failures", input.relationFailures)
|
|
121
121
|
add("services", input.services)
|
|
122
|
+
add("may spawn", input.commands.spawns)
|
|
123
|
+
add("may insert", input.commands.inserts)
|
|
124
|
+
add("may remove", input.commands.removes)
|
|
125
|
+
add("may despawn", input.commands.despawns)
|
|
126
|
+
add("may despawn scope", input.commands.despawnsScope)
|
|
127
|
+
add("may relate", input.commands.relates)
|
|
128
|
+
if (input.thisTick.length > 0) add("needs this tick", input.thisTick)
|
|
122
129
|
add("when", input.when)
|
|
123
130
|
add("pauses when", input.pauseWhen)
|
|
124
131
|
if (input.local) lines.push(" local state")
|
|
@@ -269,6 +276,22 @@ export type LineKind =
|
|
|
269
276
|
| "skip"
|
|
270
277
|
| "transition"
|
|
271
278
|
| "restore"
|
|
279
|
+
| "ignored"
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The text for a failed tick result nobody read: who failed, with what, how
|
|
283
|
+
* many later systems did not run, and when it was found unread.
|
|
284
|
+
*/
|
|
285
|
+
export const ignoredFailure = (event: Debug.FailureIgnoredEvent, options: ValueOptions = {}): string => {
|
|
286
|
+
const who = event.system === undefined ? `${event.schedule} did not run` : `${event.system} failed`
|
|
287
|
+
// A system failure shows what the system returned; anything else (missing requirements) as it is.
|
|
288
|
+
const error = typeof event.error === "object" && event.error !== null && (event.error as { readonly kind?: unknown }).kind === "SystemFailure"
|
|
289
|
+
? (event.error as { readonly error: unknown }).error
|
|
290
|
+
: event.error
|
|
291
|
+
const later = `${event.skipped.length} later ${event.skipped.length === 1 ? "system" : "systems"} did not run`
|
|
292
|
+
const found = event.when === "next-tick" ? "not read before the next tick" : "not read before the session ended"
|
|
293
|
+
return `tick failure ignored: ${who} with ${value(error, options)}; ${later} (${found})`
|
|
294
|
+
}
|
|
272
295
|
|
|
273
296
|
/**
|
|
274
297
|
* Structural equality for plain data: primitives by `Object.is`, arrays and
|
|
@@ -354,6 +377,8 @@ export const lines = (event: Debug.TraceEvent, options: ValueOptions = {}): Read
|
|
|
354
377
|
return []
|
|
355
378
|
case "restore":
|
|
356
379
|
return [{ kind: "restore", frame: event.frame, schedule: "", system: undefined, entity: undefined, subject: undefined, noop: false, text: `restore ${event.ok ? "applied" : "rejected"}` }]
|
|
380
|
+
case "failure.ignored":
|
|
381
|
+
return [{ kind: "ignored", frame: event.frame, schedule: event.schedule, system: event.system, entity: undefined, subject: undefined, noop: false, text: ignoredFailure(event, options) }]
|
|
357
382
|
case "transition":
|
|
358
383
|
return [{
|
|
359
384
|
kind: "transition",
|
package/src/Invariant.ts
CHANGED
|
@@ -1,28 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Named world checks evaluated after every frame of a session run.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* An invariant is a `Game.Inspector` whose projection returns a message
|
|
5
|
+
* describing the violation, or `undefined` when the invariant holds. The
|
|
6
|
+
* inspector declares its reads like a system and cannot write, so the check
|
|
7
|
+
* is typed and read-only, and `Session.make` rejects at compile time an
|
|
8
|
+
* invariant that reads a resource, service, or machine the runtime does not
|
|
9
|
+
* provide. The invariant is named after its inspector.
|
|
10
|
+
*
|
|
11
|
+
* Use `Invariant.fromInspector` for checks over the world during a session.
|
|
12
|
+
* Use `Game.Inspector` with `runtime.inspect(...)` directly for one-off reads
|
|
13
|
+
* in tests and scripts.
|
|
7
14
|
*
|
|
8
15
|
* @module Invariant
|
|
9
16
|
* @docGroup devtools
|
|
10
17
|
*
|
|
11
18
|
* @example
|
|
12
19
|
* ```ts
|
|
13
|
-
* const PlayerInsideWorld = Invariant.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
20
|
+
* const PlayerInsideWorld = Invariant.fromInspector(Game.Inspector("player inside world", {
|
|
21
|
+
* queries: { players: Game.Query({ selection: { position: Game.Query.read(Position) }, with: [Player] }) }
|
|
22
|
+
* }, ({ queries }) => {
|
|
23
|
+
* for (const { data } of queries.players.each()) {
|
|
24
|
+
* const { x } = data.position.get()
|
|
25
|
+
* if (x < 0 || x > WORLD_WIDTH) return `player at x=${x}`
|
|
16
26
|
* }
|
|
17
27
|
* return undefined
|
|
18
|
-
* })
|
|
28
|
+
* }))
|
|
29
|
+
*
|
|
30
|
+
* const session = Session.make(runtime, { schedules: { update }, invariants: [PlayerInsideWorld] })
|
|
19
31
|
* ```
|
|
20
32
|
*/
|
|
33
|
+
import type * as Inspector from "@typeonce/ritmo/Inspector"
|
|
34
|
+
|
|
35
|
+
export namespace Invariant {
|
|
36
|
+
/** An inspector usable as an invariant: it returns a violation message or `undefined`. */
|
|
37
|
+
export type CheckInspector = Inspector.InspectorDefinition<any, string | undefined, any, any, any>
|
|
38
|
+
}
|
|
21
39
|
|
|
22
|
-
export interface Invariant {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
export interface Invariant<I extends Invariant.CheckInspector = Invariant.CheckInspector> {
|
|
41
|
+
/** The inspector's name, printed when the invariant is violated. */
|
|
42
|
+
readonly name: I["name"]
|
|
43
|
+
/** The read-only check: a violation message, or `undefined` when the invariant holds. */
|
|
44
|
+
readonly inspector: I
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Turns a `Game.Inspector` that returns `string | undefined` into an
|
|
49
|
+
* invariant for `Session.make({ invariants })`.
|
|
50
|
+
*/
|
|
51
|
+
export const fromInspector = <const I extends Invariant.CheckInspector>(inspector: I): Invariant<I> => ({
|
|
52
|
+
name: inspector.name,
|
|
53
|
+
inspector
|
|
54
|
+
})
|
package/src/Lint.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The static lints of a game's schedules, as a tool: `describe()`'s lints over
|
|
3
|
+
* a headless runtime, each with its fix, and a verdict (warnings fail, info
|
|
4
|
+
* does not).
|
|
5
|
+
*
|
|
6
|
+
* Run it from a game's `check` script with the CLI:
|
|
7
|
+
*
|
|
8
|
+
* ```sh
|
|
9
|
+
* ritmo-devtools lint src/simulation.ts
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The entry module exports `lintTarget`, a function that builds the game's
|
|
13
|
+
* headless runtime (made with `debug: { hostClock }`) and names its schedules: the
|
|
14
|
+
* ones ticked headless in `schedules`, and the ones that need a renderer or
|
|
15
|
+
* other host services in `describe` (described and linted, never run).
|
|
16
|
+
*
|
|
17
|
+
* Use `Session.make(...).describe()` instead to read the lints while
|
|
18
|
+
* debugging; this module is for the pass/fail check.
|
|
19
|
+
*
|
|
20
|
+
* @module Lint
|
|
21
|
+
* @docGroup devtools
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* // src/simulation.ts
|
|
26
|
+
* export const lintTarget = (): Lint.Target<typeof Game.schema, typeof Game.schema> => {
|
|
27
|
+
* const sim = createSimulation({ keyboard: Keyboard.scripted(bindings, []) })
|
|
28
|
+
* return { runtime: sim.runtime, schedules: { setup: sim.setup, update: sim.update }, describe: { render } }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
import type * as Debug from "@typeonce/ritmo/Debug"
|
|
33
|
+
import type { AnyScheduleOrder, ExecutableScheduleDefinition } from "@typeonce/ritmo/Schedule"
|
|
34
|
+
import type { Schema } from "@typeonce/ritmo/Schema"
|
|
35
|
+
|
|
36
|
+
import * as Rendered from "./Rendered.ts"
|
|
37
|
+
|
|
38
|
+
type AnySchedule<S extends Schema.Any, Root> = ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>
|
|
39
|
+
|
|
40
|
+
/** What `lintTarget` returns. */
|
|
41
|
+
export interface Target<S extends Schema.Any, Root> {
|
|
42
|
+
/** A runtime made with `debug: { hostClock }`. */
|
|
43
|
+
readonly runtime: { readonly debug: Debug.Handle<S, Root> }
|
|
44
|
+
/** The schedules the game ticks, by name. */
|
|
45
|
+
readonly schedules: Readonly<Record<string, AnySchedule<S, Root>>>
|
|
46
|
+
/** Schedules to describe without running (a render schedule needing a renderer). */
|
|
47
|
+
readonly describe?: Readonly<Record<string, AnySchedule<S, Root>>>
|
|
48
|
+
/** Events the host publishes with `runtime.send` (UI requests, network messages): written, for the lints. */
|
|
49
|
+
readonly hostEvents?: ReadonlyArray<Schema.EventDescriptor<S>>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Report {
|
|
53
|
+
readonly lints: ReadonlyArray<Debug.Lint>
|
|
54
|
+
readonly warnings: number
|
|
55
|
+
readonly infos: number
|
|
56
|
+
/** `true` when there is no warning-level lint. */
|
|
57
|
+
readonly ok: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Lints the target's schedules. */
|
|
61
|
+
export const run = <S extends Schema.Any, Root>(target: Target<S, Root>): Rendered.Rendered<Report> => {
|
|
62
|
+
target.runtime.debug.nameSchedules({ ...target.describe, ...target.schedules })
|
|
63
|
+
target.runtime.debug.declareHostEvents(target.hostEvents ?? [])
|
|
64
|
+
const lints = target.runtime.debug.describe().lints
|
|
65
|
+
const warnings = lints.filter((lint) => lint.severity === "warning").length
|
|
66
|
+
const report: Report = { lints, warnings, infos: lints.length - warnings, ok: warnings === 0 }
|
|
67
|
+
const lines = [
|
|
68
|
+
...lints.flatMap((lint) => [`${lint.severity} ${lint.code} ${lint.subject}: ${lint.message}`, ` fix: ${lint.fix}`]),
|
|
69
|
+
`${warnings} ${warnings === 1 ? "warning" : "warnings"}, ${report.infos} info${warnings > 0 ? " (warnings fail)" : ""}`
|
|
70
|
+
]
|
|
71
|
+
return Rendered.make(report, lines.join("\n"))
|
|
72
|
+
}
|
package/src/Monitor.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* The monitor observes the runtime only while it is open, so a closed monitor
|
|
16
16
|
* costs nothing. While open it reduces trace events to counts and shows its
|
|
17
|
-
* own cost per tick. It needs a runtime made with `debug:
|
|
17
|
+
* own cost per tick. It needs a runtime made with `debug: { hostClock }`, and the page passed as `host`.
|
|
18
18
|
*
|
|
19
19
|
* @module Monitor
|
|
20
20
|
* @docGroup devtools
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* @example
|
|
23
23
|
* ```ts
|
|
24
24
|
* const monitor = Monitor.attach(runtime, {
|
|
25
|
+
* host: window,
|
|
25
26
|
* schedules: { fixedUpdate: step, frame },
|
|
26
27
|
* pick: (event) => entityUnderPointer(event),
|
|
27
28
|
* locate: (entity) => screenPositionOf(entity)
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
* ```
|
|
31
32
|
*/
|
|
32
33
|
import type * as Debug from "@typeonce/ritmo/Debug"
|
|
33
|
-
import type { ExecutableScheduleDefinition } from "@typeonce/ritmo/Schedule"
|
|
34
|
+
import type { AnyScheduleOrder, ExecutableScheduleDefinition } from "@typeonce/ritmo/Schedule"
|
|
34
35
|
import type { Schema } from "@typeonce/ritmo/Schema"
|
|
35
36
|
|
|
36
37
|
import * as Format from "./Format.ts"
|
|
@@ -57,8 +58,14 @@ export interface Options<S extends Schema.Any, Root> {
|
|
|
57
58
|
* panel and make up the system map. Name the schedule objects actually
|
|
58
59
|
* passed to `tick`: nested schedules are flattened into them.
|
|
59
60
|
*/
|
|
60
|
-
readonly schedules: Readonly<Record<string, ExecutableScheduleDefinition<S, any, Root, any, any>>>
|
|
61
|
-
/**
|
|
61
|
+
readonly schedules: Readonly<Record<string, ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>>>
|
|
62
|
+
/**
|
|
63
|
+
* The page the panel lives in, passed explicitly (`window`): its
|
|
64
|
+
* `document`, timers, animation frames, keyboard events, and
|
|
65
|
+
* `performance` clock. The monitor reads no globals.
|
|
66
|
+
*/
|
|
67
|
+
readonly host: Window
|
|
68
|
+
/** Where the panel is added. Defaults to `host.document.body`. */
|
|
62
69
|
readonly mount?: HTMLElement
|
|
63
70
|
/** `KeyboardEvent.code` that toggles the panel. Defaults to `Backslash`. */
|
|
64
71
|
readonly key?: string
|
|
@@ -70,6 +77,11 @@ export interface Options<S extends Schema.Any, Root> {
|
|
|
70
77
|
readonly locate?: (entity: number) => { readonly x: number; readonly y: number } | undefined
|
|
71
78
|
/** Milliseconds between panel refreshes. Defaults to 250. */
|
|
72
79
|
readonly refreshMs?: number
|
|
80
|
+
/**
|
|
81
|
+
* A replay recorder (`Replay.recorder`): adds a "Save replay" button that
|
|
82
|
+
* saves the last seconds of play with the reason "Save replay button".
|
|
83
|
+
*/
|
|
84
|
+
readonly replay?: { readonly save: (reason: string) => unknown }
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
export interface Monitor {
|
|
@@ -243,18 +255,19 @@ const STYLE = `
|
|
|
243
255
|
export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, options: Options<S, Root>): Monitor => {
|
|
244
256
|
const debug = runtime.debug
|
|
245
257
|
debug.nameSchedules(options.schedules)
|
|
246
|
-
const
|
|
258
|
+
const host = options.host
|
|
259
|
+
const mount = options.mount ?? host.document.body
|
|
247
260
|
const key = options.key ?? "Backslash"
|
|
248
261
|
const refreshMs = options.refreshMs ?? 250
|
|
249
262
|
|
|
250
|
-
if (document.getElementById("btm-style") === null) {
|
|
251
|
-
const style = document.createElement("style")
|
|
263
|
+
if (host.document.getElementById("btm-style") === null) {
|
|
264
|
+
const style = host.document.createElement("style")
|
|
252
265
|
style.id = "btm-style"
|
|
253
266
|
style.textContent = STYLE
|
|
254
|
-
document.head.append(style)
|
|
267
|
+
host.document.head.append(style)
|
|
255
268
|
}
|
|
256
269
|
|
|
257
|
-
const root = document.createElement("div")
|
|
270
|
+
const root = host.document.createElement("div")
|
|
258
271
|
root.className = "btm"
|
|
259
272
|
root.innerHTML = `
|
|
260
273
|
<button class="btm-toggle" type="button" title="Toggle the Ritmo monitor">◉ monitor</button>
|
|
@@ -264,6 +277,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
|
|
|
264
277
|
<button class="btm-tab" data-tab="overview" type="button">Overview</button>
|
|
265
278
|
<button class="btm-tab" data-tab="systems" type="button">Systems</button>
|
|
266
279
|
<button class="btm-tab" data-tab="entity" type="button">Entity</button>
|
|
280
|
+
${options.replay === undefined ? "" : `<button class="btm-save-replay" type="button" title="Save the last seconds of play as a replay">Save replay</button>`}
|
|
267
281
|
<button class="btm-close" type="button" title="Close">×</button>
|
|
268
282
|
</div>
|
|
269
283
|
<div class="btm-status"></div>
|
|
@@ -294,7 +308,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
|
|
|
294
308
|
let tab: Tab = "overview"
|
|
295
309
|
let collector: Collector | undefined
|
|
296
310
|
let stopObserving: (() => void) | undefined
|
|
297
|
-
let timer:
|
|
311
|
+
let timer: number | undefined
|
|
298
312
|
let openedAt = 0
|
|
299
313
|
let description: Debug.Description | undefined
|
|
300
314
|
/** How many systems read each component, to name what systems spawn. */
|
|
@@ -515,20 +529,20 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
|
|
|
515
529
|
|
|
516
530
|
const render = () => {
|
|
517
531
|
if (collector === undefined) return
|
|
518
|
-
const started = performance.now()
|
|
532
|
+
const started = host.performance.now()
|
|
519
533
|
const history = collector.history
|
|
520
534
|
const latest = history.slice(-RECENT)
|
|
521
535
|
const overhead = latest.reduce((total, sample) => total + sample.overheadMs, 0)
|
|
522
536
|
const ticks = latest.reduce((total, sample) => total + [...sample.schedules.values()].reduce((sum, window) => sum + window.ticks, 0), 0)
|
|
523
|
-
status.textContent = `observing for ${Math.round((performance.now() - openedAt) / 1000)}s · frame ${debug.frame()} · panel ${ticks === 0 ? "–" : number(overhead / ticks, 3)} ms/tick + ${number(renderMs, 1)} ms/refresh`
|
|
537
|
+
status.textContent = `observing for ${Math.round((host.performance.now() - openedAt) / 1000)}s · frame ${debug.frame()} · panel ${ticks === 0 ? "–" : number(overhead / ticks, 3)} ms/tick + ${number(renderMs, 1)} ms/refresh`
|
|
524
538
|
status.title = "The panel's own work. Tracing (on only while the panel is open) also slows every system run a little, and the times shown include it: compare them with each other, not with a closed-monitor run."
|
|
525
539
|
body.innerHTML = tab === "overview" ? renderOverview(history) : tab === "systems" ? renderSystems(history) : renderEntity()
|
|
526
|
-
renderMs = performance.now() - started
|
|
540
|
+
renderMs = host.performance.now() - started
|
|
527
541
|
}
|
|
528
542
|
|
|
529
543
|
const refresh = () => {
|
|
530
544
|
if (collector === undefined) return
|
|
531
|
-
collector.sample(performance.now())
|
|
545
|
+
collector.sample(host.performance.now())
|
|
532
546
|
render()
|
|
533
547
|
}
|
|
534
548
|
|
|
@@ -539,7 +553,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
|
|
|
539
553
|
ring.style.left = `${at.x}px`
|
|
540
554
|
ring.style.top = `${at.y}px`
|
|
541
555
|
}
|
|
542
|
-
if (!panel.hidden) requestAnimationFrame(placeRing)
|
|
556
|
+
if (!panel.hidden) host.requestAnimationFrame(placeRing)
|
|
543
557
|
}
|
|
544
558
|
|
|
545
559
|
const setTab = (next: Tab) => {
|
|
@@ -555,14 +569,14 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
|
|
|
555
569
|
readers = new Map(description.access.components.map((entry) => [entry.name, entry.readers.length]))
|
|
556
570
|
prefix = commonPrefix([...description.systems.map((system) => system.name), ...description.components.map((component) => component.name)])
|
|
557
571
|
browse.replaceChildren(new Option("browse by component…", ""), ...description.components.map((component) => new Option(short(component.name), component.name)))
|
|
558
|
-
collector = new Collector(() => debug.population(), performance.now())
|
|
572
|
+
collector = new Collector(() => debug.population(), () => host.performance.now())
|
|
559
573
|
collector.select(selected)
|
|
560
574
|
stopObserving = debug.observe(collector.observe)
|
|
561
|
-
openedAt = performance.now()
|
|
575
|
+
openedAt = host.performance.now()
|
|
562
576
|
panel.hidden = false
|
|
563
|
-
timer = setInterval(refresh, refreshMs)
|
|
577
|
+
timer = host.setInterval(refresh, refreshMs)
|
|
564
578
|
setTab(tab)
|
|
565
|
-
requestAnimationFrame(placeRing)
|
|
579
|
+
host.requestAnimationFrame(placeRing)
|
|
566
580
|
}
|
|
567
581
|
|
|
568
582
|
const close = () => {
|
|
@@ -571,7 +585,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
|
|
|
571
585
|
ring.hidden = true
|
|
572
586
|
stopObserving?.()
|
|
573
587
|
stopObserving = undefined
|
|
574
|
-
if (timer !== undefined) clearInterval(timer)
|
|
588
|
+
if (timer !== undefined) host.clearInterval(timer)
|
|
575
589
|
timer = undefined
|
|
576
590
|
collector = undefined
|
|
577
591
|
setPicking(false)
|
|
@@ -603,9 +617,9 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
|
|
|
603
617
|
picking = on
|
|
604
618
|
pickButton.classList.toggle("is-on", on)
|
|
605
619
|
pickButton.textContent = on ? "Click an entity… (Esc)" : "Pick on screen"
|
|
606
|
-
document.documentElement.classList.toggle("btm-picking", on)
|
|
607
|
-
if (on)
|
|
608
|
-
else
|
|
620
|
+
host.document.documentElement.classList.toggle("btm-picking", on)
|
|
621
|
+
if (on) host.addEventListener("pointerdown", onPick, { capture: true })
|
|
622
|
+
else host.removeEventListener("pointerdown", onPick, { capture: true })
|
|
609
623
|
}
|
|
610
624
|
|
|
611
625
|
// ── Events ─────────────────────────────────────────────────────────────
|
|
@@ -620,9 +634,18 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
|
|
|
620
634
|
setPicking(false)
|
|
621
635
|
}
|
|
622
636
|
}
|
|
623
|
-
|
|
637
|
+
host.addEventListener("keydown", onKey)
|
|
624
638
|
toggleButton.addEventListener("click", () => (panel.hidden ? open() : close()))
|
|
625
639
|
query<HTMLButtonElement>(".btm-close").addEventListener("click", close)
|
|
640
|
+
const replay = options.replay
|
|
641
|
+
if (replay !== undefined) {
|
|
642
|
+
const button = query<HTMLButtonElement>(".btm-save-replay")
|
|
643
|
+
button.addEventListener("click", () => {
|
|
644
|
+
replay.save("Save replay button")
|
|
645
|
+
button.textContent = "Saved"
|
|
646
|
+
host.setTimeout(() => { button.textContent = "Save replay" }, 1500)
|
|
647
|
+
})
|
|
648
|
+
}
|
|
626
649
|
for (const button of root.querySelectorAll<HTMLButtonElement>(".btm-tab")) {
|
|
627
650
|
button.addEventListener("click", () => setTab(button.dataset["tab"] as Tab))
|
|
628
651
|
}
|
|
@@ -681,7 +704,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
|
|
|
681
704
|
select,
|
|
682
705
|
detach: () => {
|
|
683
706
|
close()
|
|
684
|
-
|
|
707
|
+
host.removeEventListener("keydown", onKey)
|
|
685
708
|
root.remove()
|
|
686
709
|
}
|
|
687
710
|
}
|