zx-kit 0.2.0 → 0.2.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 CHANGED
@@ -8,356 +8,296 @@ Extracted from [Minefield](https://github.com/zrebec/minefield) — a ZX Spectru
8
8
 
9
9
  ## Installation
10
10
 
11
- The package ships TypeScript source directly (no build step). Consume it via a local path in `package.json`:
12
-
13
- ```json
14
- "dependencies": {
15
- "zx-kit": "file:../zx-kit"
16
- }
17
- ```
18
-
19
- Then add a Vite alias so bundler resolution works (Vite doesn't transform `node_modules` by default):
20
-
21
- ```ts
22
- // vite.config.ts
23
- import { resolve } from 'path'
24
-
25
- export default defineConfig({
26
- resolve: {
27
- alias: { 'zx-kit': resolve(__dirname, '../zx-kit/src/index.ts') },
28
- },
29
- })
11
+ ```bash
12
+ npm install zx-kit
30
13
  ```
31
14
 
32
15
  Import from the barrel:
33
16
 
34
17
  ```ts
35
- import { C, CELL, initAudio, beep, drawSprite, initInput, tickMovement } from 'zx-kit'
18
+ import { C, CELL, setupCanvas, initAudio, playPattern, initInput, tickMovement } from 'zx-kit'
36
19
  ```
37
20
 
21
+ No Vite alias or path mapping required — the package ships compiled JavaScript (`dist/`).
22
+
38
23
  ---
39
24
 
40
- ## Modules
25
+ ## Quick start
41
26
 
42
- ### `palette.ts` — ZX Spectrum color constants
27
+ ```ts
28
+ import { setupCanvas, C, CELL, drawText, initAudio, playPattern, initInput, tickMovement } from 'zx-kit'
43
29
 
44
- Defines the exact 15 colors of the ZX Spectrum and the 8-pixel cell size. **Never use any other hex values** — all game graphics must stay within this palette.
30
+ // Canvas one call replaces the manual boilerplate
31
+ const canvas = document.getElementById('game') as HTMLCanvasElement
32
+ const ctx = setupCanvas(canvas, 256, 192) // sets size + imageSmoothingEnabled = false
45
33
 
46
- #### Exports
34
+ // Input
35
+ initInput()
47
36
 
48
- | Export | Type | Value | Description |
49
- |--------|------|-------|-------------|
50
- | `SCALE` | `number` | `4` | CSS pixel scale factor (1 game pixel = 4 CSS pixels) |
51
- | `CELL` | `number` | `8` | Sprite / character grid size in game pixels |
52
- | `C` | `object` | see below | All 15 Spectrum colors as `#RRGGBB` hex strings |
53
- | `SpectrumColor` | `type` | union of all `C` values | TypeScript type for palette-safe color values |
37
+ // Audio (must be inside a user gesture)
38
+ window.addEventListener('keydown', () => initAudio(), { once: true })
54
39
 
55
- #### Color table (`C` object)
40
+ // Game loop
41
+ function loop(dt: number) {
42
+ const dir = tickMovement(dt)
43
+ if (dir) movePlayer(dir)
56
44
 
57
- Normal brightness colors (prefix-less):
45
+ drawText(ctx, 'SCORE:00000', 0, 0, C.B_WHITE, C.BLACK)
46
+ requestAnimationFrame(t => loop(t - lastT))
47
+ }
48
+ ```
58
49
 
59
- | Key | Hex | Appearance |
60
- |-----|-----|-----------|
61
- | `C.BLACK` | `#000000` | Black |
62
- | `C.BLUE` | `#0000CD` | Dark blue |
63
- | `C.RED` | `#CD0000` | Dark red |
64
- | `C.MAGENTA` | `#CD00CD` | Dark magenta |
65
- | `C.GREEN` | `#00CD00` | Dark green |
66
- | `C.CYAN` | `#00CDCD` | Dark cyan |
67
- | `C.YELLOW` | `#CDCD00` | Dark yellow |
68
- | `C.WHITE` | `#CDCDCD` | Light grey |
50
+ ---
69
51
 
70
- Bright variants (`B_` prefix):
52
+ ## Modules
71
53
 
72
- | Key | Hex | Appearance |
73
- |-----|-----|-----------|
74
- | `C.B_BLACK` | `#000000` | Same as `BLACK` |
75
- | `C.B_BLUE` | `#0000FF` | Bright blue |
76
- | `C.B_RED` | `#FF0000` | Bright red |
77
- | `C.B_MAGENTA` | `#FF00FF` | Bright magenta |
78
- | `C.B_GREEN` | `#00FF00` | Bright green |
79
- | `C.B_CYAN` | `#00FFFF` | Bright cyan |
80
- | `C.B_YELLOW` | `#FFFF00` | Bright yellow |
81
- | `C.B_WHITE` | `#FFFFFF` | Pure white |
54
+ ### `palette.ts` ZX Spectrum color constants
82
55
 
83
- #### Usage example
56
+ #### Exports
84
57
 
85
- ```ts
86
- import { C, CELL } from 'zx-kit'
58
+ | Export | Type | Description |
59
+ |--------|------|-------------|
60
+ | `SCALE` | `number` | CSS pixel scale factor (1 game pixel = 4 CSS pixels) |
61
+ | `CELL` | `number` | Sprite / character grid size: `8` game pixels |
62
+ | `C` | `object` | All 15 Spectrum colors as `#RRGGBB` hex strings |
63
+ | `SpectrumColor` | `type` | Union of every value in `C` — use for compile-time palette enforcement |
64
+
65
+ #### Color table
66
+
67
+ Normal brightness:
68
+
69
+ | Key | Hex |
70
+ |-----|-----|
71
+ | `C.BLACK` | `#000000` |
72
+ | `C.BLUE` | `#0000CD` |
73
+ | `C.RED` | `#CD0000` |
74
+ | `C.MAGENTA` | `#CD00CD` |
75
+ | `C.GREEN` | `#00CD00` |
76
+ | `C.CYAN` | `#00CDCD` |
77
+ | `C.YELLOW` | `#CDCD00` |
78
+ | `C.WHITE` | `#CDCDCD` |
87
79
 
88
- ctx.fillStyle = C.B_CYAN // bright cyan
89
- ctx.fillRect(x, y, CELL, CELL)
90
- ```
80
+ Bright variants (`B_` prefix):
81
+
82
+ | Key | Hex |
83
+ |-----|-----|
84
+ | `C.B_BLACK` | `#000000` |
85
+ | `C.B_BLUE` | `#0000FF` |
86
+ | `C.B_RED` | `#FF0000` |
87
+ | `C.B_MAGENTA` | `#FF00FF` |
88
+ | `C.B_GREEN` | `#00FF00` |
89
+ | `C.B_CYAN` | `#00FFFF` |
90
+ | `C.B_YELLOW` | `#FFFF00` |
91
+ | `C.B_WHITE` | `#FFFFFF` |
91
92
 
92
93
  ---
93
94
 
94
95
  ### `font.ts` — ZX Spectrum ROM bitmap font
95
96
 
96
- Contains the exact ZX Spectrum ROM character set — 96 printable characters (ASCII 32–127), each stored as 8 bytes (one byte per row, bit 7 = leftmost pixel). Character 127 is a solid block `█`, used for things like life counters.
97
-
98
- The font data is sourced from the original Spectrum ROM and must not be altered — any change breaks Spectrum authenticity.
97
+ 96 printable characters (ASCII 32–127), each 8×8 pixels. Character 127 is a solid block █.
99
98
 
100
99
  #### Exports
101
100
 
102
- | Export | Type | Description |
103
- |--------|------|-------------|
104
- | `FONT` | `Uint8Array` | Flat array: 96 chars × 8 bytes = 768 bytes total. `FONT[(charCode-32)*8 + row]` = one row bitmap. |
105
- | `getCharRow(charCode, row)` | `(number, number) => number` | Returns the bitmap byte for the given ASCII code and row (0–7). Returns `0` for out-of-range codes. |
101
+ | Export | Signature | Description |
102
+ |--------|-----------|-------------|
103
+ | `FONT` | `Uint8Array` | Flat array: 96 chars × 8 bytes. `FONT[(code-32)*8 + row]` = one row bitmap. |
104
+ | `getCharRow` | `(charCode, row) => number` | Bitmap byte for one row of a character (bit 7 = leftmost pixel). |
106
105
 
107
- #### Usage example
108
-
109
- ```ts
110
- import { getCharRow } from 'zx-kit'
111
-
112
- // Draw character 'A' (65) row by row
113
- for (let row = 0; row < 8; row++) {
114
- const byte = getCharRow(65, row)
115
- for (let bit = 0; bit < 8; bit++) {
116
- if (byte & (0x80 >> bit)) {
117
- ctx.fillRect(x + bit, y + row, 1, 1)
118
- }
119
- }
120
- }
121
- ```
122
-
123
- In practice you won't need this directly — use `drawChar` / `drawText` from `renderer.ts` instead.
106
+ In practice use `drawChar` / `drawText` from `renderer.ts` — you rarely need `getCharRow` directly.
124
107
 
125
108
  ---
126
109
 
127
110
  ### `renderer.ts` — Canvas drawing primitives
128
111
 
129
- All drawing functions work in **game pixels** (not CSS pixels). At `SCALE=4` each game pixel maps to a 4×4 CSS pixel block. Canvas must have `imageSmoothingEnabled = false` set once at init.
112
+ All functions work in **game pixels**. At `SCALE=4` each game pixel maps to a 4×4 CSS pixel block. Call `setupCanvas` once at startup to configure the canvas correctly.
130
113
 
131
- Every function follows the ZX Spectrum **ink / paper** model: each 8×8 cell has exactly one foreground color (ink) and one background color (paper).
132
-
133
- #### Exports
114
+ Every draw function follows the ZX Spectrum **ink / paper** model: each 8×8 cell has one foreground (ink) and one background (paper) color.
134
115
 
135
- ##### `mirrorSprite(src: Uint8Array): Uint8Array`
116
+ #### `setupCanvas(canvas, width, height): CanvasRenderingContext2D`
136
117
 
137
- Flips an 8×8 sprite horizontally. Returns a new `Uint8Array`. Used to derive left-facing sprites from right-facing ones at module load time.
118
+ Initialises a canvas element for pixel-perfect rendering. Sets dimensions, disables image smoothing, and returns the 2D context. **Replaces the manual canvas setup boilerplate.**
138
119
 
139
120
  ```ts
140
- import { mirrorSprite } from 'zx-kit'
121
+ const canvas = document.getElementById('game') as HTMLCanvasElement
122
+ const ctx = setupCanvas(canvas, 256, 192)
123
+ // ctx.imageSmoothingEnabled is already false — no need to set it manually
124
+ ```
125
+
126
+ #### `mirrorSprite(src): Uint8Array`
141
127
 
142
- export const PLAYER_RIGHT = new Uint8Array([0x18, 0x3C, ...])
128
+ Flips an 8×8 sprite horizontally. Returns a new `Uint8Array`.
129
+
130
+ ```ts
131
+ export const PLAYER_RIGHT = new Uint8Array([...])
143
132
  export const PLAYER_LEFT = mirrorSprite(PLAYER_RIGHT)
144
133
  ```
145
134
 
146
- ##### `drawSprite(ctx, sprite, x, y, ink, paper): void`
135
+ #### `drawSprite(ctx, sprite, x, y, ink, paper): void`
147
136
 
148
- Draws an 8×8 sprite at game coordinates `(x, y)`. Always fills the full 8×8 cell with `paper` first, then renders `ink` pixels from the sprite bitmap.
149
-
150
- | Parameter | Type | Description |
151
- |-----------|------|-------------|
152
- | `ctx` | `CanvasRenderingContext2D` | Target canvas context |
153
- | `sprite` | `Uint8Array` | 8-byte sprite (one byte per row) |
154
- | `x` | `number` | Left edge in game pixels |
155
- | `y` | `number` | Top edge in game pixels |
156
- | `ink` | `string` | Foreground color (Spectrum palette hex) |
157
- | `paper` | `string` | Background color (Spectrum palette hex) |
137
+ Draws an 8×8 sprite at game coordinates. Always fills the background first.
158
138
 
159
139
  ```ts
160
- import { drawSprite, C } from 'zx-kit'
161
-
162
- drawSprite(ctx, MINE_SPRITE, col * 8, row * 8, C.B_RED, C.BLACK)
140
+ drawSprite(ctx, MINE_SPRITE, col * CELL, row * CELL, C.B_RED, C.BLACK)
163
141
  ```
164
142
 
165
- ##### `drawChar(ctx, code, x, y, ink, paper?): void`
166
-
167
- Draws a single ASCII character at game coordinates using the ROM font. If `paper` is omitted, the background is not cleared (transparent background).
143
+ #### `drawChar(ctx, code, x, y, ink, paper?): void`
168
144
 
169
- | Parameter | Type | Description |
170
- |-----------|------|-------------|
171
- | `ctx` | `CanvasRenderingContext2D` | Target canvas context |
172
- | `code` | `number` | ASCII character code (32–127) |
173
- | `x` | `number` | Left edge in game pixels |
174
- | `y` | `number` | Top edge in game pixels |
175
- | `ink` | `string` | Foreground color |
176
- | `paper?` | `string` | Optional background color |
145
+ Draws one ASCII character using the ROM font. Omit `paper` for transparent background.
177
146
 
178
147
  ```ts
179
- import { drawChar, C } from 'zx-kit'
180
-
181
148
  drawChar(ctx, 127, x, y, C.B_GREEN, C.BLACK) // solid block █
149
+ drawChar(ctx, 'A'.charCodeAt(0), x, y, C.B_WHITE)
182
150
  ```
183
151
 
184
- ##### `drawText(ctx, text, x, y, ink, paper?): void`
185
-
186
- Draws a string left-to-right starting at `(x, y)`, one character per `CELL`-wide slot.
152
+ #### `drawText(ctx, text, x, y, ink, paper?): void`
187
153
 
188
- | Parameter | Type | Description |
189
- |-----------|------|-------------|
190
- | `ctx` | `CanvasRenderingContext2D` | Target canvas context |
191
- | `text` | `string` | ASCII string to render |
192
- | `x` | `number` | Left edge in game pixels |
193
- | `y` | `number` | Top edge in game pixels |
194
- | `ink` | `string` | Foreground color |
195
- | `paper?` | `string` | Optional background color |
154
+ Draws a string left-to-right, one character per `CELL`-wide slot.
196
155
 
197
156
  ```ts
198
- import { drawText, C } from 'zx-kit'
199
-
200
157
  drawText(ctx, 'SCORE:00000', 0, statusY, C.B_WHITE, C.BLACK)
201
158
  ```
202
159
 
203
- ##### `drawTextCentered(ctx, text, y, cols, ink, paper?): void`
204
-
205
- Draws a string horizontally centered within a canvas of `cols` character columns. The `cols` parameter must match your game's grid width (e.g. 32 for a standard Spectrum layout).
160
+ #### `drawTextCentered(ctx, text, y, cols, ink, paper?): void`
206
161
 
207
- | Parameter | Type | Description |
208
- |-----------|------|-------------|
209
- | `ctx` | `CanvasRenderingContext2D` | Target canvas context |
210
- | `text` | `string` | ASCII string to render |
211
- | `y` | `number` | Top edge in game pixels |
212
- | `cols` | `number` | Total character columns (canvas width ÷ CELL) |
213
- | `ink` | `string` | Foreground color |
214
- | `paper?` | `string` | Optional background color |
162
+ Centers a string within `cols` character columns. Bind `cols` once in a helper to avoid repetition.
215
163
 
216
164
  ```ts
217
- import { drawTextCentered, C } from 'zx-kit'
165
+ // Bind once:
166
+ const centered = (ctx: CanvasRenderingContext2D, text: string, y: number, ink: string) =>
167
+ drawTextCentered(ctx, text, y, 32, ink)
218
168
 
219
- drawTextCentered(ctx, 'GAME OVER', cy * 8, 32, C.B_RED, C.BLACK)
169
+ centered(ctx, 'GAME OVER', y, C.B_RED)
220
170
  ```
221
171
 
222
- **Tip for game code:** Bind `cols` once to avoid passing it everywhere:
172
+ #### `flashBorder(color, times, intervalMs, resetColor?): void`
173
+
174
+ Flashes `document.body.style.backgroundColor` between `color` and `resetColor`. Fire-and-forget — does not block. One flash = one `color → resetColor` cycle. Always resets to `resetColor` on completion (default `C.BLACK`).
223
175
 
224
176
  ```ts
225
- function drawCentered(ctx: CanvasRenderingContext2D, text: string, y: number, ink: string, paper?: string) {
226
- drawTextCentered(ctx, text, y, COLS, ink, paper)
227
- }
177
+ flashBorder(C.B_RED, 3, 150) // explosion 3 red flashes black
178
+ flashBorder(C.B_GREEN, 2, 200) // level complete
179
+ flashBorder(C.B_CYAN, 2, 120, C.BLUE) // flash → reset to blue border
228
180
  ```
229
181
 
230
182
  ---
231
183
 
232
184
  ### `audio.ts` — Web Audio engine (ZX Spectrum style)
233
185
 
234
- Wraps the Web Audio API to produce authentic 1-bit square-wave sound, exactly like the ZX Spectrum's single-channel beeper. All audio goes through a shared `AudioContext` and a single `GainNode` master bus.
186
+ Wraps the Web Audio API for authentic 1-bit square-wave sound. All audio goes through a shared `AudioContext` and a single master `GainNode`.
235
187
 
236
- **Important:** `AudioContext` must be created in response to a user gesture (click or keypress) due to browser autoplay policy. Call `initAudio()` from a key/click handler, then call `resumeAudio()` before playing sound in the game loop.
188
+ **Browser autoplay policy:** `AudioContext` must be created inside a user gesture (click or keydown). Call `initAudio()` from an event handler.
237
189
 
238
- #### Exports
190
+ #### `initAudio(volume?): void`
239
191
 
240
- ##### `initAudio(volume?: number): void`
241
-
242
- Creates the `AudioContext` and master `GainNode`. Idempotent — safe to call multiple times (subsequent calls are no-ops). Default volume: `0.3`.
192
+ Creates the `AudioContext` and master gain. Idempotent — safe to call multiple times.
243
193
 
244
194
  ```ts
245
- import { initAudio } from 'zx-kit'
246
-
247
- document.addEventListener('keydown', () => initAudio(), { once: true })
195
+ window.addEventListener('keydown', () => initAudio(), { once: true })
196
+ window.addEventListener('click', () => initAudio(), { once: true })
248
197
  ```
249
198
 
250
- ##### `resumeAudio(): void`
199
+ #### `resumeAudio(): void`
251
200
 
252
- Resumes a suspended `AudioContext`. Browsers suspend the context when the tab is hidden or on first load. Call this before scheduling any beep in the game loop.
201
+ Resumes a suspended `AudioContext`. Call before scheduling audio in the game loop.
253
202
 
254
- ```ts
255
- import { resumeAudio, beep, getAudioContext } from 'zx-kit'
203
+ #### `getAudioContext(): AudioContext | null`
256
204
 
257
- const ctx = getAudioContext()
258
- if (ctx) {
259
- resumeAudio()
260
- beep(440, 80, ctx.currentTime)
261
- }
262
- ```
205
+ Returns the current context, or `null` before `initAudio()`.
263
206
 
264
- ##### `getAudioContext(): AudioContext | null`
207
+ #### `getMasterGain(): GainNode | null`
265
208
 
266
- Returns the current `AudioContext`, or `null` if `initAudio()` has not been called yet. Use this to get `currentTime` for scheduling beeps.
209
+ Returns the master gain node. Connect custom oscillators here to respect global volume.
267
210
 
268
- ##### `getMasterGain(): GainNode | null`
269
-
270
- Returns the master `GainNode`. Connect your own oscillators/gains to this node to respect the global volume level. Returns `null` before `initAudio()`.
211
+ #### `Note` interface
271
212
 
272
213
  ```ts
273
- const osc = ctx.createOscillator()
274
- const gain = ctx.createGain()
275
- osc.connect(gain)
276
- gain.connect(getMasterGain()!)
214
+ interface Note {
215
+ freq: number // Hz — use 0 for a rest (silence)
216
+ dur: number // ms — duration of note or rest
217
+ }
277
218
  ```
278
219
 
279
- ##### `beep(freq: number, durationMs: number, startTime: number): void`
220
+ #### `playPattern(notes, startDelay?): void`
221
+
222
+ Schedules a sequence of notes. `freq: 0` = rest (advances time, no sound). `startDelay` delays the whole pattern in milliseconds.
223
+
224
+ ```ts
225
+ // Rising arpeggio
226
+ playPattern([
227
+ { freq: 262, dur: 100 }, // C4
228
+ { freq: 330, dur: 100 }, // E4
229
+ { freq: 392, dur: 100 }, // G4
230
+ { freq: 523, dur: 200 }, // C5
231
+ ])
232
+
233
+ // With rests and a 200ms startup delay
234
+ playPattern([
235
+ { freq: 523, dur: 120 }, // C5
236
+ { freq: 0, dur: 40 }, // rest
237
+ { freq: 784, dur: 200 }, // G5
238
+ ], 200)
239
+ ```
280
240
 
281
- Schedules a single square-wave beep. Uses a 5ms linear ramp attack and release to avoid click artifacts. The beep is routed through the master gain.
241
+ #### `beep(freq, durationMs, startTime): void`
282
242
 
283
- | Parameter | Type | Description |
284
- |-----------|------|-------------|
285
- | `freq` | `number` | Frequency in Hz |
286
- | `durationMs` | `number` | Duration in milliseconds |
287
- | `startTime` | `number` | `AudioContext.currentTime` offset to start at |
243
+ Schedules a single square-wave beep at an absolute `AudioContext.currentTime`. Use `playPattern` for sequences; use `beep` directly when you need algorithmic timing control.
288
244
 
289
245
  ```ts
290
- import { beep, resumeAudio, getAudioContext } from 'zx-kit'
291
-
292
- const ctx = getAudioContext()
293
- if (ctx) {
294
- resumeAudio()
295
- const now = ctx.currentTime
296
- // Two-pip warning: 880Hz beep twice with 60ms gap
297
- beep(880, 80, now)
298
- beep(880, 80, now + 0.14)
299
- }
246
+ const audio = getAudioContext()!
247
+ resumeAudio()
248
+ beep(880, 80, audio.currentTime)
249
+ beep(880, 80, audio.currentTime + 0.14) // 140ms later
300
250
  ```
301
251
 
302
252
  ---
303
253
 
304
254
  ### `input.ts` — Keyboard input with key-repeat
305
255
 
306
- Handles arrow key movement with configurable key-repeat (first-press immediate, hold for auto-repeat), plus single-consume flags for action keys. Designed for grid-based games where one key press = one step.
307
-
308
- **Important:** Call `initInput()` once at startup (after DOM is ready) to attach `keydown`/`keyup` listeners. Then call `tickMovement(dt)` every frame.
309
-
310
- #### Exports
256
+ Handles arrow-key movement with configurable key-repeat (immediate on first press, auto-repeat on hold) plus single-consume flags for action keys.
311
257
 
312
- ##### `initInput(repeatDelay?: number, repeatInterval?: number): void`
258
+ **Call `initInput()` once at startup.** Then call `tickMovement(dt)` every frame.
313
259
 
314
- Attaches global `keydown`/`keyup` event listeners. Default values:
315
- - `repeatDelay`: `150` ms — time before auto-repeat kicks in after initial press
316
- - `repeatInterval`: `80` ms — time between repeats while key is held
260
+ #### `Direction` type
317
261
 
318
262
  ```ts
319
- import { initInput } from 'zx-kit'
320
-
321
- initInput(150, 80)
263
+ type Direction = 'up' | 'down' | 'left' | 'right'
322
264
  ```
323
265
 
324
- ##### `tickMovement(dtMs: number): Direction | null`
266
+ #### `initInput(repeatDelay?, repeatInterval?): void`
325
267
 
326
- Call once per frame with the frame delta in milliseconds. Returns the direction to move this frame, or `null` if no movement. On the first frame a key is pressed, returns immediately. While held, returns a direction every `repeatInterval` ms (after initial `repeatDelay`).
268
+ Attaches `keydown`/`keyup` listeners. Keys: arrows = movement, `F` = flag, `P` = pause, `Ctrl+Shift+B` = debug.
327
269
 
328
270
  ```ts
329
- import { tickMovement } from 'zx-kit'
330
-
331
- function gameLoop(dtMs: number) {
332
- const dir = tickMovement(dtMs)
333
- if (dir) movePlayer(dir)
334
- }
271
+ initInput() // 150ms delay, 80ms repeat
272
+ initInput(200, 60) // custom timing
335
273
  ```
336
274
 
337
- ##### `consumeFlag(): boolean`
275
+ #### `tickMovement(dtMs): Direction | null`
338
276
 
339
- Returns `true` once if `F` key was pressed since the last call, then resets. Designed for the flag-placement action (mark suspected mine cell).
277
+ Returns the movement direction for this frame, or `null`. Call once per frame.
340
278
 
341
- ##### `consumeDebug(): boolean`
342
-
343
- Returns `true` once if `Ctrl+Shift+B` was pressed. Used to toggle debug mode (reveal all mines). Calls `e.preventDefault()` to suppress browser shortcuts.
344
-
345
- ##### `consumePause(): boolean`
346
-
347
- Returns `true` once if `P` key was pressed.
279
+ ```ts
280
+ const dir = tickMovement(dt)
281
+ if (dir) movePlayer(dir)
282
+ ```
348
283
 
349
- ##### `consumeAnyKey(): boolean`
284
+ #### Consume flags
350
285
 
351
- Returns `true` once if any key was pressed. Used to dismiss intro screens or game-over overlays.
286
+ | Function | Trigger | Use case |
287
+ |----------|---------|----------|
288
+ | `consumeFlag()` | `F` key | Flag / unflag a cell |
289
+ | `consumeDebug()` | `Ctrl+Shift+B` | Toggle debug mode |
290
+ | `consumePause()` | `P` key | Pause / unpause |
291
+ | `consumeAnyKey()` | Any key | Dismiss overlays, start game |
352
292
 
353
- ##### `isHeld(key: string): boolean`
293
+ Each returns `true` once per press, then resets.
354
294
 
355
- Returns whether a key is currently held. Argument is the `KeyboardEvent.key` string (e.g. `'ArrowUp'`, `' '`).
295
+ #### `isHeld(key): boolean`
356
296
 
357
- ##### `Direction` type
297
+ Returns whether a key is currently held. Argument is `KeyboardEvent.key`.
358
298
 
359
299
  ```ts
360
- export type Direction = 'up' | 'down' | 'left' | 'right'
300
+ if (isHeld('ArrowUp')) { ... }
361
301
  ```
362
302
 
363
303
  ---
@@ -366,46 +306,47 @@ export type Direction = 'up' | 'down' | 'left' | 'right'
366
306
 
367
307
  ```
368
308
  zx-kit/
369
- ├── package.json # { "exports": { ".": "./src/index.ts" } }
370
- ├── tsconfig.json # strict, bundler moduleResolution, noEmit
309
+ ├── package.json # exports: { ".": "./dist/index.js" }
310
+ ├── tsconfig.json # strict, emits to dist/
371
311
  ├── README.md
372
- └── src/
373
- ├── index.ts # barrel — re-exports everything
374
- ├── palette.ts # SCALE, CELL, C color constants, SpectrumColor type
375
- ├── font.ts # ROM font Uint8Array + getCharRow()
376
- ├── renderer.ts # mirrorSprite, drawSprite, drawChar, drawText, drawTextCentered
377
- ├── audio.ts # initAudio, resumeAudio, beep, getAudioContext, getMasterGain
378
- └── input.ts # initInput, tickMovement, consumeFlag/Debug/Pause/AnyKey, isHeld
312
+ ├── src/ # TypeScript source
313
+ ├── index.ts # barrel — re-exports everything
314
+ ├── palette.ts # SCALE, CELL, C, SpectrumColor
315
+ ├── font.ts # FONT, getCharRow
316
+ ├── renderer.ts # setupCanvas, mirrorSprite, drawSprite, drawChar, drawText,
317
+ │ │ # drawTextCentered, flashBorder
318
+ │ ├── audio.ts # initAudio, resumeAudio, beep, playPattern, Note,
319
+ │ │ # getAudioContext, getMasterGain
320
+ │ └── input.ts # initInput, tickMovement, consumeFlag/Debug/Pause/AnyKey,
321
+ │ # isHeld, Direction
322
+ └── dist/ # compiled output (generated by npm run build)
323
+ ├── index.js / .d.ts
324
+ └── ...
379
325
  ```
380
326
 
381
327
  ---
382
328
 
383
329
  ## Design principles
384
330
 
385
- - **No build step in the library** — exports raw TypeScript source. Consuming project's bundler (Vite, esbuild) handles transpilation.
331
+ - **Compiled distribution** — ships compiled JS + `.d.ts` in `dist/`. No bundler configuration needed in the consuming project.
386
332
  - **No runtime dependencies** — only Web platform APIs (`CanvasRenderingContext2D`, `AudioContext`, `KeyboardEvent`).
387
333
  - **Strict TypeScript** — `strict: true`, `noUnusedLocals`, `noUnusedParameters`. No `any`.
388
- - **Singleton state** — `audio.ts` and `input.ts` hold module-level state. Suitable for single-game use; not suitable for running multiple independent game instances in the same page.
389
- - **ZX Spectrum constraint enforcement** — palette, cell size, and font data are constants, not configuration. The library is deliberately opinionated.
334
+ - **Singleton state** — `audio.ts` and `input.ts` hold module-level state. Suitable for single-game use; not suitable for multiple independent game instances on the same page.
335
+ - **ZX Spectrum authenticity** — palette values, cell size, and font bytes are constants, not configuration. The library is deliberately opinionated.
390
336
 
391
337
  ---
392
338
 
393
- ## Migrating from inline code
394
-
395
- When extracting from a game that previously had these utilities inline:
396
-
397
- | What was inline | Now in zx-kit | Change required in game |
398
- |-----------------|---------------|------------------------|
399
- | `drawSprite`, `drawChar`, `drawText` | `renderer.ts` | Import from `'zx-kit'`; remove duplicate bodies |
400
- | `drawTextCentered` | `renderer.ts` (takes `cols` param) | Wrap with a local helper that binds your game's `COLS` |
401
- | `mirrorSprite` | `renderer.ts` | Import from `'zx-kit'`; keep sprite data in game's `sprites.ts` |
402
- | `C` color object | `palette.ts` | Re-export from game's `constants.ts` via `export { C } from 'zx-kit'` |
403
- | `SCALE`, `CELL` | `palette.ts` | Same as above |
404
- | `FONT`, `getCharRow` | `font.ts` | Re-export from game's `font.ts` |
405
- | `AudioContext` + `GainNode` init | `audio.ts` | Replace with `initAudio()` / `getAudioContext()` / `getMasterGain()` |
406
- | `beep()` helper | `audio.ts` | Import directly; remove inline version |
407
- | Arrow-key repeat logic | `input.ts` | Replace with `initInput()` + `tickMovement(dt)` |
408
- | Flag / debug / pause key flags | `input.ts` | Use `consumeFlag()`, `consumeDebug()`, `consumePause()` |
339
+ ## Local development
340
+
341
+ To work against a local checkout instead of the npm version:
342
+
343
+ ```bash
344
+ # In your game project
345
+ npm install ../zx-kit --prefer-online
346
+ ```
347
+
348
+ > The `--prefer-online` flag ensures npm resolves from the local path without caching issues.
349
+ > After publishing a new version to npm, switch back with `npm install zx-kit@latest`.
409
350
 
410
351
  ---
411
352