zx-kit 0.14.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 +400 -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/input.d.ts +10 -6
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +31 -6
- package/dist/input.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 +1 -1
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
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/input.d.ts
CHANGED
|
@@ -22,15 +22,19 @@ export declare function initInput(repeatDelay?: number, repeatInterval?: number)
|
|
|
22
22
|
* Returns a direction immediately on first press; repeats every `repeatInterval` ms while held.
|
|
23
23
|
* Returns `null` when no movement should occur this frame.
|
|
24
24
|
*
|
|
25
|
-
* @param dtMs
|
|
25
|
+
* @param dtMs - Frame delta in milliseconds
|
|
26
|
+
* @param analogStepMs - When > 0, analog stick input fires at most once per this many ms
|
|
27
|
+
* instead of using the standard key-repeat rate. D-pad is unaffected.
|
|
28
|
+
* Default `0` = standard repeat for both D-pad and stick.
|
|
26
29
|
*
|
|
27
30
|
* @example
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* // Grid game — stick moves one cell per 220 ms; D-pad uses normal repeat
|
|
32
|
+
* const dir = tickMovement(dt, 220)
|
|
33
|
+
*
|
|
34
|
+
* // Shooter — stick moves continuously at full repeat rate (default)
|
|
35
|
+
* const dir = tickMovement(dt)
|
|
32
36
|
*/
|
|
33
|
-
export declare function tickMovement(dtMs: number): Direction | null;
|
|
37
|
+
export declare function tickMovement(dtMs: number, analogStepMs?: number): Direction | null;
|
|
34
38
|
/** Consumes and returns the `F` key flag event (flag/unflag a cell). Resets to `false` after read. */
|
|
35
39
|
export declare function consumeFlag(): boolean;
|
|
36
40
|
/** Consumes and returns the `Ctrl+Shift+B` debug toggle event. Resets to `false` after read. */
|
package/dist/input.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AA8GxD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,WAAW,SAAM,EAAE,cAAc,SAAK,GAAG,IAAI,CAoCtE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,SAAI,GAAG,SAAS,GAAG,IAAI,CA4B7E;AAED,sGAAsG;AACtG,wBAAgB,WAAW,IAAI,OAAO,CAA+D;AAErG,gGAAgG;AAChG,wBAAgB,YAAY,IAAI,OAAO,CAA8D;AAErG,kFAAkF;AAClF,wBAAgB,YAAY,IAAI,OAAO,CAA8D;AAErG;;;GAGG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAA6D;AAErG;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAyB;AAErE;;;;;;;;GAQG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAgBjC"}
|
package/dist/input.js
CHANGED
|
@@ -23,6 +23,8 @@ let padPrevBtnFlag = false;
|
|
|
23
23
|
let padPrevBtnPause = false;
|
|
24
24
|
let padPrevBtnDebug = false;
|
|
25
25
|
let padPrevAny = false;
|
|
26
|
+
let padFromStick = false; // true when current repeatDir came from analog stick
|
|
27
|
+
let analogCooldown = 0; // ms remaining before next analog-stick step is allowed
|
|
26
28
|
function pollGamepad() {
|
|
27
29
|
if (typeof navigator === 'undefined')
|
|
28
30
|
return;
|
|
@@ -34,6 +36,7 @@ function pollGamepad() {
|
|
|
34
36
|
continue;
|
|
35
37
|
// D-pad (standard mapping: buttons 12-15) takes priority over analog stick
|
|
36
38
|
let dir = null;
|
|
39
|
+
let fromStick = false;
|
|
37
40
|
if (pad.buttons[12]?.pressed)
|
|
38
41
|
dir = 'up';
|
|
39
42
|
else if (pad.buttons[13]?.pressed)
|
|
@@ -51,6 +54,7 @@ function pollGamepad() {
|
|
|
51
54
|
dir = ax < 0 ? 'left' : 'right';
|
|
52
55
|
else
|
|
53
56
|
dir = ay < 0 ? 'up' : 'down';
|
|
57
|
+
fromStick = true;
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
// Synthesize direction changes into the existing keyboard repeat state machine
|
|
@@ -64,6 +68,11 @@ function pollGamepad() {
|
|
|
64
68
|
repeatDir = dir;
|
|
65
69
|
repeatPhase = 'delay';
|
|
66
70
|
repeatTimer = _repeatDelay;
|
|
71
|
+
padFromStick = fromStick;
|
|
72
|
+
analogCooldown = 0;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
padFromStick = false;
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
padPrevDir = dir;
|
|
@@ -152,22 +161,36 @@ export function initInput(repeatDelay = 150, repeatInterval = 80) {
|
|
|
152
161
|
* Returns a direction immediately on first press; repeats every `repeatInterval` ms while held.
|
|
153
162
|
* Returns `null` when no movement should occur this frame.
|
|
154
163
|
*
|
|
155
|
-
* @param dtMs
|
|
164
|
+
* @param dtMs - Frame delta in milliseconds
|
|
165
|
+
* @param analogStepMs - When > 0, analog stick input fires at most once per this many ms
|
|
166
|
+
* instead of using the standard key-repeat rate. D-pad is unaffected.
|
|
167
|
+
* Default `0` = standard repeat for both D-pad and stick.
|
|
156
168
|
*
|
|
157
169
|
* @example
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
170
|
+
* // Grid game — stick moves one cell per 220 ms; D-pad uses normal repeat
|
|
171
|
+
* const dir = tickMovement(dt, 220)
|
|
172
|
+
*
|
|
173
|
+
* // Shooter — stick moves continuously at full repeat rate (default)
|
|
174
|
+
* const dir = tickMovement(dt)
|
|
162
175
|
*/
|
|
163
|
-
export function tickMovement(dtMs) {
|
|
176
|
+
export function tickMovement(dtMs, analogStepMs = 0) {
|
|
164
177
|
pollGamepad();
|
|
178
|
+
analogCooldown = Math.max(0, analogCooldown - dtMs);
|
|
165
179
|
if (pendingImmediate !== null) {
|
|
166
180
|
const d = pendingImmediate;
|
|
167
181
|
pendingImmediate = null;
|
|
182
|
+
if (padFromStick && analogStepMs > 0)
|
|
183
|
+
analogCooldown = analogStepMs;
|
|
168
184
|
return d;
|
|
169
185
|
}
|
|
170
186
|
if (repeatDir !== null && repeatPhase !== 'idle') {
|
|
187
|
+
if (padFromStick && analogStepMs > 0) {
|
|
188
|
+
if (analogCooldown <= 0) {
|
|
189
|
+
analogCooldown = analogStepMs;
|
|
190
|
+
return repeatDir;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
171
194
|
repeatTimer -= dtMs;
|
|
172
195
|
if (repeatTimer <= 0) {
|
|
173
196
|
repeatTimer += _repeatInterval;
|
|
@@ -221,5 +244,7 @@ export function resetInput() {
|
|
|
221
244
|
padPrevBtnPause = false;
|
|
222
245
|
padPrevBtnDebug = false;
|
|
223
246
|
padPrevAny = false;
|
|
247
|
+
padFromStick = false;
|
|
248
|
+
analogCooldown = 0;
|
|
224
249
|
}
|
|
225
250
|
//# sourceMappingURL=input.js.map
|
package/dist/input.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.js","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAGA,MAAM,QAAQ,GAA8B;IAC1C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO;CACzE,CAAA;AAED,MAAM,UAAU,GAA8B;IAC5C,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY;CACzE,CAAA;AAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;AAC9B,IAAI,WAAW,GAAG,KAAK,CAAA;AACvB,IAAI,YAAY,GAAG,KAAK,CAAA;AACxB,IAAI,YAAY,GAAG,KAAK,CAAA;AACxB,IAAI,aAAa,GAAG,KAAK,CAAA;AAEzB,IAAI,SAAS,GAAqB,IAAI,CAAA;AACtC,IAAI,WAAW,GAAG,CAAC,CAAA;AACnB,IAAI,WAAW,GAAgC,MAAM,CAAA;AACrD,IAAI,gBAAgB,GAAqB,IAAI,CAAA;AAC7C,IAAI,YAAY,GAAG,GAAG,CAAA;AACtB,IAAI,eAAe,GAAG,EAAE,CAAA;AACxB,IAAI,gBAAgB,GAAG,KAAK,CAAA;AAE5B,kFAAkF;AAElF,MAAM,UAAU,GAAG,IAAI,CAAA;AAEvB,IAAI,UAAU,GAAqB,IAAI,CAAA;AACvC,IAAI,cAAc,GAAI,KAAK,CAAA;AAC3B,IAAI,eAAe,GAAG,KAAK,CAAA;AAC3B,IAAI,eAAe,GAAG,KAAK,CAAA;AAC3B,IAAI,UAAU,GAAQ,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"input.js","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"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"}
|
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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"}
|