zx-kit 0.15.0 → 0.16.1
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 +531 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/save.d.ts +122 -0
- package/dist/save.d.ts.map +1 -0
- package/dist/save.js +253 -0
- package/dist/save.js.map +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ The goal is simple: **it should look and sound like a Spectrum, but run like a m
|
|
|
30
30
|
- **AABB collision resolution** — sprite vs. sprite overlap, sprite vs. tile map wall resolution with directional hit flags
|
|
31
31
|
- **Keyboard input** — configurable key-repeat, single-consume action flags, instant state reset on phase transitions
|
|
32
32
|
- **ZX-style UI widgets** — progress bars with managed lifetime, boxes, frames, panel titles
|
|
33
|
+
- **Typed save / load** — persistent saves via `localStorage` with schema versioning, migrations, slot enumeration, in-memory throttling, and discriminated Result types for every failure mode
|
|
33
34
|
- **Zero dependencies** — only Web platform APIs: `Canvas`, `Web Audio`, `KeyboardEvent`
|
|
34
35
|
- **Tree-shakeable** — `sideEffects: false`, so unused modules are dropped from your production bundle
|
|
35
36
|
- **TypeScript-first** — strict mode, full `.d.ts` declarations, no `any`
|
|
@@ -128,6 +129,406 @@ requestAnimationFrame(loop)
|
|
|
128
129
|
|
|
129
130
|
---
|
|
130
131
|
|
|
132
|
+
## Getting Started — Build Your First Game
|
|
133
|
+
|
|
134
|
+
This tutorial walks you through building a working game from scratch: a character you can move around the screen with arrow keys, animated walking frames, and a sound effect on every step.
|
|
135
|
+
|
|
136
|
+
No prior game development experience needed. You need basic JavaScript/TypeScript knowledge (variables, functions, arrays).
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### What you will need
|
|
141
|
+
|
|
142
|
+
| Tool | Where to get it | Why |
|
|
143
|
+
|------|----------------|-----|
|
|
144
|
+
| **Node.js 18+** | [nodejs.org](https://nodejs.org) | Runs npm — the package manager we use to install zx-kit |
|
|
145
|
+
| **A code editor** | [code.visualstudio.com](https://code.visualstudio.com) (free) | Edits your source files |
|
|
146
|
+
| **A terminal** | Built into macOS/Linux; use PowerShell on Windows | Runs commands |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### Step 1 — Create the project
|
|
151
|
+
|
|
152
|
+
Open a terminal and run:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
mkdir my-first-game
|
|
156
|
+
cd my-first-game
|
|
157
|
+
npm init -y
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`npm init -y` creates a `package.json` file — the project's identity card. The `-y` flag accepts all defaults so you don't have to answer questions.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### Step 2 — Install dependencies
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm install zx-kit
|
|
168
|
+
npm install --save-dev vite
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- **zx-kit** — the game engine you are building with
|
|
172
|
+
- **vite** — a development server that reloads the browser whenever you save a file (installed as a dev tool, not part of your shipped game)
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### Step 3 — Configure package.json
|
|
177
|
+
|
|
178
|
+
Open `package.json` and replace it with the following. The two key additions are `"type": "module"` (enables modern JavaScript imports) and the `scripts` section (adds the `npm run dev` command):
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"name": "my-first-game",
|
|
183
|
+
"version": "1.0.0",
|
|
184
|
+
"type": "module",
|
|
185
|
+
"scripts": {
|
|
186
|
+
"dev": "vite",
|
|
187
|
+
"build": "vite build"
|
|
188
|
+
},
|
|
189
|
+
"dependencies": {
|
|
190
|
+
"zx-kit": "^0.15.0"
|
|
191
|
+
},
|
|
192
|
+
"devDependencies": {
|
|
193
|
+
"vite": "^6.0.0"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Step 4 — Create index.html
|
|
201
|
+
|
|
202
|
+
Create a file called `index.html` in your project root:
|
|
203
|
+
|
|
204
|
+
```html
|
|
205
|
+
<!DOCTYPE html>
|
|
206
|
+
<html lang="en">
|
|
207
|
+
<head>
|
|
208
|
+
<meta charset="UTF-8" />
|
|
209
|
+
<title>My First Game</title>
|
|
210
|
+
<style>
|
|
211
|
+
body {
|
|
212
|
+
margin: 0;
|
|
213
|
+
background: #000;
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
justify-content: center;
|
|
217
|
+
height: 100vh;
|
|
218
|
+
}
|
|
219
|
+
</style>
|
|
220
|
+
</head>
|
|
221
|
+
<body>
|
|
222
|
+
<canvas id="game"></canvas>
|
|
223
|
+
<script type="module" src="/src/main.ts"></script>
|
|
224
|
+
</body>
|
|
225
|
+
</html>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The `<canvas>` is the game screen. The `<script>` tag loads your game code.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### Step 5 — Create the game
|
|
233
|
+
|
|
234
|
+
Create the folder `src/` and inside it a file called `main.ts`. We will build it one piece at a time.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
#### 5a — Canvas setup
|
|
239
|
+
|
|
240
|
+
A canvas element is just a rectangle of pixels in the browser. `setupCanvas` configures it for pixel-perfect ZX Spectrum-style rendering and returns a drawing context you use to paint everything.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { setupCanvas, C, CELL } from 'zx-kit'
|
|
244
|
+
|
|
245
|
+
const canvas = document.getElementById('game') as HTMLCanvasElement
|
|
246
|
+
const ctx = setupCanvas(canvas, 4)
|
|
247
|
+
// The screen is 256 × 192 game pixels, scaled up 4× in the browser.
|
|
248
|
+
// From here on draw everything in game pixels — setupCanvas handles the scale.
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`C` is the color palette. `CELL` is the size of one sprite: 8 pixels.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
#### 5b — Define a sprite
|
|
256
|
+
|
|
257
|
+
Every character and object in zx-kit is an 8×8 pixel bitmap. You define it as 8 numbers, one per row. Each number is 8 bits — one bit per pixel. Bit 7 is the leftmost pixel.
|
|
258
|
+
|
|
259
|
+
The binary literal `0b00111100` is the same as the number `60`, but written out as ones and zeros so you can see the pixel pattern directly.
|
|
260
|
+
|
|
261
|
+
We will use two walking frames so the character animates when it moves:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
// 76543210 ← bit position (7 = leftmost pixel)
|
|
265
|
+
const WALK_A = new Uint8Array([
|
|
266
|
+
0b00111100, // ..####.. head
|
|
267
|
+
0b01111110, // .######.
|
|
268
|
+
0b00011000, // ...##... neck
|
|
269
|
+
0b01111110, // .######. arms + body
|
|
270
|
+
0b00011000, // ...##... waist
|
|
271
|
+
0b01011010, // .#.##.#. legs apart
|
|
272
|
+
0b01000010, // .#....#. feet
|
|
273
|
+
0b00000000, // ........
|
|
274
|
+
])
|
|
275
|
+
|
|
276
|
+
const WALK_B = new Uint8Array([
|
|
277
|
+
0b00111100, // ..####.. head
|
|
278
|
+
0b01111110, // .######.
|
|
279
|
+
0b00011000, // ...##... neck
|
|
280
|
+
0b01111110, // .######. arms + body
|
|
281
|
+
0b00011000, // ...##... waist
|
|
282
|
+
0b00111100, // ..####.. legs together
|
|
283
|
+
0b00011000, // ...##...
|
|
284
|
+
0b00000000, // ........
|
|
285
|
+
])
|
|
286
|
+
|
|
287
|
+
const FRAMES = [WALK_A, WALK_B] // frame 0 = legs apart, frame 1 = legs together
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
#### 5c — Input
|
|
293
|
+
|
|
294
|
+
`initInput()` attaches keyboard listeners. Call it once at startup, not inside the game loop.
|
|
295
|
+
|
|
296
|
+
`isHeld(key)` returns `true` while a key is pressed. We use it to check whether an arrow key is being held down each frame.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import { initInput, isHeld } from 'zx-kit'
|
|
300
|
+
|
|
301
|
+
initInput()
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
#### 5d — Audio
|
|
307
|
+
|
|
308
|
+
Browsers will not play any sound until the user has interacted with the page (clicked, tapped, or pressed a key). This is a browser security rule — there is nothing we can do to bypass it. The pattern below waits for the first keydown and initialises audio then:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { initAudio, getAudioContext, resumeAudio, beep } from 'zx-kit'
|
|
312
|
+
|
|
313
|
+
window.addEventListener('keydown', () => initAudio(0.3), { once: true })
|
|
314
|
+
// initAudio(0.3) = master volume 30%. Called at most once thanks to { once: true }.
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
#### 5e — Player state and animation
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import { createAnimation, tickAnimation } from 'zx-kit'
|
|
323
|
+
|
|
324
|
+
let px = 120 // player x position in game pixels
|
|
325
|
+
let py = 88 // player y position
|
|
326
|
+
const SPEED = 60 // pixels per second
|
|
327
|
+
|
|
328
|
+
// A 2-frame looping animation, 150ms per frame = one full step every 300ms
|
|
329
|
+
const walkAnim = createAnimation(2, 150)
|
|
330
|
+
let stepTimer = 0 // footstep sound timer
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
#### 5f — The game loop
|
|
336
|
+
|
|
337
|
+
Every frame the browser calls `loop`. Inside, we:
|
|
338
|
+
|
|
339
|
+
1. Calculate `dt` — how many milliseconds passed since last frame
|
|
340
|
+
2. Move the player based on held keys
|
|
341
|
+
3. Keep the player inside the screen
|
|
342
|
+
4. Pick the right animation frame
|
|
343
|
+
5. Play a footstep sound periodically while moving
|
|
344
|
+
6. Clear the screen and redraw everything
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import { drawSprite, drawText } from 'zx-kit'
|
|
348
|
+
|
|
349
|
+
let lastTime = 0
|
|
350
|
+
|
|
351
|
+
function loop(now: number): void {
|
|
352
|
+
const dt = Math.min(now - lastTime, 100)
|
|
353
|
+
// dt is milliseconds since the last frame (usually ~16ms at 60fps).
|
|
354
|
+
// We cap at 100ms so a background tab returning doesn't cause a huge position jump.
|
|
355
|
+
lastTime = now
|
|
356
|
+
|
|
357
|
+
// ── Move player ────────────────────────────────────────────────────────────
|
|
358
|
+
let moving = false
|
|
359
|
+
if (isHeld('ArrowRight')) { px += SPEED * dt / 1000; moving = true }
|
|
360
|
+
if (isHeld('ArrowLeft')) { px -= SPEED * dt / 1000; moving = true }
|
|
361
|
+
if (isHeld('ArrowDown')) { py += SPEED * dt / 1000; moving = true }
|
|
362
|
+
if (isHeld('ArrowUp')) { py -= SPEED * dt / 1000; moving = true }
|
|
363
|
+
// SPEED (60) is pixels per second. dt is milliseconds. Divide by 1000 to convert.
|
|
364
|
+
|
|
365
|
+
// Keep inside the 256×192 canvas
|
|
366
|
+
px = Math.max(0, Math.min(256 - CELL, px))
|
|
367
|
+
py = Math.max(0, Math.min(192 - CELL, py))
|
|
368
|
+
|
|
369
|
+
// ── Animation ──────────────────────────────────────────────────────────────
|
|
370
|
+
// tickAnimation advances the timer and returns the current frame index (0 or 1).
|
|
371
|
+
// When standing still we always use frame 0.
|
|
372
|
+
const frame = moving ? tickAnimation(walkAnim, dt) : 0
|
|
373
|
+
|
|
374
|
+
// ── Footstep sound ─────────────────────────────────────────────────────────
|
|
375
|
+
if (moving) {
|
|
376
|
+
stepTimer -= dt
|
|
377
|
+
if (stepTimer <= 0) {
|
|
378
|
+
stepTimer = 250 // play a sound every 250ms while moving
|
|
379
|
+
const audio = getAudioContext()
|
|
380
|
+
if (audio) {
|
|
381
|
+
resumeAudio() // un-suspend if the tab was hidden
|
|
382
|
+
beep(220, 30, audio.currentTime) // 220 Hz, 30ms — a short thud
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
stepTimer = 0 // reset so next movement starts immediately
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── Draw ───────────────────────────────────────────────────────────────────
|
|
390
|
+
ctx.fillStyle = C.BLACK
|
|
391
|
+
ctx.fillRect(0, 0, 256, 192) // clear the whole screen
|
|
392
|
+
|
|
393
|
+
drawSprite(ctx, FRAMES[frame], Math.round(px), Math.round(py), C.B_CYAN, C.BLACK)
|
|
394
|
+
// ↑ position ↑ ink color ↑ paper (background)
|
|
395
|
+
|
|
396
|
+
drawText(ctx, 'ARROW KEYS = MOVE', 8, 184, C.WHITE)
|
|
397
|
+
// drawText draws one ASCII character per 8px slot, left-to-right.
|
|
398
|
+
|
|
399
|
+
requestAnimationFrame(loop) // ask the browser to call us again next frame
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
requestAnimationFrame(loop) // kick off the first frame
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### Step 6 — Run the game
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
npm run dev
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Open [http://localhost:5173](http://localhost:5173) in your browser. Press an arrow key. Your character walks.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
### Complete file
|
|
418
|
+
|
|
419
|
+
The full `src/main.ts` all in one place:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import {
|
|
423
|
+
setupCanvas, C, CELL,
|
|
424
|
+
drawSprite, drawText,
|
|
425
|
+
initInput, isHeld,
|
|
426
|
+
initAudio, getAudioContext, resumeAudio, beep,
|
|
427
|
+
createAnimation, tickAnimation,
|
|
428
|
+
} from 'zx-kit'
|
|
429
|
+
|
|
430
|
+
// ── Canvas ────────────────────────────────────────────────────────────────────
|
|
431
|
+
const canvas = document.getElementById('game') as HTMLCanvasElement
|
|
432
|
+
const ctx = setupCanvas(canvas, 4)
|
|
433
|
+
|
|
434
|
+
// ── Sprites ───────────────────────────────────────────────────────────────────
|
|
435
|
+
const WALK_A = new Uint8Array([
|
|
436
|
+
0b00111100, // ..####.. head
|
|
437
|
+
0b01111110, // .######.
|
|
438
|
+
0b00011000, // ...##... neck
|
|
439
|
+
0b01111110, // .######. arms + body
|
|
440
|
+
0b00011000, // ...##... waist
|
|
441
|
+
0b01011010, // .#.##.#. legs apart
|
|
442
|
+
0b01000010, // .#....#. feet
|
|
443
|
+
0b00000000, // ........
|
|
444
|
+
])
|
|
445
|
+
|
|
446
|
+
const WALK_B = new Uint8Array([
|
|
447
|
+
0b00111100, // ..####.. head
|
|
448
|
+
0b01111110, // .######.
|
|
449
|
+
0b00011000, // ...##... neck
|
|
450
|
+
0b01111110, // .######. arms + body
|
|
451
|
+
0b00011000, // ...##... waist
|
|
452
|
+
0b00111100, // ..####.. legs together
|
|
453
|
+
0b00011000, // ...##...
|
|
454
|
+
0b00000000, // ........
|
|
455
|
+
])
|
|
456
|
+
|
|
457
|
+
const FRAMES = [WALK_A, WALK_B]
|
|
458
|
+
|
|
459
|
+
// ── Input ─────────────────────────────────────────────────────────────────────
|
|
460
|
+
initInput()
|
|
461
|
+
|
|
462
|
+
// ── Audio ─────────────────────────────────────────────────────────────────────
|
|
463
|
+
window.addEventListener('keydown', () => initAudio(0.3), { once: true })
|
|
464
|
+
|
|
465
|
+
// ── Player state ──────────────────────────────────────────────────────────────
|
|
466
|
+
let px = 120
|
|
467
|
+
let py = 88
|
|
468
|
+
const SPEED = 60 // pixels per second
|
|
469
|
+
|
|
470
|
+
const walkAnim = createAnimation(2, 150)
|
|
471
|
+
let stepTimer = 0
|
|
472
|
+
|
|
473
|
+
// ── Game loop ─────────────────────────────────────────────────────────────────
|
|
474
|
+
let lastTime = 0
|
|
475
|
+
|
|
476
|
+
function loop(now: number): void {
|
|
477
|
+
const dt = Math.min(now - lastTime, 100)
|
|
478
|
+
lastTime = now
|
|
479
|
+
|
|
480
|
+
let moving = false
|
|
481
|
+
if (isHeld('ArrowRight')) { px += SPEED * dt / 1000; moving = true }
|
|
482
|
+
if (isHeld('ArrowLeft')) { px -= SPEED * dt / 1000; moving = true }
|
|
483
|
+
if (isHeld('ArrowDown')) { py += SPEED * dt / 1000; moving = true }
|
|
484
|
+
if (isHeld('ArrowUp')) { py -= SPEED * dt / 1000; moving = true }
|
|
485
|
+
|
|
486
|
+
px = Math.max(0, Math.min(256 - CELL, px))
|
|
487
|
+
py = Math.max(0, Math.min(192 - CELL, py))
|
|
488
|
+
|
|
489
|
+
const frame = moving ? tickAnimation(walkAnim, dt) : 0
|
|
490
|
+
|
|
491
|
+
if (moving) {
|
|
492
|
+
stepTimer -= dt
|
|
493
|
+
if (stepTimer <= 0) {
|
|
494
|
+
stepTimer = 250
|
|
495
|
+
const audio = getAudioContext()
|
|
496
|
+
if (audio) { resumeAudio(); beep(220, 30, audio.currentTime) }
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
stepTimer = 0
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
ctx.fillStyle = C.BLACK
|
|
503
|
+
ctx.fillRect(0, 0, 256, 192)
|
|
504
|
+
|
|
505
|
+
drawSprite(ctx, FRAMES[frame], Math.round(px), Math.round(py), C.B_CYAN, C.BLACK)
|
|
506
|
+
drawText(ctx, 'ARROW KEYS = MOVE', 8, 184, C.WHITE)
|
|
507
|
+
|
|
508
|
+
requestAnimationFrame(loop)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
requestAnimationFrame(loop)
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### What to try next
|
|
517
|
+
|
|
518
|
+
**Change the sprite.** Edit the binary rows in `WALK_A` / `WALK_B` — each `1` is a pixel, each `0` is background. Draw a spaceship, a gem, or a face.
|
|
519
|
+
|
|
520
|
+
**Change the color.** Replace `C.B_CYAN` with any palette color: `C.B_GREEN`, `C.B_YELLOW`, `C.B_RED`, `C.B_MAGENTA`, `C.B_WHITE`. The full list is in the [palette reference](#palettets--color-constants).
|
|
521
|
+
|
|
522
|
+
**Add a second character.** Copy the player variables (`px2`, `py2`, `walkAnim2`) and add `W A S D` controls using `isHeld('w')` etc.
|
|
523
|
+
|
|
524
|
+
**Add obstacles.** Use `createTileMap` to place solid wall tiles and `resolveX` / `resolveY` to stop the player at them.
|
|
525
|
+
|
|
526
|
+
**Add chiptune music.** Call `playAY()` with a note array to play a three-channel melody — see [`ay.ts`](#ayts--ay-3-8912-melodik-audio).
|
|
527
|
+
|
|
528
|
+
**Study a complete game.** [Minefield](https://github.com/zrebec/minefield) is built entirely with zx-kit. Every mechanic in this tutorial — sprites, input, animation, audio, tilemap — appears there in a production context.
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
131
532
|
## Modules
|
|
132
533
|
|
|
133
534
|
| Module | What it provides |
|
|
@@ -142,6 +543,7 @@ requestAnimationFrame(loop)
|
|
|
142
543
|
| [`animation.ts`](#animationts--frame-timer--tween) | Frame-timer for sprite strips, position tween between two points |
|
|
143
544
|
| [`camera.ts`](#camerats--scrolling-camera) | Viewport that follows a target with lerp + deadzone, world-bounds clamping |
|
|
144
545
|
| [`scene.ts`](#scenets--scene-manager) | Stack-based scene manager with onEnter/onExit/onPause/onResume hooks |
|
|
546
|
+
| [`save.ts`](#savets--typed-save--load) | Typed save/load via callbacks, versioning + migrations, slot enumeration, throttling, Result types |
|
|
145
547
|
| [`tilemap.ts`](#tilemapts--tile-map-engine) | Scrollable maps, solid tiles, O(1) id-index, background swap |
|
|
146
548
|
| [`palette.ts`](#palettets--color-constants) | 15 Spectrum colors, `SpectrumColor` type, `CELL`, `SCALE` |
|
|
147
549
|
| [`font.ts`](#fontts--rom-bitmap-font) | 96-character ROM font, raw bitmap access |
|
|
@@ -1134,6 +1536,135 @@ Renders every scene from bottom to top. No-op on an empty manager.
|
|
|
1134
1536
|
|
|
1135
1537
|
---
|
|
1136
1538
|
|
|
1539
|
+
## `save.ts` — Typed Save / Load
|
|
1540
|
+
|
|
1541
|
+
Persistent save / load via `localStorage` with versioning, schema migration, slot enumeration, and in-memory throttling. The game declares its state shape through `serialize` / `deserialize` callbacks; the kit handles storage, namespacing, error mapping, and throttle timing. Every operation returns a discriminated Result type — `quota`, `disabled`, `corrupt`, `version_unsupported` and the rest are distinct, so the game can react to each failure mode if it cares.
|
|
1542
|
+
|
|
1543
|
+
```ts
|
|
1544
|
+
import {
|
|
1545
|
+
createSaveProfile, writeSave, writeSaveThrottled,
|
|
1546
|
+
readSave, readSaveLatest, saveExists, deleteSave, listSaves,
|
|
1547
|
+
} from 'zx-kit'
|
|
1548
|
+
|
|
1549
|
+
type MyGameSave = {
|
|
1550
|
+
score: number
|
|
1551
|
+
lives: number
|
|
1552
|
+
probed: string[] // Set<string> serialized to array
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const save = createSaveProfile<MyGameSave>({
|
|
1556
|
+
key: 'my-game',
|
|
1557
|
+
version: 1,
|
|
1558
|
+
serialize: () => ({
|
|
1559
|
+
score: game.score,
|
|
1560
|
+
lives: game.lives,
|
|
1561
|
+
probed: [...game.probedCells],
|
|
1562
|
+
}),
|
|
1563
|
+
deserialize: (data) => {
|
|
1564
|
+
game.score = data.score
|
|
1565
|
+
game.lives = data.lives
|
|
1566
|
+
game.probedCells = new Set(data.probed)
|
|
1567
|
+
},
|
|
1568
|
+
})
|
|
1569
|
+
|
|
1570
|
+
writeSave(save, 'manual') // immediate write to 'manual'
|
|
1571
|
+
writeSaveThrottled(save, 'auto', 5000) // skips if last 'auto' write < 5s ago
|
|
1572
|
+
readSaveLatest(save) // load newest slot, calls deserialize
|
|
1573
|
+
deleteSave(save, 'auto') // remove slot + clear its throttle entry
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
### Why callbacks, not a "save the whole state" snapshot
|
|
1577
|
+
|
|
1578
|
+
In emulators a full RAM dump round-trips losslessly because RAM is a byte array. JavaScript state is an object graph: `JSON.stringify(gameState)` silently corrupts `Set`, `Map`, class instances and circular references; a snapshot also persists transient runtime state (audio nodes, `requestAnimationFrame` IDs) that has no business surviving. Forcing the game to declare what's in a save via `serialize` keeps the kit state-agnostic and gives the game a place to convert non-JSON values back and forth.
|
|
1579
|
+
|
|
1580
|
+
### `SaveProfileConfig<T>` interface
|
|
1581
|
+
|
|
1582
|
+
| Field | Description |
|
|
1583
|
+
|-------|-------------|
|
|
1584
|
+
| `key` | Game key — used as namespace in storage. Unique per game. |
|
|
1585
|
+
| `version` | Current schema version. Increment when the shape of `T` changes. |
|
|
1586
|
+
| `serialize` | Returns the current game state as a JSON-safe `T`. |
|
|
1587
|
+
| `deserialize` | Applies a loaded `T` back to the game (side effect — the game owns the mutation). |
|
|
1588
|
+
| `migrate?` | `(data: unknown, fromVersion: number) => T` — runs when the loaded envelope is older than `version`. If absent and `fromVersion < version`, load fails with `version_unsupported`. |
|
|
1589
|
+
|
|
1590
|
+
### `SaveResult` / `LoadResult`
|
|
1591
|
+
|
|
1592
|
+
```ts
|
|
1593
|
+
type SaveResult =
|
|
1594
|
+
| { ok: true }
|
|
1595
|
+
| { ok: false, reason: 'quota' | 'disabled' | 'serialize_error' | 'throttled', error?: Error }
|
|
1596
|
+
|
|
1597
|
+
type LoadResult =
|
|
1598
|
+
| { ok: true, slot: string }
|
|
1599
|
+
| { ok: false, reason: 'not_found' | 'corrupt' | 'version_unsupported' | 'parse_error' | 'disabled', error?: Error }
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
`throttled` is not a true failure — it means the throttle interval hadn't elapsed and the write was skipped. Surfaced as `ok: false` so callers can distinguish skipping from a real success, but typically ignored.
|
|
1603
|
+
|
|
1604
|
+
### `createSaveProfile<T>(config): SaveProfile<T>`
|
|
1605
|
+
|
|
1606
|
+
Registers a save profile. Call once at startup and reuse the returned handle for every operation. The handle also carries in-memory throttle state (per-slot last-write timestamps).
|
|
1607
|
+
|
|
1608
|
+
### `writeSave(profile, slot?): SaveResult`
|
|
1609
|
+
|
|
1610
|
+
Writes immediately. Calls `serialize`, wraps the result as `{ version, timestamp, data }` and stores under `zxkit:<key>:<slot>`. Default slot is `'default'`.
|
|
1611
|
+
|
|
1612
|
+
### `writeSaveThrottled(profile, slot, minIntervalMs): SaveResult`
|
|
1613
|
+
|
|
1614
|
+
Writes only if at least `minIntervalMs` has elapsed since the last successful write to the same slot in this session. The first call to a given slot always proceeds — the throttle only applies once there's a prior write to compare against. Throttle state lives in memory; a page reload resets it.
|
|
1615
|
+
|
|
1616
|
+
### `readSave(profile, slot?): LoadResult`
|
|
1617
|
+
|
|
1618
|
+
Reads a slot, runs `migrate` if the stored version is older than the profile version, then calls `deserialize` with the result. On `ok`, the game state has been restored.
|
|
1619
|
+
|
|
1620
|
+
### `readSaveLatest(profile): LoadResult`
|
|
1621
|
+
|
|
1622
|
+
Enumerates every slot belonging to this profile's key and loads the one with the most recent `timestamp`. Returns `{ ok: false, reason: 'not_found' }` when no slots exist.
|
|
1623
|
+
|
|
1624
|
+
### `saveExists(profile, slot?): boolean`
|
|
1625
|
+
|
|
1626
|
+
True iff the slot key exists in storage. Does not validate envelope shape — use `readSave` for that.
|
|
1627
|
+
|
|
1628
|
+
### `deleteSave(profile, slot?): boolean`
|
|
1629
|
+
|
|
1630
|
+
Removes the slot. Also clears the slot's throttle entry, so the next `writeSaveThrottled` to that slot proceeds immediately. Returns `true` if a slot was actually removed.
|
|
1631
|
+
|
|
1632
|
+
### `listSaves(profile): SlotInfo[]`
|
|
1633
|
+
|
|
1634
|
+
Returns `{ name, timestamp, version, sizeBytes }[]` for every slot belonging to this profile. Corrupt or mis-shaped entries are silently skipped — they will surface as `corrupt` if loaded explicitly via `readSave`.
|
|
1635
|
+
|
|
1636
|
+
### Versioning and migration
|
|
1637
|
+
|
|
1638
|
+
Every saved payload carries `{ version, timestamp, data }`. On load, when the stored version is older than the profile's current version, `migrate` is called with the raw `data` and the version it was saved at. If the stored version is newer than the profile's, the load fails with `version_unsupported` — a downgrade cannot read forward.
|
|
1639
|
+
|
|
1640
|
+
```ts
|
|
1641
|
+
createSaveProfile({
|
|
1642
|
+
key: 'my-game',
|
|
1643
|
+
version: 3,
|
|
1644
|
+
migrate: (data, fromVersion) => {
|
|
1645
|
+
let d = data as Record<string, unknown>
|
|
1646
|
+
if (fromVersion < 2) d = { ...d, lives: 3 } // v1 → v2: gained 'lives'
|
|
1647
|
+
if (fromVersion < 3) d = { ...d, probed: [] } // v2 → v3: gained 'probed'
|
|
1648
|
+
return d as MyGameSave
|
|
1649
|
+
},
|
|
1650
|
+
deserialize: (data) => { /* always receives v3 shape */ },
|
|
1651
|
+
})
|
|
1652
|
+
```
|
|
1653
|
+
|
|
1654
|
+
If `migrate` throws, the read fails with `corrupt`.
|
|
1655
|
+
|
|
1656
|
+
### Slot naming — convention, not policy
|
|
1657
|
+
|
|
1658
|
+
The kit places no policy on slot names. A useful pattern for retro-style games:
|
|
1659
|
+
|
|
1660
|
+
- `'auto'` — written via `writeSaveThrottled` at meaningful game events (level complete, checkpoint, after a major state change).
|
|
1661
|
+
- `'manual'` — written via `writeSave` when the player hits a save key.
|
|
1662
|
+
- Load via `readSaveLatest` to pick whichever is newer.
|
|
1663
|
+
|
|
1664
|
+
This composes a "your last meaningful checkpoint is always available" UX without a load-slot menu.
|
|
1665
|
+
|
|
1666
|
+
---
|
|
1667
|
+
|
|
1137
1668
|
## `tilemap.ts` — Tile Map Engine
|
|
1138
1669
|
|
|
1139
1670
|
A scrollable, queryable tile map backed by an O(1) id-index. Tiles use the same 8×8 sprite format as `drawSprite`. Supports solid-tile collision queries, viewport-clipped rendering, and smart seasonal background swapping.
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA"}
|
package/dist/save.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for a game's save profile.
|
|
3
|
+
*
|
|
4
|
+
* @typeParam T - The JSON-safe shape of the saved payload. The game converts
|
|
5
|
+
* non-JSON values (Sets, Maps, class instances) inside `serialize` /
|
|
6
|
+
* `deserialize`.
|
|
7
|
+
*/
|
|
8
|
+
export interface SaveProfileConfig<T> {
|
|
9
|
+
/** Game key — used as namespace in storage. Must be unique per game. */
|
|
10
|
+
key: string;
|
|
11
|
+
/** Current schema version. Increment when the shape of T changes. */
|
|
12
|
+
version: number;
|
|
13
|
+
/** Returns the current game state as a JSON-safe value. */
|
|
14
|
+
serialize: () => T;
|
|
15
|
+
/** Applies a loaded snapshot back to the game (side effect). */
|
|
16
|
+
deserialize: (data: T) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Optional migration. Called when the loaded envelope's version is older
|
|
19
|
+
* than `version`. Receives the raw data and the version it was saved at,
|
|
20
|
+
* returns data shaped for the current version. If absent and an older
|
|
21
|
+
* version is encountered, the load fails with `version_unsupported`.
|
|
22
|
+
*/
|
|
23
|
+
migrate?: (data: unknown, fromVersion: number) => T;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returned by {@link createSaveProfile}. Carries config plus in-memory
|
|
27
|
+
* throttle state. Pass to all save/load operations.
|
|
28
|
+
*
|
|
29
|
+
* Throttle state lives only in memory — a page reload resets it, so the
|
|
30
|
+
* first {@link writeSaveThrottled} after reload always proceeds.
|
|
31
|
+
*/
|
|
32
|
+
export interface SaveProfile<T> {
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly version: number;
|
|
35
|
+
readonly config: SaveProfileConfig<T>;
|
|
36
|
+
/** Per-slot last successful write timestamp (ms). Internal. */
|
|
37
|
+
lastWrites: Map<string, number>;
|
|
38
|
+
}
|
|
39
|
+
/** Metadata for a single stored slot. */
|
|
40
|
+
export interface SlotInfo {
|
|
41
|
+
name: string;
|
|
42
|
+
timestamp: number;
|
|
43
|
+
version: number;
|
|
44
|
+
sizeBytes: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Result of a write operation.
|
|
48
|
+
*
|
|
49
|
+
* `throttled` indicates the write was intentionally skipped because the
|
|
50
|
+
* throttle interval had not elapsed — not a true failure. Callers that
|
|
51
|
+
* only care whether data hit storage can branch on `ok`; callers that
|
|
52
|
+
* want to distinguish "skipped" from "failed" can check `reason`.
|
|
53
|
+
*/
|
|
54
|
+
export type SaveResult = {
|
|
55
|
+
ok: true;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: 'quota' | 'disabled' | 'serialize_error' | 'throttled';
|
|
59
|
+
error?: Error;
|
|
60
|
+
};
|
|
61
|
+
/** Result of a read operation. On `ok: true`, `deserialize` has already been called. */
|
|
62
|
+
export type LoadResult = {
|
|
63
|
+
ok: true;
|
|
64
|
+
slot: string;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
reason: 'not_found' | 'corrupt' | 'version_unsupported' | 'parse_error' | 'disabled';
|
|
68
|
+
error?: Error;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Registers a save profile for a game. Call once at startup and reuse the
|
|
72
|
+
* returned handle for all save/load operations.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* const save = createSaveProfile<MinefieldSave>({
|
|
76
|
+
* key: 'minefield',
|
|
77
|
+
* version: 1,
|
|
78
|
+
* serialize: () => ({ score, lives, probed: [...probedCells] }),
|
|
79
|
+
* deserialize: (data) => { score = data.score; probedCells = new Set(data.probed) },
|
|
80
|
+
* })
|
|
81
|
+
*/
|
|
82
|
+
export declare function createSaveProfile<T>(config: SaveProfileConfig<T>): SaveProfile<T>;
|
|
83
|
+
/**
|
|
84
|
+
* Writes the current game state to a slot immediately. Calls `serialize`
|
|
85
|
+
* to obtain the payload, then stores `{ version, timestamp, data }` as JSON.
|
|
86
|
+
*
|
|
87
|
+
* @param slot - Slot name; defaults to `'default'`. Games typically use
|
|
88
|
+
* convention-based names like `'auto'` or `'manual'`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function writeSave<T>(profile: SaveProfile<T>, slot?: string): SaveResult;
|
|
91
|
+
/**
|
|
92
|
+
* Writes to `slot` only if at least `minIntervalMs` has elapsed since the
|
|
93
|
+
* last successful write to the same slot in this session. Otherwise returns
|
|
94
|
+
* `{ ok: false, reason: 'throttled' }`.
|
|
95
|
+
*
|
|
96
|
+
* Throttle state is in-memory; a page reload resets it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function writeSaveThrottled<T>(profile: SaveProfile<T>, slot: string, minIntervalMs: number): SaveResult;
|
|
99
|
+
/**
|
|
100
|
+
* Reads a slot, runs migration if needed, then calls `deserialize` with
|
|
101
|
+
* the resulting payload. On success the game state has been restored.
|
|
102
|
+
*/
|
|
103
|
+
export declare function readSave<T>(profile: SaveProfile<T>, slot?: string): LoadResult;
|
|
104
|
+
/**
|
|
105
|
+
* Reads whichever slot has the newest timestamp. Returns `not_found` if
|
|
106
|
+
* no slots exist for this profile's key.
|
|
107
|
+
*/
|
|
108
|
+
export declare function readSaveLatest<T>(profile: SaveProfile<T>): LoadResult;
|
|
109
|
+
/** True iff the slot exists in storage. Does not validate envelope shape. */
|
|
110
|
+
export declare function saveExists<T>(profile: SaveProfile<T>, slot?: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Removes a slot. Returns `true` if a slot was removed, `false` if it
|
|
113
|
+
* didn't exist or storage is unavailable.
|
|
114
|
+
*/
|
|
115
|
+
export declare function deleteSave<T>(profile: SaveProfile<T>, slot?: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Enumerates all slots that belong to this profile's key. Corrupt or
|
|
118
|
+
* mis-shaped entries are silently skipped — they will surface as `corrupt`
|
|
119
|
+
* if loaded explicitly via {@link readSave}.
|
|
120
|
+
*/
|
|
121
|
+
export declare function listSaves<T>(profile: SaveProfile<T>): SlotInfo[];
|
|
122
|
+
//# sourceMappingURL=save.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../src/save.ts"],"names":[],"mappings":"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"}
|
package/dist/save.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// ── Save: typed save/load with versioning, throttling, slot enumeration ─────
|
|
2
|
+
const NAMESPACE = 'zxkit';
|
|
3
|
+
const DEFAULT_SLOT = 'default';
|
|
4
|
+
function storageKey(profileKey, slot) {
|
|
5
|
+
return `${NAMESPACE}:${profileKey}:${slot}`;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the localStorage reference, returning null if unavailable
|
|
9
|
+
* (Node, SSR, private browsing where access itself throws, etc.).
|
|
10
|
+
*/
|
|
11
|
+
function getStorage() {
|
|
12
|
+
try {
|
|
13
|
+
if (typeof globalThis === 'undefined')
|
|
14
|
+
return null;
|
|
15
|
+
return globalThis.localStorage ?? null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isValidEnvelope(value) {
|
|
22
|
+
if (typeof value !== 'object' || value === null)
|
|
23
|
+
return false;
|
|
24
|
+
const v = value;
|
|
25
|
+
return (typeof v.version === 'number' &&
|
|
26
|
+
Number.isFinite(v.version) &&
|
|
27
|
+
typeof v.timestamp === 'number' &&
|
|
28
|
+
Number.isFinite(v.timestamp) &&
|
|
29
|
+
'data' in v);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Registers a save profile for a game. Call once at startup and reuse the
|
|
33
|
+
* returned handle for all save/load operations.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const save = createSaveProfile<MinefieldSave>({
|
|
37
|
+
* key: 'minefield',
|
|
38
|
+
* version: 1,
|
|
39
|
+
* serialize: () => ({ score, lives, probed: [...probedCells] }),
|
|
40
|
+
* deserialize: (data) => { score = data.score; probedCells = new Set(data.probed) },
|
|
41
|
+
* })
|
|
42
|
+
*/
|
|
43
|
+
export function createSaveProfile(config) {
|
|
44
|
+
return {
|
|
45
|
+
key: config.key,
|
|
46
|
+
version: config.version,
|
|
47
|
+
config,
|
|
48
|
+
lastWrites: new Map(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Writes the current game state to a slot immediately. Calls `serialize`
|
|
53
|
+
* to obtain the payload, then stores `{ version, timestamp, data }` as JSON.
|
|
54
|
+
*
|
|
55
|
+
* @param slot - Slot name; defaults to `'default'`. Games typically use
|
|
56
|
+
* convention-based names like `'auto'` or `'manual'`.
|
|
57
|
+
*/
|
|
58
|
+
export function writeSave(profile, slot = DEFAULT_SLOT) {
|
|
59
|
+
const storage = getStorage();
|
|
60
|
+
if (!storage)
|
|
61
|
+
return { ok: false, reason: 'disabled' };
|
|
62
|
+
let payload;
|
|
63
|
+
try {
|
|
64
|
+
payload = profile.config.serialize();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return { ok: false, reason: 'serialize_error', error: error };
|
|
68
|
+
}
|
|
69
|
+
const envelope = {
|
|
70
|
+
version: profile.version,
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
data: payload,
|
|
73
|
+
};
|
|
74
|
+
let serialized;
|
|
75
|
+
try {
|
|
76
|
+
serialized = JSON.stringify(envelope);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return { ok: false, reason: 'serialize_error', error: error };
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
storage.setItem(storageKey(profile.key, slot), serialized);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const err = error;
|
|
86
|
+
if (err.name === 'QuotaExceededError' || err.code === 22) {
|
|
87
|
+
return { ok: false, reason: 'quota', error: err };
|
|
88
|
+
}
|
|
89
|
+
return { ok: false, reason: 'disabled', error: err };
|
|
90
|
+
}
|
|
91
|
+
profile.lastWrites.set(slot, envelope.timestamp);
|
|
92
|
+
return { ok: true };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Writes to `slot` only if at least `minIntervalMs` has elapsed since the
|
|
96
|
+
* last successful write to the same slot in this session. Otherwise returns
|
|
97
|
+
* `{ ok: false, reason: 'throttled' }`.
|
|
98
|
+
*
|
|
99
|
+
* Throttle state is in-memory; a page reload resets it.
|
|
100
|
+
*/
|
|
101
|
+
export function writeSaveThrottled(profile, slot, minIntervalMs) {
|
|
102
|
+
const last = profile.lastWrites.get(slot);
|
|
103
|
+
if (last !== undefined && Date.now() - last < minIntervalMs) {
|
|
104
|
+
return { ok: false, reason: 'throttled' };
|
|
105
|
+
}
|
|
106
|
+
return writeSave(profile, slot);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reads a slot, runs migration if needed, then calls `deserialize` with
|
|
110
|
+
* the resulting payload. On success the game state has been restored.
|
|
111
|
+
*/
|
|
112
|
+
export function readSave(profile, slot = DEFAULT_SLOT) {
|
|
113
|
+
const storage = getStorage();
|
|
114
|
+
if (!storage)
|
|
115
|
+
return { ok: false, reason: 'disabled' };
|
|
116
|
+
const raw = storage.getItem(storageKey(profile.key, slot));
|
|
117
|
+
if (raw === null)
|
|
118
|
+
return { ok: false, reason: 'not_found' };
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(raw);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return { ok: false, reason: 'parse_error', error: error };
|
|
125
|
+
}
|
|
126
|
+
if (!isValidEnvelope(parsed)) {
|
|
127
|
+
return { ok: false, reason: 'corrupt' };
|
|
128
|
+
}
|
|
129
|
+
let data = parsed.data;
|
|
130
|
+
if (parsed.version !== profile.version) {
|
|
131
|
+
if (parsed.version > profile.version) {
|
|
132
|
+
return { ok: false, reason: 'version_unsupported' };
|
|
133
|
+
}
|
|
134
|
+
if (!profile.config.migrate) {
|
|
135
|
+
return { ok: false, reason: 'version_unsupported' };
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
data = profile.config.migrate(data, parsed.version);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return { ok: false, reason: 'corrupt', error: error };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
profile.config.deserialize(data);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return { ok: false, reason: 'corrupt', error: error };
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, slot };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Reads whichever slot has the newest timestamp. Returns `not_found` if
|
|
154
|
+
* no slots exist for this profile's key.
|
|
155
|
+
*/
|
|
156
|
+
export function readSaveLatest(profile) {
|
|
157
|
+
const slots = listSaves(profile);
|
|
158
|
+
if (slots.length === 0)
|
|
159
|
+
return { ok: false, reason: 'not_found' };
|
|
160
|
+
let newest = slots[0];
|
|
161
|
+
for (let i = 1; i < slots.length; i++) {
|
|
162
|
+
if (slots[i].timestamp > newest.timestamp)
|
|
163
|
+
newest = slots[i];
|
|
164
|
+
}
|
|
165
|
+
return readSave(profile, newest.name);
|
|
166
|
+
}
|
|
167
|
+
/** True iff the slot exists in storage. Does not validate envelope shape. */
|
|
168
|
+
export function saveExists(profile, slot = DEFAULT_SLOT) {
|
|
169
|
+
const storage = getStorage();
|
|
170
|
+
if (!storage)
|
|
171
|
+
return false;
|
|
172
|
+
try {
|
|
173
|
+
return storage.getItem(storageKey(profile.key, slot)) !== null;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Removes a slot. Returns `true` if a slot was removed, `false` if it
|
|
181
|
+
* didn't exist or storage is unavailable.
|
|
182
|
+
*/
|
|
183
|
+
export function deleteSave(profile, slot = DEFAULT_SLOT) {
|
|
184
|
+
const storage = getStorage();
|
|
185
|
+
if (!storage)
|
|
186
|
+
return false;
|
|
187
|
+
const key = storageKey(profile.key, slot);
|
|
188
|
+
try {
|
|
189
|
+
if (storage.getItem(key) === null)
|
|
190
|
+
return false;
|
|
191
|
+
storage.removeItem(key);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
profile.lastWrites.delete(slot);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Enumerates all slots that belong to this profile's key. Corrupt or
|
|
201
|
+
* mis-shaped entries are silently skipped — they will surface as `corrupt`
|
|
202
|
+
* if loaded explicitly via {@link readSave}.
|
|
203
|
+
*/
|
|
204
|
+
export function listSaves(profile) {
|
|
205
|
+
const storage = getStorage();
|
|
206
|
+
if (!storage)
|
|
207
|
+
return [];
|
|
208
|
+
const prefix = `${NAMESPACE}:${profile.key}:`;
|
|
209
|
+
const slots = [];
|
|
210
|
+
let length = 0;
|
|
211
|
+
try {
|
|
212
|
+
length = storage.length;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
for (let i = 0; i < length; i++) {
|
|
218
|
+
let key = null;
|
|
219
|
+
try {
|
|
220
|
+
key = storage.key(i);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!key || !key.startsWith(prefix))
|
|
226
|
+
continue;
|
|
227
|
+
let raw = null;
|
|
228
|
+
try {
|
|
229
|
+
raw = storage.getItem(key);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (raw === null)
|
|
235
|
+
continue;
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(raw);
|
|
238
|
+
if (isValidEnvelope(parsed)) {
|
|
239
|
+
slots.push({
|
|
240
|
+
name: key.slice(prefix.length),
|
|
241
|
+
timestamp: parsed.timestamp,
|
|
242
|
+
version: parsed.version,
|
|
243
|
+
sizeBytes: raw.length,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// skip corrupt entries
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return slots;
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=save.js.map
|
package/dist/save.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save.js","sourceRoot":"","sources":["../src/save.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zx-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/zrebec/zx-kit.git"
|
|
@@ -36,11 +36,15 @@
|
|
|
36
36
|
"node": ">=22"
|
|
37
37
|
},
|
|
38
38
|
"license": "MIT",
|
|
39
|
+
"overrides": {
|
|
40
|
+
"npm": ">=11.14.1"
|
|
41
|
+
},
|
|
39
42
|
"devDependencies": {
|
|
40
43
|
"@semantic-release/changelog": "^6.0.3",
|
|
41
44
|
"@semantic-release/git": "^10.0.1",
|
|
42
45
|
"@semantic-release/github": "^12.0.6",
|
|
43
46
|
"@semantic-release/npm": "^13.1.5",
|
|
47
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
44
48
|
"jsdom": "^29.1.1",
|
|
45
49
|
"semantic-release": "^25.0.3",
|
|
46
50
|
"typescript": "^6.0.3",
|