@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.
- package/dist/App.d.ts.map +1 -1
- package/dist/debugui/components/BottomBar.d.ts.map +1 -1
- package/dist/debugui/state.d.ts +2 -0
- 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 +195 -16
- package/dist/mod.js.map +9 -9
- package/dist/netcode/reconcile.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/App.ts +2 -0
- package/src/debugui/components/BottomBar.tsx +68 -7
- package/src/debugui/state.ts +8 -0
- package/src/debugui/styles.ts +92 -6
- package/src/netcode/reconcile.ts +9 -0
|
@@ -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,
|
|
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.
|
|
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.
|
|
37
|
-
"@bloopjs/engine": "0.0.
|
|
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
|
-
|
|
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
|
-
|
|
226
|
+
{Icons.load}
|
|
227
|
+
<span className="btn-label">Load</span>
|
|
167
228
|
<span className="tooltip">Load tape</span>
|
|
168
229
|
</button>
|
|
169
230
|
</div>
|
package/src/debugui/state.ts
CHANGED
|
@@ -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;
|
package/src/debugui/styles.ts
CHANGED
|
@@ -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:
|
|
378
|
+
/* Desktop: sized to match indicators */
|
|
312
379
|
@media (min-width: 769px) {
|
|
313
380
|
.playbar-btn {
|
|
314
|
-
width:
|
|
315
|
-
height:
|
|
316
|
-
min-width:
|
|
317
|
-
min-height:
|
|
318
|
-
|
|
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
|
|
package/src/netcode/reconcile.ts
CHANGED
|
@@ -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", {
|