zx-kit 0.15.0 → 0.16.0

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 CHANGED
@@ -128,6 +128,406 @@ requestAnimationFrame(loop)
128
128
 
129
129
  ---
130
130
 
131
+ ## Getting Started — Build Your First Game
132
+
133
+ 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.
134
+
135
+ No prior game development experience needed. You need basic JavaScript/TypeScript knowledge (variables, functions, arrays).
136
+
137
+ ---
138
+
139
+ ### What you will need
140
+
141
+ | Tool | Where to get it | Why |
142
+ |------|----------------|-----|
143
+ | **Node.js 18+** | [nodejs.org](https://nodejs.org) | Runs npm — the package manager we use to install zx-kit |
144
+ | **A code editor** | [code.visualstudio.com](https://code.visualstudio.com) (free) | Edits your source files |
145
+ | **A terminal** | Built into macOS/Linux; use PowerShell on Windows | Runs commands |
146
+
147
+ ---
148
+
149
+ ### Step 1 — Create the project
150
+
151
+ Open a terminal and run:
152
+
153
+ ```bash
154
+ mkdir my-first-game
155
+ cd my-first-game
156
+ npm init -y
157
+ ```
158
+
159
+ `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.
160
+
161
+ ---
162
+
163
+ ### Step 2 — Install dependencies
164
+
165
+ ```bash
166
+ npm install zx-kit
167
+ npm install --save-dev vite
168
+ ```
169
+
170
+ - **zx-kit** — the game engine you are building with
171
+ - **vite** — a development server that reloads the browser whenever you save a file (installed as a dev tool, not part of your shipped game)
172
+
173
+ ---
174
+
175
+ ### Step 3 — Configure package.json
176
+
177
+ 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):
178
+
179
+ ```json
180
+ {
181
+ "name": "my-first-game",
182
+ "version": "1.0.0",
183
+ "type": "module",
184
+ "scripts": {
185
+ "dev": "vite",
186
+ "build": "vite build"
187
+ },
188
+ "dependencies": {
189
+ "zx-kit": "^0.15.0"
190
+ },
191
+ "devDependencies": {
192
+ "vite": "^6.0.0"
193
+ }
194
+ }
195
+ ```
196
+
197
+ ---
198
+
199
+ ### Step 4 — Create index.html
200
+
201
+ Create a file called `index.html` in your project root:
202
+
203
+ ```html
204
+ <!DOCTYPE html>
205
+ <html lang="en">
206
+ <head>
207
+ <meta charset="UTF-8" />
208
+ <title>My First Game</title>
209
+ <style>
210
+ body {
211
+ margin: 0;
212
+ background: #000;
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ height: 100vh;
217
+ }
218
+ </style>
219
+ </head>
220
+ <body>
221
+ <canvas id="game"></canvas>
222
+ <script type="module" src="/src/main.ts"></script>
223
+ </body>
224
+ </html>
225
+ ```
226
+
227
+ The `<canvas>` is the game screen. The `<script>` tag loads your game code.
228
+
229
+ ---
230
+
231
+ ### Step 5 — Create the game
232
+
233
+ Create the folder `src/` and inside it a file called `main.ts`. We will build it one piece at a time.
234
+
235
+ ---
236
+
237
+ #### 5a — Canvas setup
238
+
239
+ 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.
240
+
241
+ ```ts
242
+ import { setupCanvas, C, CELL } from 'zx-kit'
243
+
244
+ const canvas = document.getElementById('game') as HTMLCanvasElement
245
+ const ctx = setupCanvas(canvas, 4)
246
+ // The screen is 256 × 192 game pixels, scaled up 4× in the browser.
247
+ // From here on draw everything in game pixels — setupCanvas handles the scale.
248
+ ```
249
+
250
+ `C` is the color palette. `CELL` is the size of one sprite: 8 pixels.
251
+
252
+ ---
253
+
254
+ #### 5b — Define a sprite
255
+
256
+ 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.
257
+
258
+ 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.
259
+
260
+ We will use two walking frames so the character animates when it moves:
261
+
262
+ ```ts
263
+ // 76543210 ← bit position (7 = leftmost pixel)
264
+ const WALK_A = new Uint8Array([
265
+ 0b00111100, // ..####.. head
266
+ 0b01111110, // .######.
267
+ 0b00011000, // ...##... neck
268
+ 0b01111110, // .######. arms + body
269
+ 0b00011000, // ...##... waist
270
+ 0b01011010, // .#.##.#. legs apart
271
+ 0b01000010, // .#....#. feet
272
+ 0b00000000, // ........
273
+ ])
274
+
275
+ const WALK_B = new Uint8Array([
276
+ 0b00111100, // ..####.. head
277
+ 0b01111110, // .######.
278
+ 0b00011000, // ...##... neck
279
+ 0b01111110, // .######. arms + body
280
+ 0b00011000, // ...##... waist
281
+ 0b00111100, // ..####.. legs together
282
+ 0b00011000, // ...##...
283
+ 0b00000000, // ........
284
+ ])
285
+
286
+ const FRAMES = [WALK_A, WALK_B] // frame 0 = legs apart, frame 1 = legs together
287
+ ```
288
+
289
+ ---
290
+
291
+ #### 5c — Input
292
+
293
+ `initInput()` attaches keyboard listeners. Call it once at startup, not inside the game loop.
294
+
295
+ `isHeld(key)` returns `true` while a key is pressed. We use it to check whether an arrow key is being held down each frame.
296
+
297
+ ```ts
298
+ import { initInput, isHeld } from 'zx-kit'
299
+
300
+ initInput()
301
+ ```
302
+
303
+ ---
304
+
305
+ #### 5d — Audio
306
+
307
+ 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:
308
+
309
+ ```ts
310
+ import { initAudio, getAudioContext, resumeAudio, beep } from 'zx-kit'
311
+
312
+ window.addEventListener('keydown', () => initAudio(0.3), { once: true })
313
+ // initAudio(0.3) = master volume 30%. Called at most once thanks to { once: true }.
314
+ ```
315
+
316
+ ---
317
+
318
+ #### 5e — Player state and animation
319
+
320
+ ```ts
321
+ import { createAnimation, tickAnimation } from 'zx-kit'
322
+
323
+ let px = 120 // player x position in game pixels
324
+ let py = 88 // player y position
325
+ const SPEED = 60 // pixels per second
326
+
327
+ // A 2-frame looping animation, 150ms per frame = one full step every 300ms
328
+ const walkAnim = createAnimation(2, 150)
329
+ let stepTimer = 0 // footstep sound timer
330
+ ```
331
+
332
+ ---
333
+
334
+ #### 5f — The game loop
335
+
336
+ Every frame the browser calls `loop`. Inside, we:
337
+
338
+ 1. Calculate `dt` — how many milliseconds passed since last frame
339
+ 2. Move the player based on held keys
340
+ 3. Keep the player inside the screen
341
+ 4. Pick the right animation frame
342
+ 5. Play a footstep sound periodically while moving
343
+ 6. Clear the screen and redraw everything
344
+
345
+ ```ts
346
+ import { drawSprite, drawText } from 'zx-kit'
347
+
348
+ let lastTime = 0
349
+
350
+ function loop(now: number): void {
351
+ const dt = Math.min(now - lastTime, 100)
352
+ // dt is milliseconds since the last frame (usually ~16ms at 60fps).
353
+ // We cap at 100ms so a background tab returning doesn't cause a huge position jump.
354
+ lastTime = now
355
+
356
+ // ── Move player ────────────────────────────────────────────────────────────
357
+ let moving = false
358
+ if (isHeld('ArrowRight')) { px += SPEED * dt / 1000; moving = true }
359
+ if (isHeld('ArrowLeft')) { px -= SPEED * dt / 1000; moving = true }
360
+ if (isHeld('ArrowDown')) { py += SPEED * dt / 1000; moving = true }
361
+ if (isHeld('ArrowUp')) { py -= SPEED * dt / 1000; moving = true }
362
+ // SPEED (60) is pixels per second. dt is milliseconds. Divide by 1000 to convert.
363
+
364
+ // Keep inside the 256×192 canvas
365
+ px = Math.max(0, Math.min(256 - CELL, px))
366
+ py = Math.max(0, Math.min(192 - CELL, py))
367
+
368
+ // ── Animation ──────────────────────────────────────────────────────────────
369
+ // tickAnimation advances the timer and returns the current frame index (0 or 1).
370
+ // When standing still we always use frame 0.
371
+ const frame = moving ? tickAnimation(walkAnim, dt) : 0
372
+
373
+ // ── Footstep sound ─────────────────────────────────────────────────────────
374
+ if (moving) {
375
+ stepTimer -= dt
376
+ if (stepTimer <= 0) {
377
+ stepTimer = 250 // play a sound every 250ms while moving
378
+ const audio = getAudioContext()
379
+ if (audio) {
380
+ resumeAudio() // un-suspend if the tab was hidden
381
+ beep(220, 30, audio.currentTime) // 220 Hz, 30ms — a short thud
382
+ }
383
+ }
384
+ } else {
385
+ stepTimer = 0 // reset so next movement starts immediately
386
+ }
387
+
388
+ // ── Draw ───────────────────────────────────────────────────────────────────
389
+ ctx.fillStyle = C.BLACK
390
+ ctx.fillRect(0, 0, 256, 192) // clear the whole screen
391
+
392
+ drawSprite(ctx, FRAMES[frame], Math.round(px), Math.round(py), C.B_CYAN, C.BLACK)
393
+ // ↑ position ↑ ink color ↑ paper (background)
394
+
395
+ drawText(ctx, 'ARROW KEYS = MOVE', 8, 184, C.WHITE)
396
+ // drawText draws one ASCII character per 8px slot, left-to-right.
397
+
398
+ requestAnimationFrame(loop) // ask the browser to call us again next frame
399
+ }
400
+
401
+ requestAnimationFrame(loop) // kick off the first frame
402
+ ```
403
+
404
+ ---
405
+
406
+ ### Step 6 — Run the game
407
+
408
+ ```bash
409
+ npm run dev
410
+ ```
411
+
412
+ Open [http://localhost:5173](http://localhost:5173) in your browser. Press an arrow key. Your character walks.
413
+
414
+ ---
415
+
416
+ ### Complete file
417
+
418
+ The full `src/main.ts` all in one place:
419
+
420
+ ```ts
421
+ import {
422
+ setupCanvas, C, CELL,
423
+ drawSprite, drawText,
424
+ initInput, isHeld,
425
+ initAudio, getAudioContext, resumeAudio, beep,
426
+ createAnimation, tickAnimation,
427
+ } from 'zx-kit'
428
+
429
+ // ── Canvas ────────────────────────────────────────────────────────────────────
430
+ const canvas = document.getElementById('game') as HTMLCanvasElement
431
+ const ctx = setupCanvas(canvas, 4)
432
+
433
+ // ── Sprites ───────────────────────────────────────────────────────────────────
434
+ const WALK_A = new Uint8Array([
435
+ 0b00111100, // ..####.. head
436
+ 0b01111110, // .######.
437
+ 0b00011000, // ...##... neck
438
+ 0b01111110, // .######. arms + body
439
+ 0b00011000, // ...##... waist
440
+ 0b01011010, // .#.##.#. legs apart
441
+ 0b01000010, // .#....#. feet
442
+ 0b00000000, // ........
443
+ ])
444
+
445
+ const WALK_B = new Uint8Array([
446
+ 0b00111100, // ..####.. head
447
+ 0b01111110, // .######.
448
+ 0b00011000, // ...##... neck
449
+ 0b01111110, // .######. arms + body
450
+ 0b00011000, // ...##... waist
451
+ 0b00111100, // ..####.. legs together
452
+ 0b00011000, // ...##...
453
+ 0b00000000, // ........
454
+ ])
455
+
456
+ const FRAMES = [WALK_A, WALK_B]
457
+
458
+ // ── Input ─────────────────────────────────────────────────────────────────────
459
+ initInput()
460
+
461
+ // ── Audio ─────────────────────────────────────────────────────────────────────
462
+ window.addEventListener('keydown', () => initAudio(0.3), { once: true })
463
+
464
+ // ── Player state ──────────────────────────────────────────────────────────────
465
+ let px = 120
466
+ let py = 88
467
+ const SPEED = 60 // pixels per second
468
+
469
+ const walkAnim = createAnimation(2, 150)
470
+ let stepTimer = 0
471
+
472
+ // ── Game loop ─────────────────────────────────────────────────────────────────
473
+ let lastTime = 0
474
+
475
+ function loop(now: number): void {
476
+ const dt = Math.min(now - lastTime, 100)
477
+ lastTime = now
478
+
479
+ let moving = false
480
+ if (isHeld('ArrowRight')) { px += SPEED * dt / 1000; moving = true }
481
+ if (isHeld('ArrowLeft')) { px -= SPEED * dt / 1000; moving = true }
482
+ if (isHeld('ArrowDown')) { py += SPEED * dt / 1000; moving = true }
483
+ if (isHeld('ArrowUp')) { py -= SPEED * dt / 1000; moving = true }
484
+
485
+ px = Math.max(0, Math.min(256 - CELL, px))
486
+ py = Math.max(0, Math.min(192 - CELL, py))
487
+
488
+ const frame = moving ? tickAnimation(walkAnim, dt) : 0
489
+
490
+ if (moving) {
491
+ stepTimer -= dt
492
+ if (stepTimer <= 0) {
493
+ stepTimer = 250
494
+ const audio = getAudioContext()
495
+ if (audio) { resumeAudio(); beep(220, 30, audio.currentTime) }
496
+ }
497
+ } else {
498
+ stepTimer = 0
499
+ }
500
+
501
+ ctx.fillStyle = C.BLACK
502
+ ctx.fillRect(0, 0, 256, 192)
503
+
504
+ drawSprite(ctx, FRAMES[frame], Math.round(px), Math.round(py), C.B_CYAN, C.BLACK)
505
+ drawText(ctx, 'ARROW KEYS = MOVE', 8, 184, C.WHITE)
506
+
507
+ requestAnimationFrame(loop)
508
+ }
509
+
510
+ requestAnimationFrame(loop)
511
+ ```
512
+
513
+ ---
514
+
515
+ ### What to try next
516
+
517
+ **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.
518
+
519
+ **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).
520
+
521
+ **Add a second character.** Copy the player variables (`px2`, `py2`, `walkAnim2`) and add `W A S D` controls using `isHeld('w')` etc.
522
+
523
+ **Add obstacles.** Use `createTileMap` to place solid wall tiles and `resolveX` / `resolveY` to stop the player at them.
524
+
525
+ **Add chiptune music.** Call `playAY()` with a note array to play a three-channel melody — see [`ay.ts`](#ayts--ay-3-8912-melodik-audio).
526
+
527
+ **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.
528
+
529
+ ---
530
+
131
531
  ## Modules
132
532
 
133
533
  | Module | What it provides |
package/dist/index.d.ts CHANGED
@@ -11,4 +11,5 @@ export * from './collision.js';
11
11
  export * from './animation.js';
12
12
  export * from './camera.js';
13
13
  export * from './scene.js';
14
+ export * from './save.js';
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -11,4 +11,5 @@ export * from './collision.js';
11
11
  export * from './animation.js';
12
12
  export * from './camera.js';
13
13
  export * from './scene.js';
14
+ export * from './save.js';
14
15
  //# sourceMappingURL=index.js.map
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
@@ -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.15.0",
3
+ "version": "0.16.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zrebec/zx-kit.git"