@bloopjs/web 0.0.95 → 0.0.97

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.
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/netcode/reconcile.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAiBrC,wBAAsB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC5E"}
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/netcode/reconcile.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAiBrC,wBAAsB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA0C5E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloopjs/web",
3
- "version": "0.0.95",
3
+ "version": "0.0.97",
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.95",
37
- "@bloopjs/engine": "0.0.95",
36
+ "@bloopjs/bloop": "0.0.97",
37
+ "@bloopjs/engine": "0.0.97",
38
38
  "@preact/signals": "^1.3.1",
39
39
  "partysocket": "^1.1.6",
40
40
  "preact": "^10.25.4"
package/src/App.ts CHANGED
@@ -379,6 +379,8 @@ export class App {
379
379
 
380
380
  // Update tape playback state
381
381
  debugState.isPlaying.value = !this.sim.isPaused;
382
+ debugState.isRecording.value = this.sim.isRecording;
383
+ debugState.isReplaying.value = this.sim.isReplaying;
382
384
  if (this.sim.hasHistory && debugState.tapeFrameCount.value > 0) {
383
385
  const currentFrame = this.sim.time.frame;
384
386
  const startFrame = debugState.tapeStartFrame.value;
@@ -2,6 +2,52 @@ import { useRef, useCallback } from "preact/hooks";
2
2
  import { debugState } from "../state.ts";
3
3
  import { LoadTapeDialog } from "./LoadTapeDialog.tsx";
4
4
 
5
+ // Simple SVG icons for playbar
6
+ const iconProps = { width: 14, height: 14, viewBox: "0 0 24 24", fill: "currentColor" };
7
+
8
+ const Icons = {
9
+ jumpBack: (
10
+ <svg {...iconProps}>
11
+ <path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" />
12
+ </svg>
13
+ ),
14
+ stepBack: (
15
+ <svg {...iconProps}>
16
+ <path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
17
+ </svg>
18
+ ),
19
+ play: (
20
+ <svg {...iconProps}>
21
+ <path d="M8 5v14l11-7z" />
22
+ </svg>
23
+ ),
24
+ pause: (
25
+ <svg {...iconProps}>
26
+ <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
27
+ </svg>
28
+ ),
29
+ stepForward: (
30
+ <svg {...iconProps}>
31
+ <path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
32
+ </svg>
33
+ ),
34
+ jumpForward: (
35
+ <svg {...iconProps}>
36
+ <path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z" />
37
+ </svg>
38
+ ),
39
+ save: (
40
+ <svg {...iconProps}>
41
+ <path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z" />
42
+ </svg>
43
+ ),
44
+ load: (
45
+ <svg {...iconProps}>
46
+ <path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
47
+ </svg>
48
+ ),
49
+ };
50
+
5
51
  /** Hook that returns handlers for repeat-on-hold behavior with initial debounce */
6
52
  function useRepeatOnHold(action: () => void) {
7
53
  const rafId = useRef<number | null>(null);
@@ -81,6 +127,8 @@ function useSeekDrag(onSeek: (ratio: number) => void) {
81
127
 
82
128
  export function BottomBar() {
83
129
  const isPlaying = debugState.isPlaying.value;
130
+ const isRecording = debugState.isRecording.value;
131
+ const isReplaying = debugState.isReplaying.value;
84
132
  const tapeUtilization = debugState.tapeUtilization.value;
85
133
  const playheadPosition = debugState.playheadPosition.value;
86
134
 
@@ -126,44 +174,57 @@ export function BottomBar() {
126
174
  return (
127
175
  <div className="bottom-bar">
128
176
  <div className="playbar-controls">
177
+ {isRecording && (
178
+ <span className="recording-indicator" title="Recording">
179
+ <span className="recording-dot" />
180
+ <span className="recording-label">REC</span>
181
+ </span>
182
+ )}
183
+ {isReplaying && (
184
+ <span className="replay-indicator" title="Replaying tape">
185
+ REPLAY
186
+ </span>
187
+ )}
129
188
  <button className="playbar-btn jump-back" {...jumpBackRepeat}>
130
- {"<<"}
189
+ {Icons.jumpBack}
131
190
  <span className="tooltip tooltip-left">
132
191
  Jump back <kbd>4</kbd>
133
192
  </span>
134
193
  </button>
135
194
  <button className="playbar-btn step-back" {...stepBackRepeat}>
136
- {"<"}
195
+ {Icons.stepBack}
137
196
  <span className="tooltip">
138
197
  Step back <kbd>5</kbd>
139
198
  </span>
140
199
  </button>
141
200
  <button className="playbar-btn play-pause" onClick={handlePlayPause}>
142
- {isPlaying ? "||" : ">"}
201
+ {isPlaying ? Icons.pause : Icons.play}
143
202
  <span className="tooltip">
144
203
  {isPlaying ? "Pause" : "Play"} <kbd>6</kbd>
145
204
  </span>
146
205
  </button>
147
206
  <button className="playbar-btn step-forward" {...stepForwardRepeat}>
148
- {">"}
207
+ {Icons.stepForward}
149
208
  <span className="tooltip">
150
209
  Step forward <kbd>7</kbd>
151
210
  </span>
152
211
  </button>
153
212
  <button className="playbar-btn jump-forward" {...jumpForwardRepeat}>
154
- {">>"}
213
+ {Icons.jumpForward}
155
214
  <span className="tooltip">
156
215
  Jump forward <kbd>8</kbd>
157
216
  </span>
158
217
  </button>
159
218
  <button className="playbar-btn save-tape-btn" onClick={handleSaveTapeClick}>
160
- Save
219
+ {Icons.save}
220
+ <span className="btn-label">Save</span>
161
221
  <span className="tooltip">
162
222
  Save tape <kbd>Cmd+S</kbd>
163
223
  </span>
164
224
  </button>
165
225
  <button className="playbar-btn load-tape-btn" onClick={handleLoadTapeClick}>
166
- Load
226
+ {Icons.load}
227
+ <span className="btn-label">Load</span>
167
228
  <span className="tooltip">Load tape</span>
168
229
  </button>
169
230
  </div>
@@ -42,6 +42,8 @@ export type DebugState = {
42
42
  hmrFlash: Signal<boolean>;
43
43
  // Tape playback state
44
44
  isPlaying: Signal<boolean>;
45
+ isRecording: Signal<boolean>; // whether sim is currently recording
46
+ isReplaying: Signal<boolean>; // whether sim is replaying a tape
45
47
  tapeUtilization: Signal<number>; // 0-1, how full the tape buffer is
46
48
  playheadPosition: Signal<number>; // 0-1, current position in tape
47
49
  tapeStartFrame: Signal<number>; // first frame in tape
@@ -77,6 +79,8 @@ const hmrFlash = signal(false);
77
79
 
78
80
  // Tape playback state
79
81
  const isPlaying = signal(true);
82
+ const isRecording = signal(false);
83
+ const isReplaying = signal(false);
80
84
  const tapeUtilization = signal(0);
81
85
  const playheadPosition = signal(0);
82
86
  const tapeStartFrame = signal(0);
@@ -130,6 +134,8 @@ export const debugState: DebugState = {
130
134
 
131
135
  /** Tape playback state */
132
136
  isPlaying,
137
+ isRecording,
138
+ isReplaying,
133
139
  tapeUtilization,
134
140
  playheadPosition,
135
141
  tapeStartFrame,
@@ -267,6 +273,8 @@ export function resetState(): void {
267
273
  debugState.hmrFlash.value = false;
268
274
  // Tape state
269
275
  debugState.isPlaying.value = true;
276
+ debugState.isRecording.value = false;
277
+ debugState.isReplaying.value = false;
270
278
  debugState.tapeUtilization.value = 0;
271
279
  debugState.playheadPosition.value = 0;
272
280
  debugState.tapeStartFrame.value = 0;
@@ -270,6 +270,73 @@ export const styles = /*css*/ `
270
270
  flex-shrink: 0;
271
271
  }
272
272
 
273
+ /* Recording indicator - mobile: just the dot */
274
+ .recording-indicator {
275
+ display: flex;
276
+ align-items: center;
277
+ margin-right: 4px;
278
+ }
279
+
280
+ .recording-indicator .recording-label {
281
+ display: none;
282
+ }
283
+
284
+ .recording-dot {
285
+ width: 10px;
286
+ height: 10px;
287
+ background: #ff4444;
288
+ border-radius: 50%;
289
+ animation: recording-pulse 1s ease-in-out infinite;
290
+ }
291
+
292
+ @keyframes recording-pulse {
293
+ 0%, 100% { opacity: 1; }
294
+ 50% { opacity: 0.4; }
295
+ }
296
+
297
+ /* Replay indicator - mobile: hidden */
298
+ .replay-indicator {
299
+ display: none;
300
+ }
301
+
302
+ /* Desktop: full indicators with text */
303
+ @media (min-width: 769px) {
304
+ .recording-indicator {
305
+ gap: 4px;
306
+ padding: 2px 6px;
307
+ background: rgba(255, 0, 0, 0.2);
308
+ border: 1px solid #ff4444;
309
+ border-radius: 3px;
310
+ }
311
+
312
+ .recording-indicator .recording-label {
313
+ display: inline;
314
+ color: #ff4444;
315
+ font-size: 10px;
316
+ font-weight: bold;
317
+ font-family: monospace;
318
+ }
319
+
320
+ .recording-dot {
321
+ width: 8px;
322
+ height: 8px;
323
+ }
324
+
325
+ .replay-indicator {
326
+ display: flex;
327
+ align-items: center;
328
+ padding: 2px 6px;
329
+ background: rgba(100, 100, 255, 0.2);
330
+ border: 1px solid #6666ff;
331
+ border-radius: 3px;
332
+ color: #6666ff;
333
+ font-size: 10px;
334
+ font-weight: bold;
335
+ font-family: monospace;
336
+ margin-right: 4px;
337
+ }
338
+ }
339
+
273
340
  /* Mobile-first: hide step/jump buttons */
274
341
  .playbar-btn.jump-back,
275
342
  .playbar-btn.step-back,
@@ -308,14 +375,33 @@ export const styles = /*css*/ `
308
375
  position: relative;
309
376
  }
310
377
 
311
- /* Desktop: smaller buttons */
378
+ /* Desktop: sized to match indicators */
312
379
  @media (min-width: 769px) {
313
380
  .playbar-btn {
314
- width: 1.5vh;
315
- height: 1.5vh;
316
- min-width: 18px;
317
- min-height: 18px;
318
- font-size: 10px;
381
+ width: 24px;
382
+ height: 24px;
383
+ min-width: 24px;
384
+ min-height: 24px;
385
+ }
386
+
387
+ /* Save/Load buttons need room for icon + text */
388
+ .playbar-btn.save-tape-btn,
389
+ .playbar-btn.load-tape-btn {
390
+ width: auto;
391
+ padding: 0 6px;
392
+ gap: 4px;
393
+ }
394
+ }
395
+
396
+ /* Mobile-first: hide button text labels, show only icons */
397
+ .btn-label {
398
+ display: none;
399
+ }
400
+
401
+ /* Desktop: show button text labels */
402
+ @media (min-width: 769px) {
403
+ .btn-label {
404
+ display: inline;
319
405
  }
320
406
  }
321
407
 
@@ -19,6 +19,9 @@ const actual = {
19
19
  export async function reconcile(app: App, signal: AbortSignal): Promise<void> {
20
20
  // Process packets and send our state each frame
21
21
  app.beforeFrame.subscribe((_frame) => {
22
+ // Skip network processing during tape replay
23
+ if (app.sim.isReplaying) return;
24
+
22
25
  if (!app.game.context.net.isInSession) {
23
26
  return;
24
27
  }
@@ -37,6 +40,12 @@ export async function reconcile(app: App, signal: AbortSignal): Promise<void> {
37
40
  };
38
41
 
39
42
  while (!signal.aborted) {
43
+ // Skip room join checks during tape replay
44
+ if (app.sim.isReplaying) {
45
+ await sleep(150);
46
+ continue;
47
+ }
48
+
40
49
  const { net } = app.game.context;
41
50
  if (net.wantsRoomCode && actual.roomCode !== net.wantsRoomCode) {
42
51
  console.log("[netcode] wants a room code", {