zx-kit 0.2.0 → 0.2.2

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,302 @@ 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, 4) // scale=4, 256×192 game px → 1024×768 CSS px
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
53
+
54
+ ### `palette.ts` — ZX Spectrum color constants
71
55
 
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 |
56
+ #### Exports
82
57
 
83
- #### Usage example
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` |
84
79
 
85
- ```ts
86
- import { C, CELL } from 'zx-kit'
80
+ Bright variants (`B_` prefix):
87
81
 
88
- ctx.fillStyle = C.B_CYAN // bright cyan
89
- ctx.fillRect(x, y, CELL, CELL)
90
- ```
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).
114
+ Every draw function follows the ZX Spectrum **ink / paper** model: each 8×8 cell has one foreground (ink) and one background (paper) color.
132
115
 
133
- #### Exports
116
+ #### `setupCanvas(canvas, scale, width?, height?): CanvasRenderingContext2D`
134
117
 
135
- ##### `mirrorSprite(src: Uint8Array): Uint8Array`
118
+ Initialises a canvas element for pixel-perfect scaled rendering. Sets canvas dimensions, applies CSS size, disables image smoothing, and calls `ctx.scale(scale, scale)` so all subsequent draw calls use game-pixel coordinates. **Replaces the manual canvas setup boilerplate.**
136
119
 
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.
120
+ - `scale` CSS pixels per game pixel (`4` = standard ZX Spectrum display)
121
+ - `width` — game pixels wide (default `256`)
122
+ - `height` — game pixels tall (default `192`)
138
123
 
139
124
  ```ts
140
- import { mirrorSprite } from 'zx-kit'
125
+ const canvas = document.getElementById('game') as HTMLCanvasElement
126
+ const ctx = setupCanvas(canvas, 4) // 256×192 game px → 1024×768 CSS px
127
+ const ctx = setupCanvas(canvas, 4, 256, 208) // taller canvas for status rows
128
+ // ctx.imageSmoothingEnabled is already false — no need to set it manually
129
+ // all draw calls use game-pixel coordinates — ctx.scale() is already applied
130
+ ```
131
+
132
+ #### `mirrorSprite(src): Uint8Array`
141
133
 
142
- export const PLAYER_RIGHT = new Uint8Array([0x18, 0x3C, ...])
134
+ Flips an 8×8 sprite horizontally. Returns a new `Uint8Array`.
135
+
136
+ ```ts
137
+ export const PLAYER_RIGHT = new Uint8Array([...])
143
138
  export const PLAYER_LEFT = mirrorSprite(PLAYER_RIGHT)
144
139
  ```
145
140
 
146
- ##### `drawSprite(ctx, sprite, x, y, ink, paper): void`
141
+ #### `drawSprite(ctx, sprite, x, y, ink, paper): void`
147
142
 
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) |
143
+ Draws an 8×8 sprite at game coordinates. Always fills the background first.
158
144
 
159
145
  ```ts
160
- import { drawSprite, C } from 'zx-kit'
161
-
162
- drawSprite(ctx, MINE_SPRITE, col * 8, row * 8, C.B_RED, C.BLACK)
146
+ drawSprite(ctx, MINE_SPRITE, col * CELL, row * CELL, C.B_RED, C.BLACK)
163
147
  ```
164
148
 
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).
149
+ #### `drawChar(ctx, code, x, y, ink, paper?): void`
168
150
 
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 |
151
+ Draws one ASCII character using the ROM font. Omit `paper` for transparent background.
177
152
 
178
153
  ```ts
179
- import { drawChar, C } from 'zx-kit'
180
-
181
154
  drawChar(ctx, 127, x, y, C.B_GREEN, C.BLACK) // solid block █
155
+ drawChar(ctx, 'A'.charCodeAt(0), x, y, C.B_WHITE)
182
156
  ```
183
157
 
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.
158
+ #### `drawText(ctx, text, x, y, ink, paper?): void`
187
159
 
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 |
160
+ Draws a string left-to-right, one character per `CELL`-wide slot.
196
161
 
197
162
  ```ts
198
- import { drawText, C } from 'zx-kit'
199
-
200
163
  drawText(ctx, 'SCORE:00000', 0, statusY, C.B_WHITE, C.BLACK)
201
164
  ```
202
165
 
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).
166
+ #### `drawTextCentered(ctx, text, y, cols, ink, paper?): void`
206
167
 
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 |
168
+ Centers a string within `cols` character columns. Bind `cols` once in a helper to avoid repetition.
215
169
 
216
170
  ```ts
217
- import { drawTextCentered, C } from 'zx-kit'
171
+ // Bind once:
172
+ const centered = (ctx: CanvasRenderingContext2D, text: string, y: number, ink: string) =>
173
+ drawTextCentered(ctx, text, y, 32, ink)
218
174
 
219
- drawTextCentered(ctx, 'GAME OVER', cy * 8, 32, C.B_RED, C.BLACK)
175
+ centered(ctx, 'GAME OVER', y, C.B_RED)
220
176
  ```
221
177
 
222
- **Tip for game code:** Bind `cols` once to avoid passing it everywhere:
178
+ #### `flashBorder(color, times, intervalMs, resetColor?): void`
179
+
180
+ 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
181
 
224
182
  ```ts
225
- function drawCentered(ctx: CanvasRenderingContext2D, text: string, y: number, ink: string, paper?: string) {
226
- drawTextCentered(ctx, text, y, COLS, ink, paper)
227
- }
183
+ flashBorder(C.B_RED, 3, 150) // explosion 3 red flashes black
184
+ flashBorder(C.B_GREEN, 2, 200) // level complete
185
+ flashBorder(C.B_CYAN, 2, 120, C.BLUE) // flash → reset to blue border
228
186
  ```
229
187
 
230
188
  ---
231
189
 
232
190
  ### `audio.ts` — Web Audio engine (ZX Spectrum style)
233
191
 
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.
192
+ 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
193
 
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.
194
+ **Browser autoplay policy:** `AudioContext` must be created inside a user gesture (click or keydown). Call `initAudio()` from an event handler.
237
195
 
238
- #### Exports
196
+ #### `initAudio(volume?): void`
239
197
 
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`.
198
+ Creates the `AudioContext` and master gain. Idempotent — safe to call multiple times.
243
199
 
244
200
  ```ts
245
- import { initAudio } from 'zx-kit'
246
-
247
- document.addEventListener('keydown', () => initAudio(), { once: true })
201
+ window.addEventListener('keydown', () => initAudio(), { once: true })
202
+ window.addEventListener('click', () => initAudio(), { once: true })
248
203
  ```
249
204
 
250
- ##### `resumeAudio(): void`
205
+ #### `resumeAudio(): void`
251
206
 
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.
207
+ Resumes a suspended `AudioContext`. Call before scheduling audio in the game loop.
253
208
 
254
- ```ts
255
- import { resumeAudio, beep, getAudioContext } from 'zx-kit'
209
+ #### `getAudioContext(): AudioContext | null`
256
210
 
257
- const ctx = getAudioContext()
258
- if (ctx) {
259
- resumeAudio()
260
- beep(440, 80, ctx.currentTime)
261
- }
262
- ```
211
+ Returns the current context, or `null` before `initAudio()`.
263
212
 
264
- ##### `getAudioContext(): AudioContext | null`
213
+ #### `getMasterGain(): GainNode | null`
265
214
 
266
- Returns the current `AudioContext`, or `null` if `initAudio()` has not been called yet. Use this to get `currentTime` for scheduling beeps.
215
+ Returns the master gain node. Connect custom oscillators here to respect global volume.
267
216
 
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()`.
217
+ #### `Note` interface
271
218
 
272
219
  ```ts
273
- const osc = ctx.createOscillator()
274
- const gain = ctx.createGain()
275
- osc.connect(gain)
276
- gain.connect(getMasterGain()!)
220
+ interface Note {
221
+ freq: number // Hz — use 0 for a rest (silence)
222
+ dur: number // ms — duration of note or rest
223
+ }
277
224
  ```
278
225
 
279
- ##### `beep(freq: number, durationMs: number, startTime: number): void`
226
+ #### `playPattern(notes, startDelay?): void`
227
+
228
+ Schedules a sequence of notes. `freq: 0` = rest (advances time, no sound). `startDelay` delays the whole pattern in milliseconds.
229
+
230
+ ```ts
231
+ // Rising arpeggio
232
+ playPattern([
233
+ { freq: 262, dur: 100 }, // C4
234
+ { freq: 330, dur: 100 }, // E4
235
+ { freq: 392, dur: 100 }, // G4
236
+ { freq: 523, dur: 200 }, // C5
237
+ ])
238
+
239
+ // With rests and a 200ms startup delay
240
+ playPattern([
241
+ { freq: 523, dur: 120 }, // C5
242
+ { freq: 0, dur: 40 }, // rest
243
+ { freq: 784, dur: 200 }, // G5
244
+ ], 200)
245
+ ```
280
246
 
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.
247
+ #### `beep(freq, durationMs, startTime): void`
282
248
 
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 |
249
+ 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
250
 
289
251
  ```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
- }
252
+ const audio = getAudioContext()!
253
+ resumeAudio()
254
+ beep(880, 80, audio.currentTime)
255
+ beep(880, 80, audio.currentTime + 0.14) // 140ms later
300
256
  ```
301
257
 
302
258
  ---
303
259
 
304
260
  ### `input.ts` — Keyboard input with key-repeat
305
261
 
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
262
+ Handles arrow-key movement with configurable key-repeat (immediate on first press, auto-repeat on hold) plus single-consume flags for action keys.
311
263
 
312
- ##### `initInput(repeatDelay?: number, repeatInterval?: number): void`
264
+ **Call `initInput()` once at startup.** Then call `tickMovement(dt)` every frame.
313
265
 
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
266
+ #### `Direction` type
317
267
 
318
268
  ```ts
319
- import { initInput } from 'zx-kit'
320
-
321
- initInput(150, 80)
269
+ type Direction = 'up' | 'down' | 'left' | 'right'
322
270
  ```
323
271
 
324
- ##### `tickMovement(dtMs: number): Direction | null`
272
+ #### `initInput(repeatDelay?, repeatInterval?): void`
325
273
 
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`).
274
+ Attaches `keydown`/`keyup` listeners. Keys: arrows = movement, `F` = flag, `P` = pause, `Ctrl+Shift+B` = debug.
327
275
 
328
276
  ```ts
329
- import { tickMovement } from 'zx-kit'
330
-
331
- function gameLoop(dtMs: number) {
332
- const dir = tickMovement(dtMs)
333
- if (dir) movePlayer(dir)
334
- }
277
+ initInput() // 150ms delay, 80ms repeat
278
+ initInput(200, 60) // custom timing
335
279
  ```
336
280
 
337
- ##### `consumeFlag(): boolean`
281
+ #### `tickMovement(dtMs): Direction | null`
338
282
 
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).
283
+ Returns the movement direction for this frame, or `null`. Call once per frame.
340
284
 
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.
285
+ ```ts
286
+ const dir = tickMovement(dt)
287
+ if (dir) movePlayer(dir)
288
+ ```
348
289
 
349
- ##### `consumeAnyKey(): boolean`
290
+ #### Consume flags
350
291
 
351
- Returns `true` once if any key was pressed. Used to dismiss intro screens or game-over overlays.
292
+ | Function | Trigger | Use case |
293
+ |----------|---------|----------|
294
+ | `consumeFlag()` | `F` key | Flag / unflag a cell |
295
+ | `consumeDebug()` | `Ctrl+Shift+B` | Toggle debug mode |
296
+ | `consumePause()` | `P` key | Pause / unpause |
297
+ | `consumeAnyKey()` | Any key | Dismiss overlays, start game |
352
298
 
353
- ##### `isHeld(key: string): boolean`
299
+ Each returns `true` once per press, then resets.
354
300
 
355
- Returns whether a key is currently held. Argument is the `KeyboardEvent.key` string (e.g. `'ArrowUp'`, `' '`).
301
+ #### `isHeld(key): boolean`
356
302
 
357
- ##### `Direction` type
303
+ Returns whether a key is currently held. Argument is `KeyboardEvent.key`.
358
304
 
359
305
  ```ts
360
- export type Direction = 'up' | 'down' | 'left' | 'right'
306
+ if (isHeld('ArrowUp')) { ... }
361
307
  ```
362
308
 
363
309
  ---
@@ -366,46 +312,47 @@ export type Direction = 'up' | 'down' | 'left' | 'right'
366
312
 
367
313
  ```
368
314
  zx-kit/
369
- ├── package.json # { "exports": { ".": "./src/index.ts" } }
370
- ├── tsconfig.json # strict, bundler moduleResolution, noEmit
315
+ ├── package.json # exports: { ".": "./dist/index.js" }
316
+ ├── tsconfig.json # strict, emits to dist/
371
317
  ├── 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
318
+ ├── src/ # TypeScript source
319
+ ├── index.ts # barrel — re-exports everything
320
+ ├── palette.ts # SCALE, CELL, C, SpectrumColor
321
+ ├── font.ts # FONT, getCharRow
322
+ ├── renderer.ts # setupCanvas, mirrorSprite, drawSprite, drawChar, drawText,
323
+ │ │ # drawTextCentered, flashBorder
324
+ │ ├── audio.ts # initAudio, resumeAudio, beep, playPattern, Note,
325
+ │ │ # getAudioContext, getMasterGain
326
+ │ └── input.ts # initInput, tickMovement, consumeFlag/Debug/Pause/AnyKey,
327
+ │ # isHeld, Direction
328
+ └── dist/ # compiled output (generated by npm run build)
329
+ ├── index.js / .d.ts
330
+ └── ...
379
331
  ```
380
332
 
381
333
  ---
382
334
 
383
335
  ## Design principles
384
336
 
385
- - **No build step in the library** — exports raw TypeScript source. Consuming project's bundler (Vite, esbuild) handles transpilation.
337
+ - **Compiled distribution** — ships compiled JS + `.d.ts` in `dist/`. No bundler configuration needed in the consuming project.
386
338
  - **No runtime dependencies** — only Web platform APIs (`CanvasRenderingContext2D`, `AudioContext`, `KeyboardEvent`).
387
339
  - **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.
340
+ - **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.
341
+ - **ZX Spectrum authenticity** — palette values, cell size, and font bytes are constants, not configuration. The library is deliberately opinionated.
390
342
 
391
343
  ---
392
344
 
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()` |
345
+ ## Local development
346
+
347
+ To work against a local checkout instead of the npm version:
348
+
349
+ ```bash
350
+ # In your game project
351
+ npm install ../zx-kit --prefer-online
352
+ ```
353
+
354
+ > The `--prefer-online` flag ensures npm resolves from the local path without caching issues.
355
+ > After publishing a new version to npm, switch back with `npm install zx-kit@latest`.
409
356
 
410
357
  ---
411
358