@bloopjs/web 0.0.57 → 0.0.59

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloopjs/web",
3
- "version": "0.0.57",
3
+ "version": "0.0.59",
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.57",
37
- "@bloopjs/engine": "0.0.57",
36
+ "@bloopjs/bloop": "0.0.59",
37
+ "@bloopjs/engine": "0.0.59",
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,7 +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 } from "./debugui/state.ts";
10
+ import { debugState, triggerHmrFlash } from "./debugui/state.ts";
11
11
 
12
12
  export type StartOptions = {
13
13
  /** A bloop game instance */
@@ -131,6 +131,8 @@ export class App {
131
131
  beforeFrame: ReturnType<typeof createListener> = createListener<[number]>();
132
132
  /** Event listeners for after a frame is processed */
133
133
  afterFrame: ReturnType<typeof createListener> = createListener<[number]>();
134
+ /** Event listeners for HMR events */
135
+ onHmr: ReturnType<typeof createListener> = createListener<[HmrEvent]>();
134
136
 
135
137
  /** Subscribe to the browser events and start the render loop */
136
138
  subscribe(): void {
@@ -276,6 +278,7 @@ export class App {
276
278
  this.sim.unmount();
277
279
  this.beforeFrame.unsubscribeAll();
278
280
  this.afterFrame.unsubscribeAll();
281
+ this.onHmr.unsubscribeAll();
279
282
  this.#debugUi?.unmount();
280
283
  }
281
284
 
@@ -288,10 +291,14 @@ export class App {
288
291
  * import.meta.hot?.accept("./game", async (newModule) => {
289
292
  * await app.acceptHmr(newModule?.game, {
290
293
  * wasmUrl: monorepoWasmUrl,
294
+ * files: ["./game"],
291
295
  * });
292
296
  * ```
293
297
  */
294
- async acceptHmr(module: any, opts?: Partial<MountOpts>): Promise<void> {
298
+ async acceptHmr(
299
+ module: any,
300
+ opts?: Partial<MountOpts> & { files?: string[] },
301
+ ): Promise<void> {
295
302
  const game = (module.game ?? module) as Bloop<any>;
296
303
  if (!game.hooks) {
297
304
  throw new Error(
@@ -310,6 +317,10 @@ export class App {
310
317
  this.sim.unmount();
311
318
  this.sim = sim;
312
319
  this.game = game;
320
+
321
+ // Trigger HMR flash and notify listeners
322
+ triggerHmrFlash();
323
+ this.onHmr.notify({ files: opts?.files ?? [] });
313
324
  }
314
325
  }
315
326
 
@@ -343,3 +354,7 @@ function createListener<T extends any[]>(): {
343
354
  }
344
355
 
345
356
  export type UnsubscribeFn = () => void;
357
+
358
+ export type HmrEvent = {
359
+ files: string[];
360
+ };
@@ -59,6 +59,7 @@ function LetterboxedLayout({ canvas }: { canvas: HTMLCanvasElement }) {
59
59
  const advantage = debugState.advantage.value ?? 0;
60
60
  const frameTime = debugState.frameTime.value;
61
61
  const snapshotSize = debugState.snapshotSize.value;
62
+ const hmrFlash = debugState.hmrFlash.value;
62
63
 
63
64
  // Left bar: frame advantage (online) or frame time % (offline)
64
65
  const leftValue = isOnline ? Math.abs(advantage) : frameTime;
@@ -78,6 +79,8 @@ function LetterboxedLayout({ canvas }: { canvas: HTMLCanvasElement }) {
78
79
  const rightMax = isOnline ? 10 : 10000; // 10 frames rollback or 10KB
79
80
  const rightLabel = isOnline ? "RB" : "KB";
80
81
 
82
+ const gameClassName = hmrFlash ? "letterboxed-game hmr-flash" : "letterboxed-game";
83
+
81
84
  return (
82
85
  <main className="layout-letterboxed">
83
86
  <TopBar leftLabel={leftLabel} rightLabel={rightLabel} />
@@ -87,7 +90,7 @@ function LetterboxedLayout({ canvas }: { canvas: HTMLCanvasElement }) {
87
90
  side="left"
88
91
  color={leftColor}
89
92
  />
90
- <div className="letterboxed-game">
93
+ <div className={gameClassName}>
91
94
  <GameCanvas canvas={canvas} />
92
95
  </div>
93
96
  <VerticalBar
@@ -37,6 +37,8 @@ export type DebugState = {
37
37
  frameTime: Signal<number>; // ms per frame
38
38
  snapshotSize: Signal<number>; // bytes
39
39
  frameNumber: Signal<number>;
40
+ // HMR flash indicator
41
+ hmrFlash: Signal<boolean>;
40
42
  };
41
43
 
42
44
  const layoutMode = signal<LayoutMode>("off");
@@ -51,6 +53,7 @@ const fps = signal(0);
51
53
  const frameTime = signal(0);
52
54
  const snapshotSize = signal(0);
53
55
  const frameNumber = signal(0);
56
+ const hmrFlash = signal(false);
54
57
 
55
58
  export const debugState: DebugState = {
56
59
  /** Layout mode: off, letterboxed, or full */
@@ -79,6 +82,9 @@ export const debugState: DebugState = {
79
82
  frameTime,
80
83
  snapshotSize,
81
84
  frameNumber,
85
+
86
+ /** HMR flash indicator */
87
+ hmrFlash,
82
88
  };
83
89
 
84
90
  /** Cycle through layout modes: off -> letterboxed -> full -> off */
@@ -93,6 +99,39 @@ export function cycleLayout(): void {
93
99
  }
94
100
  }
95
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
+
96
135
  export function addLog(log: Log): void {
97
136
  debugState.logs.value = [...debugState.logs.value, log];
98
137
  }
@@ -161,4 +200,5 @@ export function resetState(): void {
161
200
  debugState.frameTime.value = 0;
162
201
  debugState.snapshotSize.value = 0;
163
202
  debugState.frameNumber.value = 0;
203
+ debugState.hmrFlash.value = false;
164
204
  }
@@ -256,6 +256,23 @@ export const styles = /*css*/ `
256
256
  display: block;
257
257
  }
258
258
 
259
+ .letterboxed-game {
260
+ position: relative;
261
+ }
262
+
263
+ .letterboxed-game.hmr-flash::after {
264
+ content: "";
265
+ position: absolute;
266
+ inset: 0;
267
+ pointer-events: none;
268
+ animation: hmr-pulse 0.3s ease-out forwards;
269
+ }
270
+
271
+ @keyframes hmr-pulse {
272
+ 0% { box-shadow: inset 0 0 0 36px #7b3fa0; }
273
+ 100% { box-shadow: inset 0 0 0 0 #7b3fa0; }
274
+ }
275
+
259
276
  .game {
260
277
  grid-area: game;
261
278
  border-radius: 8px;