@bloopjs/web 0.0.56 → 0.0.58
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/App.d.ts +9 -1
- package/dist/App.d.ts.map +1 -1
- package/dist/debugui/DebugUi.d.ts.map +1 -1
- package/dist/debugui/components/BottomBar.d.ts +8 -0
- package/dist/debugui/components/BottomBar.d.ts.map +1 -0
- package/dist/debugui/components/DebugToggle.d.ts.map +1 -1
- package/dist/debugui/components/Root.d.ts.map +1 -1
- package/dist/debugui/components/TopBar.d.ts +7 -0
- package/dist/debugui/components/TopBar.d.ts.map +1 -0
- package/dist/debugui/components/VerticalBar.d.ts +9 -0
- package/dist/debugui/components/VerticalBar.d.ts.map +1 -0
- package/dist/debugui/state.d.ts +12 -1
- package/dist/debugui/state.d.ts.map +1 -1
- package/dist/debugui/styles.d.ts +1 -1
- package/dist/debugui/styles.d.ts.map +1 -1
- package/dist/mod.js +624 -26
- package/dist/mod.js.map +12 -9
- package/package.json +3 -3
- package/src/App.ts +48 -2
- package/src/debugui/DebugUi.ts +7 -7
- package/src/debugui/components/BottomBar.tsx +68 -0
- package/src/debugui/components/DebugToggle.tsx +2 -6
- package/src/debugui/components/Root.tsx +84 -17
- package/src/debugui/components/TopBar.tsx +36 -0
- package/src/debugui/components/VerticalBar.tsx +26 -0
- package/src/debugui/state.ts +82 -5
- package/src/debugui/styles.ts +249 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloopjs/web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.58",
|
|
4
4
|
"author": "Neil Sarkar",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"typescript": "^5"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@bloopjs/bloop": "0.0.
|
|
37
|
-
"@bloopjs/engine": "0.0.
|
|
36
|
+
"@bloopjs/bloop": "0.0.58",
|
|
37
|
+
"@bloopjs/engine": "0.0.58",
|
|
38
38
|
"@preact/signals": "^1.3.1",
|
|
39
39
|
"partysocket": "^1.1.6",
|
|
40
40
|
"preact": "^10.25.4"
|
package/src/App.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "./netcode/broker";
|
|
8
8
|
import { logger } from "./netcode/logs.ts";
|
|
9
9
|
import { DebugUi, type DebugUiOptions } from "./debugui/mod.ts";
|
|
10
|
+
import { debugState, triggerHmrFlash } from "./debugui/state.ts";
|
|
10
11
|
|
|
11
12
|
export type StartOptions = {
|
|
12
13
|
/** A bloop game instance */
|
|
@@ -130,6 +131,8 @@ export class App {
|
|
|
130
131
|
beforeFrame: ReturnType<typeof createListener> = createListener<[number]>();
|
|
131
132
|
/** Event listeners for after a frame is processed */
|
|
132
133
|
afterFrame: ReturnType<typeof createListener> = createListener<[number]>();
|
|
134
|
+
/** Event listeners for HMR events */
|
|
135
|
+
onHmr: ReturnType<typeof createListener> = createListener<[HmrEvent]>();
|
|
133
136
|
|
|
134
137
|
/** Subscribe to the browser events and start the render loop */
|
|
135
138
|
subscribe(): void {
|
|
@@ -208,8 +211,38 @@ export class App {
|
|
|
208
211
|
};
|
|
209
212
|
window.addEventListener("keydown", playbarHotkeys);
|
|
210
213
|
|
|
214
|
+
// FPS calculation
|
|
215
|
+
let fpsFrames = 0;
|
|
216
|
+
let fpsLastTime = performance.now();
|
|
217
|
+
|
|
211
218
|
const frame = () => {
|
|
212
|
-
|
|
219
|
+
const stepStart = performance.now();
|
|
220
|
+
const ticks = this.sim.step(stepStart - this.#now);
|
|
221
|
+
|
|
222
|
+
// Update debug metrics only when we actually ran simulation
|
|
223
|
+
if (ticks > 0) {
|
|
224
|
+
const stepEnd = performance.now();
|
|
225
|
+
debugState.frameTime.value = stepEnd - stepStart;
|
|
226
|
+
debugState.frameNumber.value = this.sim.time.frame;
|
|
227
|
+
|
|
228
|
+
// Measure snapshot size when debug UI is visible (letterboxed mode)
|
|
229
|
+
if (debugState.layoutMode.value === "letterboxed") {
|
|
230
|
+
const bag = this.game.bag;
|
|
231
|
+
if (bag) {
|
|
232
|
+
debugState.snapshotSize.value = JSON.stringify(bag).length;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Calculate FPS every second
|
|
237
|
+
fpsFrames++;
|
|
238
|
+
const elapsed = stepEnd - fpsLastTime;
|
|
239
|
+
if (elapsed >= 1000) {
|
|
240
|
+
debugState.fps.value = Math.round((fpsFrames * 1000) / elapsed);
|
|
241
|
+
fpsFrames = 0;
|
|
242
|
+
fpsLastTime = stepEnd;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
213
246
|
if (!this.sim.isPaused) {
|
|
214
247
|
try {
|
|
215
248
|
this.afterFrame.notify(this.sim.time.frame);
|
|
@@ -245,6 +278,7 @@ export class App {
|
|
|
245
278
|
this.sim.unmount();
|
|
246
279
|
this.beforeFrame.unsubscribeAll();
|
|
247
280
|
this.afterFrame.unsubscribeAll();
|
|
281
|
+
this.onHmr.unsubscribeAll();
|
|
248
282
|
this.#debugUi?.unmount();
|
|
249
283
|
}
|
|
250
284
|
|
|
@@ -257,10 +291,14 @@ export class App {
|
|
|
257
291
|
* import.meta.hot?.accept("./game", async (newModule) => {
|
|
258
292
|
* await app.acceptHmr(newModule?.game, {
|
|
259
293
|
* wasmUrl: monorepoWasmUrl,
|
|
294
|
+
* files: ["./game"],
|
|
260
295
|
* });
|
|
261
296
|
* ```
|
|
262
297
|
*/
|
|
263
|
-
async acceptHmr(
|
|
298
|
+
async acceptHmr(
|
|
299
|
+
module: any,
|
|
300
|
+
opts?: Partial<MountOpts> & { files?: string[] },
|
|
301
|
+
): Promise<void> {
|
|
264
302
|
const game = (module.game ?? module) as Bloop<any>;
|
|
265
303
|
if (!game.hooks) {
|
|
266
304
|
throw new Error(
|
|
@@ -279,6 +317,10 @@ export class App {
|
|
|
279
317
|
this.sim.unmount();
|
|
280
318
|
this.sim = sim;
|
|
281
319
|
this.game = game;
|
|
320
|
+
|
|
321
|
+
// Trigger HMR flash and notify listeners
|
|
322
|
+
triggerHmrFlash();
|
|
323
|
+
this.onHmr.notify({ files: opts?.files ?? [] });
|
|
282
324
|
}
|
|
283
325
|
}
|
|
284
326
|
|
|
@@ -312,3 +354,7 @@ function createListener<T extends any[]>(): {
|
|
|
312
354
|
}
|
|
313
355
|
|
|
314
356
|
export type UnsubscribeFn = () => void;
|
|
357
|
+
|
|
358
|
+
export type HmrEvent = {
|
|
359
|
+
files: string[];
|
|
360
|
+
};
|
package/src/debugui/DebugUi.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ComponentChild, render } from "preact";
|
|
2
2
|
import { Root } from "./components/Root.tsx";
|
|
3
|
-
import { type DebugState, debugState } from "./state.ts";
|
|
3
|
+
import { type DebugState, cycleLayout, debugState } from "./state.ts";
|
|
4
4
|
import { styles } from "./styles.ts";
|
|
5
5
|
|
|
6
6
|
export type DebugUiOptions = {
|
|
@@ -30,7 +30,7 @@ export class DebugUi {
|
|
|
30
30
|
// Create host element
|
|
31
31
|
this.#host = document.createElement("bloop-debug-ui");
|
|
32
32
|
this.#host.style.cssText =
|
|
33
|
-
"display:block;width:100%;height:100%;position:absolute;top:0;left:0;";
|
|
33
|
+
"display:block;width:100%;height:100%;position:absolute;top:0;left:0;overflow:hidden;overscroll-behavior:none;";
|
|
34
34
|
|
|
35
35
|
// Attach shadow DOM
|
|
36
36
|
this.#shadow = this.#host.attachShadow({ mode: "open" });
|
|
@@ -47,7 +47,7 @@ export class DebugUi {
|
|
|
47
47
|
this.#shadow.appendChild(this.#mountPoint);
|
|
48
48
|
|
|
49
49
|
// Initialize state
|
|
50
|
-
debugState.
|
|
50
|
+
debugState.layoutMode.value = initiallyVisible ? "letterboxed" : "off";
|
|
51
51
|
|
|
52
52
|
// Create canvas element (game renders here)
|
|
53
53
|
this.#canvas = document.createElement("canvas");
|
|
@@ -61,8 +61,8 @@ export class DebugUi {
|
|
|
61
61
|
// Set up hotkey listener
|
|
62
62
|
this.#cleanup = this.#setupHotkey();
|
|
63
63
|
|
|
64
|
-
// Re-render when
|
|
65
|
-
debugState.
|
|
64
|
+
// Re-render when layoutMode changes
|
|
65
|
+
debugState.layoutMode.subscribe(() => {
|
|
66
66
|
this.#render();
|
|
67
67
|
});
|
|
68
68
|
}
|
|
@@ -77,7 +77,7 @@ export class DebugUi {
|
|
|
77
77
|
#setupHotkey(): () => void {
|
|
78
78
|
const handler = (e: KeyboardEvent) => {
|
|
79
79
|
if (e.key === this.#hotkey) {
|
|
80
|
-
|
|
80
|
+
cycleLayout();
|
|
81
81
|
}
|
|
82
82
|
};
|
|
83
83
|
window.addEventListener("keydown", handler);
|
|
@@ -100,7 +100,7 @@ export class DebugUi {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
set isVisible(value: boolean) {
|
|
103
|
-
debugState.
|
|
103
|
+
debugState.layoutMode.value = value ? "letterboxed" : "off";
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
unmount(): void {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
type BottomBarProps = {
|
|
2
|
+
tapeUtilization: number; // 0-1, how full the tape is
|
|
3
|
+
playheadPosition: number; // 0-1, current position in tape
|
|
4
|
+
isPlaying: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function BottomBar({
|
|
8
|
+
tapeUtilization = 0,
|
|
9
|
+
playheadPosition = 0,
|
|
10
|
+
isPlaying = true,
|
|
11
|
+
}: BottomBarProps) {
|
|
12
|
+
// Placeholder handlers - behavior not wired up yet
|
|
13
|
+
const handleJumpBack = () => {};
|
|
14
|
+
const handleStepBack = () => {};
|
|
15
|
+
const handlePlayPause = () => {};
|
|
16
|
+
const handleStepForward = () => {};
|
|
17
|
+
const handleJumpForward = () => {};
|
|
18
|
+
const handleSeek = () => {};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="bottom-bar">
|
|
22
|
+
<div className="playbar-controls">
|
|
23
|
+
<button className="playbar-btn" onClick={handleJumpBack}>
|
|
24
|
+
{"<<"}
|
|
25
|
+
<span className="tooltip tooltip-left">
|
|
26
|
+
Jump back <kbd>4</kbd>
|
|
27
|
+
</span>
|
|
28
|
+
</button>
|
|
29
|
+
<button className="playbar-btn" onClick={handleStepBack}>
|
|
30
|
+
{"<"}
|
|
31
|
+
<span className="tooltip">
|
|
32
|
+
Step back <kbd>5</kbd>
|
|
33
|
+
</span>
|
|
34
|
+
</button>
|
|
35
|
+
<button className="playbar-btn" onClick={handlePlayPause}>
|
|
36
|
+
{isPlaying ? "||" : ">"}
|
|
37
|
+
<span className="tooltip">
|
|
38
|
+
{isPlaying ? "Pause" : "Play"} <kbd>6</kbd>
|
|
39
|
+
</span>
|
|
40
|
+
</button>
|
|
41
|
+
<button className="playbar-btn" onClick={handleStepForward}>
|
|
42
|
+
{">"}
|
|
43
|
+
<span className="tooltip">
|
|
44
|
+
Step forward <kbd>7</kbd>
|
|
45
|
+
</span>
|
|
46
|
+
</button>
|
|
47
|
+
<button className="playbar-btn" onClick={handleJumpForward}>
|
|
48
|
+
{">>"}
|
|
49
|
+
<span className="tooltip">
|
|
50
|
+
Jump forward <kbd>8</kbd>
|
|
51
|
+
</span>
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="seek-bar" onClick={handleSeek}>
|
|
55
|
+
<div
|
|
56
|
+
className="seek-bar-fill"
|
|
57
|
+
style={{ width: `${tapeUtilization * 100}%` }}
|
|
58
|
+
/>
|
|
59
|
+
{tapeUtilization > 0 && (
|
|
60
|
+
<div
|
|
61
|
+
className="seek-bar-position"
|
|
62
|
+
style={{ left: `${playheadPosition * tapeUtilization * 100}%` }}
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { debugState } from "../state.ts";
|
|
1
|
+
import { cycleLayout, debugState } from "../state.ts";
|
|
2
2
|
|
|
3
3
|
type DebugToggleProps = {
|
|
4
4
|
hotkey?: string;
|
|
@@ -7,14 +7,10 @@ type DebugToggleProps = {
|
|
|
7
7
|
export function DebugToggle({ hotkey = "Escape" }: DebugToggleProps) {
|
|
8
8
|
const isVisible = debugState.isVisible.value;
|
|
9
9
|
|
|
10
|
-
const toggle = () => {
|
|
11
|
-
debugState.isVisible.value = !debugState.isVisible.value;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
10
|
return (
|
|
15
11
|
<button
|
|
16
12
|
className="debug-toggle"
|
|
17
|
-
onClick={
|
|
13
|
+
onClick={cycleLayout}
|
|
18
14
|
onMouseDown={(e) => e.stopPropagation()}
|
|
19
15
|
onMouseUp={(e) => e.stopPropagation()}
|
|
20
16
|
title={isVisible ? `Hide debug (${hotkey})` : `Show debug (${hotkey})`}
|
|
@@ -3,6 +3,9 @@ import { debugState } from "../state.ts";
|
|
|
3
3
|
import { Stats } from "./Stats.tsx";
|
|
4
4
|
import { Logs } from "./Logs.tsx";
|
|
5
5
|
import { DebugToggle } from "./DebugToggle.tsx";
|
|
6
|
+
import { TopBar } from "./TopBar.tsx";
|
|
7
|
+
import { VerticalBar } from "./VerticalBar.tsx";
|
|
8
|
+
import { BottomBar } from "./BottomBar.tsx";
|
|
6
9
|
|
|
7
10
|
type RootProps = {
|
|
8
11
|
canvas: HTMLCanvasElement;
|
|
@@ -10,32 +13,96 @@ type RootProps = {
|
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export function Root({ canvas, hotkey = "Escape" }: RootProps) {
|
|
13
|
-
const
|
|
16
|
+
const layoutMode = debugState.layoutMode.value;
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<main className="layout">
|
|
19
|
-
<section className="game">
|
|
20
|
-
<GameCanvas canvas={canvas} />
|
|
21
|
-
</section>
|
|
22
|
-
<section className="stats">
|
|
23
|
-
<Stats />
|
|
24
|
-
</section>
|
|
25
|
-
<section className="logs">
|
|
26
|
-
<Logs />
|
|
27
|
-
</section>
|
|
28
|
-
</main>
|
|
29
|
-
) : (
|
|
18
|
+
if (layoutMode === "off") {
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
30
21
|
<main className="fullscreen">
|
|
31
22
|
<GameCanvas canvas={canvas} />
|
|
32
23
|
</main>
|
|
33
|
-
|
|
24
|
+
<DebugToggle hotkey={hotkey} />
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (layoutMode === "letterboxed") {
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<LetterboxedLayout canvas={canvas} />
|
|
33
|
+
<DebugToggle hotkey={hotkey} />
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Full layout (netcode debug)
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<main className="layout">
|
|
42
|
+
<section className="game">
|
|
43
|
+
<GameCanvas canvas={canvas} />
|
|
44
|
+
</section>
|
|
45
|
+
<section className="stats">
|
|
46
|
+
<Stats />
|
|
47
|
+
</section>
|
|
48
|
+
<section className="logs">
|
|
49
|
+
<Logs />
|
|
50
|
+
</section>
|
|
51
|
+
</main>
|
|
34
52
|
<DebugToggle hotkey={hotkey} />
|
|
35
53
|
</>
|
|
36
54
|
);
|
|
37
55
|
}
|
|
38
56
|
|
|
57
|
+
function LetterboxedLayout({ canvas }: { canvas: HTMLCanvasElement }) {
|
|
58
|
+
const isOnline = debugState.netStatus.value.peers.length > 0;
|
|
59
|
+
const advantage = debugState.advantage.value ?? 0;
|
|
60
|
+
const frameTime = debugState.frameTime.value;
|
|
61
|
+
const snapshotSize = debugState.snapshotSize.value;
|
|
62
|
+
const hmrFlash = debugState.hmrFlash.value;
|
|
63
|
+
|
|
64
|
+
// Left bar: frame advantage (online) or frame time % (offline)
|
|
65
|
+
const leftValue = isOnline ? Math.abs(advantage) : frameTime;
|
|
66
|
+
const leftMax = isOnline ? 10 : 16.67; // 10 frames advantage or 16.67ms budget
|
|
67
|
+
const leftLabel = isOnline ? "ADV" : "MS";
|
|
68
|
+
const leftColor = isOnline
|
|
69
|
+
? advantage >= 0
|
|
70
|
+
? "#4a9eff"
|
|
71
|
+
: "#ff4a4a"
|
|
72
|
+
: frameTime > 16.67
|
|
73
|
+
? "#ff4a4a"
|
|
74
|
+
: "#4aff4a";
|
|
75
|
+
|
|
76
|
+
// Right bar: rollback depth (online) or snapshot size (offline)
|
|
77
|
+
// For now, we don't have rollback depth exposed, so use a placeholder
|
|
78
|
+
const rightValue = isOnline ? 0 : snapshotSize;
|
|
79
|
+
const rightMax = isOnline ? 10 : 10000; // 10 frames rollback or 10KB
|
|
80
|
+
const rightLabel = isOnline ? "RB" : "KB";
|
|
81
|
+
|
|
82
|
+
const gameClassName = hmrFlash ? "letterboxed-game hmr-flash" : "letterboxed-game";
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<main className="layout-letterboxed">
|
|
86
|
+
<TopBar leftLabel={leftLabel} rightLabel={rightLabel} />
|
|
87
|
+
<VerticalBar
|
|
88
|
+
value={leftValue}
|
|
89
|
+
max={leftMax}
|
|
90
|
+
side="left"
|
|
91
|
+
color={leftColor}
|
|
92
|
+
/>
|
|
93
|
+
<div className={gameClassName}>
|
|
94
|
+
<GameCanvas canvas={canvas} />
|
|
95
|
+
</div>
|
|
96
|
+
<VerticalBar
|
|
97
|
+
value={rightValue}
|
|
98
|
+
max={rightMax}
|
|
99
|
+
side="right"
|
|
100
|
+
/>
|
|
101
|
+
<BottomBar tapeUtilization={0.67} playheadPosition={0.8} isPlaying={true} />
|
|
102
|
+
</main>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
39
106
|
function GameCanvas({ canvas }: { canvas: HTMLCanvasElement }) {
|
|
40
107
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
41
108
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { debugState } from "../state.ts";
|
|
2
|
+
|
|
3
|
+
type TopBarProps = {
|
|
4
|
+
leftLabel: string;
|
|
5
|
+
rightLabel: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function TopBar({ leftLabel, rightLabel }: TopBarProps) {
|
|
9
|
+
const fps = debugState.fps.value;
|
|
10
|
+
const frameNumber = debugState.frameNumber.value;
|
|
11
|
+
const rtt = debugState.netStatus.value.rtt;
|
|
12
|
+
const isOnline = debugState.netStatus.value.peers.length > 0;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="top-bar">
|
|
16
|
+
<span className="top-bar-side-label">{leftLabel}</span>
|
|
17
|
+
<div className="top-bar-center">
|
|
18
|
+
<div className="top-bar-item">
|
|
19
|
+
<span className="top-bar-label">FPS</span>
|
|
20
|
+
<span className="top-bar-value">{fps}</span>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="top-bar-item">
|
|
23
|
+
<span className="top-bar-label">Frame</span>
|
|
24
|
+
<span className="top-bar-value">{frameNumber}</span>
|
|
25
|
+
</div>
|
|
26
|
+
{isOnline && rtt !== null && (
|
|
27
|
+
<div className="top-bar-item">
|
|
28
|
+
<span className="top-bar-label">Ping</span>
|
|
29
|
+
<span className="top-bar-value">{rtt}ms</span>
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
<span className="top-bar-side-label">{rightLabel}</span>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type VerticalBarProps = {
|
|
2
|
+
value: number;
|
|
3
|
+
max: number;
|
|
4
|
+
side: "left" | "right";
|
|
5
|
+
color?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function VerticalBar({
|
|
9
|
+
value,
|
|
10
|
+
max,
|
|
11
|
+
side,
|
|
12
|
+
color = "#4a9eff",
|
|
13
|
+
}: VerticalBarProps) {
|
|
14
|
+
const percentage = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className={`${side}-bar`}>
|
|
18
|
+
<div className="vertical-bar">
|
|
19
|
+
<div
|
|
20
|
+
className="vertical-bar-fill"
|
|
21
|
+
style={{ height: `${percentage}%`, background: color }}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
package/src/debugui/state.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type { Log } from "../netcode/logs.ts";
|
|
|
8
8
|
|
|
9
9
|
export type FrameNumber = number;
|
|
10
10
|
|
|
11
|
+
export type LayoutMode = "off" | "letterboxed" | "full";
|
|
12
|
+
|
|
11
13
|
export type Peer = {
|
|
12
14
|
id: string;
|
|
13
15
|
nickname: string;
|
|
@@ -24,14 +26,22 @@ export type NetStatus = {
|
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
export type DebugState = {
|
|
27
|
-
|
|
29
|
+
layoutMode: Signal<LayoutMode>;
|
|
30
|
+
isVisible: ReadonlySignal<boolean>;
|
|
28
31
|
netStatus: Signal<NetStatus>;
|
|
29
32
|
logs: Signal<Log[]>;
|
|
30
33
|
peer: ReadonlySignal<Peer | null>;
|
|
31
34
|
advantage: ReadonlySignal<number | null>;
|
|
35
|
+
// Metrics for letterboxed layout
|
|
36
|
+
fps: Signal<number>;
|
|
37
|
+
frameTime: Signal<number>; // ms per frame
|
|
38
|
+
snapshotSize: Signal<number>; // bytes
|
|
39
|
+
frameNumber: Signal<number>;
|
|
40
|
+
// HMR flash indicator
|
|
41
|
+
hmrFlash: Signal<boolean>;
|
|
32
42
|
};
|
|
33
43
|
|
|
34
|
-
const
|
|
44
|
+
const layoutMode = signal<LayoutMode>("off");
|
|
35
45
|
const netStatus = signal<NetStatus>({
|
|
36
46
|
ourId: null,
|
|
37
47
|
remoteId: null,
|
|
@@ -39,10 +49,18 @@ const netStatus = signal<NetStatus>({
|
|
|
39
49
|
peers: [],
|
|
40
50
|
});
|
|
41
51
|
const logs = signal<Log[]>([]);
|
|
52
|
+
const fps = signal(0);
|
|
53
|
+
const frameTime = signal(0);
|
|
54
|
+
const snapshotSize = signal(0);
|
|
55
|
+
const frameNumber = signal(0);
|
|
56
|
+
const hmrFlash = signal(false);
|
|
42
57
|
|
|
43
58
|
export const debugState: DebugState = {
|
|
44
|
-
/**
|
|
45
|
-
|
|
59
|
+
/** Layout mode: off, letterboxed, or full */
|
|
60
|
+
layoutMode,
|
|
61
|
+
|
|
62
|
+
/** Whether debug UI is visible (derived from layoutMode) */
|
|
63
|
+
isVisible: computed(() => layoutMode.value !== "off"),
|
|
46
64
|
|
|
47
65
|
/** Network status */
|
|
48
66
|
netStatus,
|
|
@@ -58,8 +76,62 @@ export const debugState: DebugState = {
|
|
|
58
76
|
const peer = netStatus.value.peers[0];
|
|
59
77
|
return peer ? peer.seq - peer.ack : null;
|
|
60
78
|
}),
|
|
79
|
+
|
|
80
|
+
/** Metrics for letterboxed layout */
|
|
81
|
+
fps,
|
|
82
|
+
frameTime,
|
|
83
|
+
snapshotSize,
|
|
84
|
+
frameNumber,
|
|
85
|
+
|
|
86
|
+
/** HMR flash indicator */
|
|
87
|
+
hmrFlash,
|
|
61
88
|
};
|
|
62
89
|
|
|
90
|
+
/** Cycle through layout modes: off -> letterboxed -> full -> off */
|
|
91
|
+
export function cycleLayout(): void {
|
|
92
|
+
const current = layoutMode.value;
|
|
93
|
+
if (current === "off") {
|
|
94
|
+
layoutMode.value = "letterboxed";
|
|
95
|
+
} else if (current === "letterboxed") {
|
|
96
|
+
layoutMode.value = "full";
|
|
97
|
+
} else {
|
|
98
|
+
layoutMode.value = "off";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let hmrFlashQueued = false;
|
|
103
|
+
|
|
104
|
+
/** Trigger HMR flash (only when debug UI is visible) */
|
|
105
|
+
export function triggerHmrFlash(): void {
|
|
106
|
+
if (!debugState.isVisible.value) return;
|
|
107
|
+
|
|
108
|
+
// If window doesn't have focus, queue the flash for when focus returns
|
|
109
|
+
if (!document.hasFocus()) {
|
|
110
|
+
if (!hmrFlashQueued) {
|
|
111
|
+
hmrFlashQueued = true;
|
|
112
|
+
window.addEventListener("focus", onWindowFocus);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
doFlash();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function onWindowFocus(): void {
|
|
121
|
+
if (hmrFlashQueued) {
|
|
122
|
+
hmrFlashQueued = false;
|
|
123
|
+
window.removeEventListener("focus", onWindowFocus);
|
|
124
|
+
doFlash();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function doFlash(): void {
|
|
129
|
+
debugState.hmrFlash.value = true;
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
debugState.hmrFlash.value = false;
|
|
132
|
+
}, 300);
|
|
133
|
+
}
|
|
134
|
+
|
|
63
135
|
export function addLog(log: Log): void {
|
|
64
136
|
debugState.logs.value = [...debugState.logs.value, log];
|
|
65
137
|
}
|
|
@@ -116,7 +188,7 @@ export function clearLogs(): void {
|
|
|
116
188
|
}
|
|
117
189
|
|
|
118
190
|
export function resetState(): void {
|
|
119
|
-
debugState.
|
|
191
|
+
debugState.layoutMode.value = "off";
|
|
120
192
|
debugState.logs.value = [];
|
|
121
193
|
debugState.netStatus.value = {
|
|
122
194
|
ourId: null,
|
|
@@ -124,4 +196,9 @@ export function resetState(): void {
|
|
|
124
196
|
rtt: null,
|
|
125
197
|
peers: [],
|
|
126
198
|
};
|
|
199
|
+
debugState.fps.value = 0;
|
|
200
|
+
debugState.frameTime.value = 0;
|
|
201
|
+
debugState.snapshotSize.value = 0;
|
|
202
|
+
debugState.frameNumber.value = 0;
|
|
203
|
+
debugState.hmrFlash.value = false;
|
|
127
204
|
}
|