zx-kit 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,6 +31,7 @@ The goal is simple: **it should look and sound like a Spectrum, but run like a m
31
31
  - **Keyboard input** — configurable key-repeat, single-consume action flags, instant state reset on phase transitions
32
32
  - **ZX-style UI widgets** — progress bars with managed lifetime, boxes, frames, panel titles
33
33
  - **Typed save / load** — persistent saves via `localStorage` with schema versioning, migrations, slot enumeration, in-memory throttling, and discriminated Result types for every failure mode
34
+ - **Runtime locale switching** — type-safe string-pack selection via `pickLocale()`, so a game can switch language while running — unimaginable on the original Spectrum, natural in the browser
34
35
  - **Zero dependencies** — only Web platform APIs: `Canvas`, `Web Audio`, `KeyboardEvent`
35
36
  - **Tree-shakeable** — `sideEffects: false`, so unused modules are dropped from your production bundle
36
37
  - **TypeScript-first** — strict mode, full `.d.ts` declarations, no `any`
@@ -545,8 +546,12 @@ requestAnimationFrame(loop)
545
546
  | [`scene.ts`](#scenets--scene-manager) | Stack-based scene manager with onEnter/onExit/onPause/onResume hooks |
546
547
  | [`save.ts`](#savets--typed-save--load) | Typed save/load via callbacks, versioning + migrations, slot enumeration, throttling, Result types |
547
548
  | [`tilemap.ts`](#tilemapts--tile-map-engine) | Scrollable maps, solid tiles, O(1) id-index, background swap |
549
+ | [`tilescroll.ts`](#tilescrollts--pixel-smooth-scrolling) | Pixel-smooth tile-map rendering at any camera position (sub-tile scroll) |
550
+ | [`particles.ts`](#particlests--particle-pool) | Allocation-free particle pool for pixel effects: sparks, dust, puffs |
551
+ | [`rng.ts`](#rngts--seeded-rng) | Seeded deterministic PRNG (mulberry32): int/range/float/chance/pick/shuffle/fork |
548
552
  | [`palette.ts`](#palettets--color-constants) | 15 Spectrum colors, `SpectrumColor` type, `CELL`, `SCALE` |
549
553
  | [`font.ts`](#fontts--rom-bitmap-font) | 96-character ROM font, raw bitmap access |
554
+ | [`i18n.ts`](#i18nts--runtime-locale-selection) | Type-safe runtime locale selection for translated string packs |
550
555
 
551
556
  ---
552
557
 
@@ -902,6 +907,36 @@ curveDisplay(ctx) // default strength
902
907
  curveDisplay(ctx, 256, 208, 6) // stronger warp
903
908
  ```
904
909
 
910
+ ### `createBitmapFromRows(rows): Bitmap`
911
+
912
+ Builds an arbitrary-size `Bitmap` from readable pixel-art rows instead of
913
+ hand-packed bytes. This is useful for sprites larger than 8x8 where hex arrays
914
+ become hard to review.
915
+
916
+ - `X` or `#` = solid pixel
917
+ - `.` or space = transparent pixel
918
+ - every row must have the same width
919
+ - width must be a positive multiple of 8
920
+
921
+ ```ts
922
+ const TRUCK = createBitmapFromRows([
923
+ '....XXXXXXXX....',
924
+ '..XXXXXXXXXXXX..',
925
+ '.XXXX......XXXX.',
926
+ 'XXXXXXXXXXXXXXXX',
927
+ 'XX..XXXXXXXX..XX',
928
+ 'XX............XX',
929
+ '..XXX......XXX..',
930
+ '................',
931
+ ])
932
+
933
+ drawBitmap(ctx, TRUCK, x, y, C.B_WHITE)
934
+ ```
935
+
936
+ The returned object is the same `Bitmap` shape produced by `createBitmap()`, so
937
+ it works with `drawBitmap`, `drawBitmapAttrs`, `mirrorBitmap`, and collision
938
+ helpers such as `bitmapPixelMask`.
939
+
905
940
  ---
906
941
 
907
942
  ## `audio.ts` — Beeper Audio
@@ -1637,6 +1672,8 @@ When `groundContact` is 0, a circle-shaped hero hanging over a tile edge won't t
1637
1672
  | Bullet vs. irregular boss sprite | `masksOverlap` — pixel-precise, returns overlap count for damage |
1638
1673
  | Off-road detection for a truck with a bumpy silhouette | `pixelSolidCount` / custom mask loop — checks each opaque pixel against road boundary |
1639
1674
 
1675
+ For a step-by-step walkthrough of both tiers — including how to combine AABB and pixel-precise in one loop and how to handle non-tile boundaries — see **[docs/collision.md](docs/collision.md)**.
1676
+
1640
1677
  ---
1641
1678
 
1642
1679
  ## `animation.ts` — Frame Timer & Tween
@@ -2192,6 +2229,201 @@ for (let row = 0; row < 8; row++) {
2192
2229
 
2193
2230
  ---
2194
2231
 
2232
+ ## `i18n.ts` — Runtime Locale Selection
2233
+
2234
+ A tiny helper for choosing a translated string pack at runtime. The original ZX Spectrum could not realistically swap whole languages while a game was running; zx-kit keeps the Spectrum presentation, but lets browser games offer modern language switching without a framework or dependency.
2235
+
2236
+ ### `pickLocale(defaultLocale, locales, code): T`
2237
+
2238
+ Selects a locale object by language code and falls back to the default locale when the code is missing, empty, or unknown. Matching is case-insensitive.
2239
+
2240
+ ```ts
2241
+ import { pickLocale } from 'zx-kit'
2242
+ import * as en from './strings'
2243
+ import * as sk from './strings.sk'
2244
+ import * as ru from './strings.ru'
2245
+
2246
+ let languageCode = localStorage.getItem('language') // e.g. 'sk'
2247
+ let L = pickLocale(en, { sk, ru }, languageCode)
2248
+
2249
+ drawText(ctx, L.STR_PRESS_START, 32, 88, C.B_WHITE, C.BLACK)
2250
+
2251
+ function setLanguage(code: string): void {
2252
+ languageCode = code
2253
+ localStorage.setItem('language', code)
2254
+ L = pickLocale(en, { sk, ru }, languageCode)
2255
+ }
2256
+ ```
2257
+
2258
+ Every locale must have the same shape as the default locale. Because `pickLocale` is generic over that shape, missing keys and wrong function signatures are caught by TypeScript:
2259
+
2260
+ ```ts
2261
+ // strings.ts
2262
+ export const STR_PRESS_START = 'PRESS START'
2263
+ export const STR_DEPTH = (m: number) => `D:${m}M`
2264
+
2265
+ // strings.sk.ts
2266
+ export const STR_PRESS_START = 'STLAC START'
2267
+ export const STR_DEPTH = (m: number) => `H:${m}M`
2268
+ ```
2269
+
2270
+ Selection rules:
2271
+
2272
+ | Code | Result |
2273
+ |------|--------|
2274
+ | `null`, `undefined`, `''` | `defaultLocale` |
2275
+ | `'sk'`, `'SK'`, `'Sk'` | `locales.sk` |
2276
+ | unknown code | `defaultLocale` |
2277
+ | `'en'` when `en` is not in `locales` | `defaultLocale` |
2278
+
2279
+ The default language does not need its own `strings.en.ts` file. Keep the source language at `strings.ts`, put only additional translations in the locale map, and call `pickLocale` again whenever the player changes language.
2280
+
2281
+ ---
2282
+
2283
+ ## `tilescroll.ts` — Pixel-Smooth Scrolling
2284
+
2285
+ [`tilemap.ts`](#tilemapts--tile-map-engine)'s `render(viewport)` takes a viewport in **whole tiles**, so a camera can only move in 8-pixel steps. That is perfect for grid games, but visibly steppy in a platformer where the player moves sub-pixel-smooth while jumping. `tilescroll.ts` renders the map at an **arbitrary pixel camera position** by drawing one overscan row/column and offsetting every tile by the camera remainder.
2286
+
2287
+ Pair it with [`camera.ts`](#camerats--scrolling-camera): use `tileMapWorldSize` for the camera's `worldW` / `worldH`, then feed `cam.x` / `cam.y` straight into `drawTileMapAt`.
2288
+
2289
+ ### `tileMapWorldSize(map): { width, height }`
2290
+
2291
+ Returns the map's full size in pixels (`cols × CELL`, `rows × CELL`) — handy for the camera's world bounds.
2292
+
2293
+ ```ts
2294
+ import { createCamera } from 'zx-kit'
2295
+ const cam = createCamera({ viewW: 256, viewH: 192, ...tileMapWorldSize(map) })
2296
+ ```
2297
+
2298
+ ### `drawTileMapAt(ctx, map, camX, camY, viewW?, viewH?): void`
2299
+
2300
+ Renders `map` with the viewport's top-left at world pixel `(camX, camY)`. The camera position is rounded to whole pixels for crisp output; off-screen and empty cells are skipped. `viewW` (default `256`) and `viewH` (default `192`) must be positive — throws otherwise. The leading/trailing partial tiles are drawn and naturally clipped by the canvas bounds, so keep the play area at the canvas origin (or render the map first) to avoid spilling under a status bar.
2301
+
2302
+ ```ts
2303
+ // game loop
2304
+ setCameraTarget(cam, player.x, player.y)
2305
+ tickCamera(cam, dt)
2306
+ drawTileMapAt(ctx, map, cam.x, cam.y) // smooth background
2307
+ renderSprite(ctx, /* player drawn at (x - cam.x, y - cam.y) */)
2308
+ ```
2309
+
2310
+ ---
2311
+
2312
+ ## `particles.ts` — Particle Pool
2313
+
2314
+ An **allocation-free** particle pool for ZX-style pixel effects: carrot-shot sparks, landing dust, an enemy curling into a puff, glowing motes around a crystal. The pool is created once at startup with a fixed capacity; emitting and ticking never allocate, so it is safe to run every frame. Particles are plain coloured squares drawn in the Spectrum palette. Pass an `rng` for deterministic effects (replays, seeded worlds); otherwise it uses `Math.random`.
2315
+
2316
+ ### `Particle` / `ParticleSystem` interfaces
2317
+
2318
+ ```ts
2319
+ interface Particle {
2320
+ x: number; y: number // world pixels
2321
+ vx: number; vy: number // px per ms
2322
+ life: number; maxLife: number // ms (fade with life / maxLife)
2323
+ color: SpectrumColor
2324
+ size: number // square side in px
2325
+ active: boolean // false slots are free for reuse
2326
+ }
2327
+
2328
+ interface ParticleSystem {
2329
+ readonly particles: Particle[] // length === capacity
2330
+ readonly capacity: number
2331
+ activeCount: number
2332
+ }
2333
+ ```
2334
+
2335
+ ### `createParticleSystem(capacity): ParticleSystem`
2336
+
2337
+ Creates a pool of `capacity` particles, all inactive. Throws when `capacity` is not a positive integer.
2338
+
2339
+ ### `emitParticles(ps, opts): number`
2340
+
2341
+ Emits up to `opts.count` particles and returns the number actually emitted (fewer when the pool is full). Throws when `count` is negative or non-integer.
2342
+
2343
+ | Option | Type | Default | Meaning |
2344
+ |--------|------|---------|---------|
2345
+ | `x`, `y` | `number` | — | spawn position (world px) |
2346
+ | `count` | `number` | — | how many to emit |
2347
+ | `color` | `SpectrumColor \| SpectrumColor[]` | — | single colour, or palette to pick per particle |
2348
+ | `speed` | `number \| [min, max]` | `0.03` | px/ms |
2349
+ | `angle` | `number` | `0` | base direction in radians (`-π/2` = up) |
2350
+ | `spread` | `number` | `0` | angular jitter centred on `angle` |
2351
+ | `life` | `number \| [min, max]` | `300` | lifetime in ms |
2352
+ | `size` | `number` | `1` | square side in px |
2353
+ | `rng` | `() => number` | `Math.random` | deterministic source |
2354
+
2355
+ ```ts
2356
+ const sparks = createParticleSystem(128)
2357
+
2358
+ // on carrot impact — a fan of yellow/white sparks shooting upward
2359
+ emitParticles(sparks, {
2360
+ x: hit.x, y: hit.y, count: 12,
2361
+ color: [C.B_YELLOW, C.B_WHITE],
2362
+ speed: [0.02, 0.06], angle: -Math.PI / 2, spread: Math.PI,
2363
+ life: [200, 400],
2364
+ })
2365
+ ```
2366
+
2367
+ ### `tickParticles(ps, dtMs, gravity?): void`
2368
+
2369
+ Advances every active particle by `dtMs`, applying optional `gravity` (px/ms², default `0`) to vertical velocity. Expired particles are deactivated and returned to the pool.
2370
+
2371
+ ### `renderParticles(ctx, ps, offsetX?, offsetY?): void`
2372
+
2373
+ Draws every active particle as a filled square, rounding world coordinates to whole pixels. Subtract the camera world position via `offsetX` / `offsetY` to convert world → screen.
2374
+
2375
+ ### `clearParticles(ps): void`
2376
+
2377
+ Deactivates all particles immediately (e.g. on room change).
2378
+
2379
+ ```ts
2380
+ // game loop
2381
+ tickParticles(sparks, dt, 0.0004) // gentle gravity
2382
+ renderParticles(ctx, sparks, cam.x, cam.y) // scrolled world
2383
+ ```
2384
+
2385
+ ---
2386
+
2387
+ ## `rng.ts` — Seeded RNG
2388
+
2389
+ A **seeded deterministic** pseudo-random generator: the same seed produces the same sequence on every machine and every run — exactly what procedural worlds need. Built on **mulberry32** (fast, allocation-free, good statistical quality for games). It is **not** cryptographically secure.
2390
+
2391
+ The call order is part of the determinism contract: call the methods in the same order to reproduce a world.
2392
+
2393
+ ### `createRng(seed): Rng`
2394
+
2395
+ Creates a generator from a `string` (hashed via `hashSeed`) or a finite `number` (coerced to uint32). Throws on a non-finite numeric seed.
2396
+
2397
+ ### `Rng` methods
2398
+
2399
+ | Method | Returns | Throws when |
2400
+ |--------|---------|-------------|
2401
+ | `next()` | float `[0, 1)` | — |
2402
+ | `int(maxExclusive)` | int `[0, max)` | `max` not a positive integer |
2403
+ | `range(min, max)` | int `[min, max)` | bounds non-integer, or `max <= min` |
2404
+ | `float(min, max)` | float `[min, max)` | `max < min` |
2405
+ | `chance(p)` | boolean (`true` ~`p`) | `p` outside `[0, 1]` |
2406
+ | `pick(items)` | random element | `items` empty |
2407
+ | `shuffle(items)` | same array, shuffled in place | — |
2408
+ | `fork()` | independent `Rng` (advances parent one step) | — |
2409
+
2410
+ ```ts
2411
+ const rng = createRng('cave-level-7')
2412
+ const roomCount = rng.range(3, 7)
2413
+ const theme = rng.pick(['spider', 'centipede', 'crystal'])
2414
+ if (rng.chance(0.15)) placeSecret()
2415
+
2416
+ // independent sub-streams so adding enemies doesn't shift terrain layout
2417
+ const terrainRng = rng.fork()
2418
+ const enemyRng = rng.fork()
2419
+ ```
2420
+
2421
+ ### `hashSeed(seed): number`
2422
+
2423
+ Hashes a string to an unsigned 32-bit integer (FNV-1a). Deterministic; exported for keying sub-streams by name.
2424
+
2425
+ ---
2426
+
2195
2427
  ## Architecture
2196
2428
 
2197
2429
  ### Module structure
@@ -2205,9 +2437,8 @@ zx-kit/
2205
2437
  │ ├── index.ts # barrel — re-exports everything
2206
2438
  │ ├── palette.ts # SCALE, CELL, C, SpectrumColor
2207
2439
  │ ├── font.ts # FONT, getCharRow
2208
- │ ├── renderer.ts # setupCanvas, mirrorSprite, drawSprite, drawChar,
2209
- │ │ # drawText, drawTextCentered, flashBorder,
2210
- │ │ # drawScanlines, curveDisplay
2440
+ │ ├── renderer.ts # canvas setup, 8×8 sprites, arbitrary-size Bitmap,
2441
+ │ │ # AttrMap colour attributes, text, scanlines, border flash
2211
2442
  │ ├── audio.ts # initAudio, resumeAudio, beep, playPattern,
2212
2443
  │ │ # getAudioContext, getMasterGain,
2213
2444
  │ │ # getMasterVolume, setMasterVolume,
@@ -2218,15 +2449,20 @@ zx-kit/
2218
2449
  │ │ # consumePause, consumeDebug, consumeAnyKey,
2219
2450
  │ │ # isHeld, resetInput, Direction
2220
2451
  │ ├── ui.ts # drawBox, drawFrame, drawPanelTitle,
2221
- │ │ # drawProgressBar, tickUI, renderUI, resetUI,
2222
- │ │ # BorderOptions, DrawProgressBarOptions
2452
+ │ │ # instrumentation widgets, progress bars
2223
2453
  │ ├── tilemap.ts # createTileMap, Tile, Viewport, TileMap
2454
+ │ ├── tilescroll.ts # drawTileMapAt, tileMapWorldSize (sub-pixel scroll)
2224
2455
  │ ├── sprite.ts # createSprite, moveSprite, applyGravity,
2225
2456
  │ │ # renderSprite, Sprite
2226
- └── collision.ts # spriteRect, bitmapRect, rectsOverlap, spritesOverlap,
2227
- # isSolidAt, resolveRectX, resolveRectY, resolveX, resolveY,
2228
- # bitmapPixelMask, masksOverlap, pixelSolidCount,
2229
- # Rect, PixelMask
2457
+ ├── collision.ts # AABB, rect-vs-tile, pixel-precise masks
2458
+ ├── particles.ts # createParticleSystem, emitParticles,
2459
+ # tickParticles, renderParticles, clearParticles
2460
+ ├── rng.ts # createRng, hashSeed (seeded mulberry32)
2461
+ │ ├── animation.ts # frame timers, tweens, blinkers
2462
+ │ ├── camera.ts # scrolling viewport, lerp, deadzone, bounds
2463
+ │ ├── scene.ts # stack-based scene manager
2464
+ │ ├── save.ts # typed localStorage save/load with migrations
2465
+ │ └── i18n.ts # pickLocale runtime locale selection
2230
2466
  └── dist/ # compiled output (npm run build)
2231
2467
  ├── index.js
2232
2468
  ├── index.d.ts
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pixel-perfect collision — visual ground truth test.
3
+ *
4
+ * Two bitmaps: 8×8 px each, but only a 2×2 px region is lit
5
+ * (rows 0–1, cols 0–1). The effective sprite shape is 2×2 — a size that
6
+ * is intentionally NOT a multiple of the ZX tile size (8 px).
7
+ *
8
+ * col: 01234567
9
+ * row 0: ██...... (0xC0)
10
+ * row 1: ██...... (0xC0)
11
+ * rows 2–7: empty
12
+ *
13
+ * Ground truth: a brute-force Set-of-pixel-coords intersection.
14
+ * Both sprites are projected to absolute (x,y) coordinates, all lit pixels
15
+ * are collected into a flat Set, and any shared coordinate counts as overlap.
16
+ * No formulas, no assumptions — just "which pixels are painted and do any
17
+ * coordinates match?"
18
+ *
19
+ * Every assertion compares masksOverlap against this ground truth, sweeping
20
+ * sprite B toward sprite A one pixel at a time in all four directions and
21
+ * diagonally. A separate block shows that AABB (8×8 bounding-box) fires
22
+ * 6 pixels earlier than pixel-perfect — the gap the ground truth exposes.
23
+ */
24
+ export {};
25
+ //# sourceMappingURL=collision.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collision.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/collision.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Pixel-perfect collision — visual ground truth test.
3
+ *
4
+ * Two bitmaps: 8×8 px each, but only a 2×2 px region is lit
5
+ * (rows 0–1, cols 0–1). The effective sprite shape is 2×2 — a size that
6
+ * is intentionally NOT a multiple of the ZX tile size (8 px).
7
+ *
8
+ * col: 01234567
9
+ * row 0: ██...... (0xC0)
10
+ * row 1: ██...... (0xC0)
11
+ * rows 2–7: empty
12
+ *
13
+ * Ground truth: a brute-force Set-of-pixel-coords intersection.
14
+ * Both sprites are projected to absolute (x,y) coordinates, all lit pixels
15
+ * are collected into a flat Set, and any shared coordinate counts as overlap.
16
+ * No formulas, no assumptions — just "which pixels are painted and do any
17
+ * coordinates match?"
18
+ *
19
+ * Every assertion compares masksOverlap against this ground truth, sweeping
20
+ * sprite B toward sprite A one pixel at a time in all four directions and
21
+ * diagonally. A separate block shows that AABB (8×8 bounding-box) fires
22
+ * 6 pixels earlier than pixel-perfect — the gap the ground truth exposes.
23
+ */
24
+ import { describe, it, expect } from 'vitest';
25
+ import { bitmapPixelMask, masksOverlap, rectsOverlap } from '../collision.js';
26
+ import { createBitmap } from '../renderer.js';
27
+ // ── Fixture ───────────────────────────────────────────────────────────────────
28
+ // width=8 (minimum for Bitmap: 1 byte per row). height=8.
29
+ // Lit region: rows 0–1, cols 0–1. All other bytes 0x00.
30
+ const BMP = createBitmap(new Uint8Array([
31
+ 0xC0, // row 0: bits 7,6 set → cols 0,1
32
+ 0xC0, // row 1: bits 7,6 set → cols 0,1
33
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rows 2–7: empty
34
+ ]), 8, 8);
35
+ const MASK = bitmapPixelMask(BMP);
36
+ // ── Brute-force ground truth ──────────────────────────────────────────────────
37
+ // Project every lit pixel of a mask to world (x,y). Put them in a Set.
38
+ // Check if any pixel of B appears in the Set of A. Completely explicit —
39
+ // no clever algorithm, just coordinate comparison.
40
+ function litPixels(mask, ox, oy) {
41
+ const s = new Set();
42
+ for (let row = 0; row < mask.height; row++) {
43
+ for (const col of mask.rows[row]) {
44
+ s.add(`${ox + col},${oy + row}`);
45
+ }
46
+ }
47
+ return s;
48
+ }
49
+ function bruteForce(maskA, ax, ay, maskB, bx, by) {
50
+ const setA = litPixels(maskA, ax, ay);
51
+ for (let row = 0; row < maskB.height; row++) {
52
+ for (const col of maskB.rows[row]) {
53
+ if (setA.has(`${bx + col},${by + row}`))
54
+ return true;
55
+ }
56
+ }
57
+ return false;
58
+ }
59
+ // ─────────────────────────────────────────────────────────────────────────────
60
+ describe('masksOverlap — 2×2 lit region, visual ground truth', () => {
61
+ // ── Sanity-check the mask ─────────────────────────────────────────────────
62
+ describe('mask shape', () => {
63
+ it('has exactly 4 lit pixels (the 2×2 region)', () => {
64
+ expect(MASK.totalPixels).toBe(4);
65
+ });
66
+ it('rows 0–1 contain columns [0, 1]; rows 2–7 are empty', () => {
67
+ expect(MASK.rows[0]).toEqual([0, 1]);
68
+ expect(MASK.rows[1]).toEqual([0, 1]);
69
+ for (let r = 2; r < MASK.height; r++) {
70
+ expect(MASK.rows[r]).toEqual([]);
71
+ }
72
+ });
73
+ });
74
+ // ── Horizontal sweep ──────────────────────────────────────────────────────
75
+ // A is fixed at (20,20). B sweeps from right (x=30) to left (x=17), same y.
76
+ // A's lit pixels: x∈[20,21], y∈[20,21].
77
+ // First contact: bx=21 — B column 0 (x=21) meets A column 1 (x=21).
78
+ // AABB first contact: bx=27 — 8×8 boxes share one column. Gap = 6 px.
79
+ describe('horizontal sweep: B approaches A from the right', () => {
80
+ const AX = 20, AY = 20;
81
+ it('masksOverlap matches brute-force at every pixel step', () => {
82
+ for (let bx = AX + 10; bx >= AX - 3; bx--) {
83
+ const expected = bruteForce(MASK, AX, AY, MASK, bx, AY);
84
+ const actual = masksOverlap(MASK, AX, AY, MASK, bx, AY) > 0;
85
+ if (actual !== expected) {
86
+ throw new Error(`bx=${bx}: masksOverlap=${actual} but brute-force says ${expected}`);
87
+ }
88
+ }
89
+ });
90
+ it('no collision at bx = AX+2 — 1-pixel gap between lit edges', () => {
91
+ // A's rightmost lit column is AX+1=21. B's leftmost at bx=AX+2=22. Gap 1 px.
92
+ expect(masksOverlap(MASK, AX, AY, MASK, AX + 2, AY)).toBe(0);
93
+ expect(bruteForce(MASK, AX, AY, MASK, AX + 2, AY)).toBe(false);
94
+ });
95
+ it('collision fires at bx = AX+1 — lit edges share column AX+1', () => {
96
+ expect(masksOverlap(MASK, AX, AY, MASK, AX + 1, AY)).toBeGreaterThan(0);
97
+ expect(bruteForce(MASK, AX, AY, MASK, AX + 1, AY)).toBe(true);
98
+ });
99
+ it('AABB (8×8 box) fires 6 pixels before pixel-perfect', () => {
100
+ const boxA = { x: AX, y: AY, w: 8, h: 8 };
101
+ // AABB says: first overlap at bx=AX+7 (right edge of A box = left edge of B box)
102
+ expect(rectsOverlap(boxA, { x: AX + 8, y: AY, w: 8, h: 8 })).toBe(false);
103
+ expect(rectsOverlap(boxA, { x: AX + 7, y: AY, w: 8, h: 8 })).toBe(true);
104
+ // Pixel-perfect: no lit pixels near each other at bx=AX+7 — false positive
105
+ expect(masksOverlap(MASK, AX, AY, MASK, AX + 7, AY)).toBe(0);
106
+ expect(bruteForce(MASK, AX, AY, MASK, AX + 7, AY)).toBe(false);
107
+ });
108
+ });
109
+ // ── Vertical sweep ────────────────────────────────────────────────────────
110
+ // A fixed at (20,20). B sweeps from below (y=30) upward (y=17), same x.
111
+ // First contact: by=21 — B row 0 (y=21) meets A row 1 (y=21).
112
+ // AABB first contact: by=27. Gap = 6 px.
113
+ describe('vertical sweep: B approaches A from below', () => {
114
+ const AX = 20, AY = 20;
115
+ it('masksOverlap matches brute-force at every pixel step', () => {
116
+ for (let by = AY + 10; by >= AY - 3; by--) {
117
+ const expected = bruteForce(MASK, AX, AY, MASK, AX, by);
118
+ const actual = masksOverlap(MASK, AX, AY, MASK, AX, by) > 0;
119
+ if (actual !== expected) {
120
+ throw new Error(`by=${by}: masksOverlap=${actual} but brute-force says ${expected}`);
121
+ }
122
+ }
123
+ });
124
+ it('no collision at by = AY+2 — 1-pixel gap between lit rows', () => {
125
+ expect(masksOverlap(MASK, AX, AY, MASK, AX, AY + 2)).toBe(0);
126
+ expect(bruteForce(MASK, AX, AY, MASK, AX, AY + 2)).toBe(false);
127
+ });
128
+ it('collision fires at by = AY+1 — lit rows share row AY+1', () => {
129
+ expect(masksOverlap(MASK, AX, AY, MASK, AX, AY + 1)).toBeGreaterThan(0);
130
+ expect(bruteForce(MASK, AX, AY, MASK, AX, AY + 1)).toBe(true);
131
+ });
132
+ it('AABB fires 6 pixels before pixel-perfect', () => {
133
+ const boxA = { x: AX, y: AY, w: 8, h: 8 };
134
+ expect(rectsOverlap(boxA, { x: AX, y: AY + 8, w: 8, h: 8 })).toBe(false);
135
+ expect(rectsOverlap(boxA, { x: AX, y: AY + 7, w: 8, h: 8 })).toBe(true);
136
+ expect(masksOverlap(MASK, AX, AY, MASK, AX, AY + 7)).toBe(0);
137
+ expect(bruteForce(MASK, AX, AY, MASK, AX, AY + 7)).toBe(false);
138
+ });
139
+ });
140
+ // ── Diagonal sweep ────────────────────────────────────────────────────────
141
+ // A fixed at (20,20). B sweeps along the main diagonal: bx=by=AX+d, d=10→-3.
142
+ // First contact: d=1 — corner pixel (AX+1, AY+1) is shared by both sprites.
143
+ // At d=2: B's nearest pixel is (AX+2, AY+2) — corner of A is (AX+1, AY+1). Gap.
144
+ describe('diagonal sweep: B approaches A from bottom-right', () => {
145
+ const AX = 20, AY = 20;
146
+ it('masksOverlap matches brute-force at every diagonal step', () => {
147
+ for (let d = 10; d >= -3; d--) {
148
+ const bx = AX + d, by = AY + d;
149
+ const expected = bruteForce(MASK, AX, AY, MASK, bx, by);
150
+ const actual = masksOverlap(MASK, AX, AY, MASK, bx, by) > 0;
151
+ if (actual !== expected) {
152
+ throw new Error(`d=${d} (bx=${bx}, by=${by}): masksOverlap=${actual} but brute-force says ${expected}`);
153
+ }
154
+ }
155
+ });
156
+ it('no collision at d=2 — corner pixels are 1 diagonal step apart', () => {
157
+ expect(masksOverlap(MASK, AX, AY, MASK, AX + 2, AY + 2)).toBe(0);
158
+ expect(bruteForce(MASK, AX, AY, MASK, AX + 2, AY + 2)).toBe(false);
159
+ });
160
+ it('collision fires at d=1 — single corner pixel (AX+1, AY+1) coincides', () => {
161
+ expect(masksOverlap(MASK, AX, AY, MASK, AX + 1, AY + 1)).toBeGreaterThan(0);
162
+ expect(bruteForce(MASK, AX, AY, MASK, AX + 1, AY + 1)).toBe(true);
163
+ });
164
+ });
165
+ // ── Overlap count ─────────────────────────────────────────────────────────
166
+ // masksOverlap returns a count, not just a boolean. Verify it grows as the
167
+ // sprites move deeper into each other.
168
+ describe('overlap pixel count increases with penetration depth', () => {
169
+ const AX = 20, AY = 20;
170
+ it('1 overlapping pixel at first horizontal contact (bx = AX+1)', () => {
171
+ // Only column AX+1 is shared (both rows): 2 pixels
172
+ expect(masksOverlap(MASK, AX, AY, MASK, AX + 1, AY)).toBe(2);
173
+ });
174
+ it('4 overlapping pixels when fully aligned (bx = AX)', () => {
175
+ // Complete overlap of the 2×2 regions
176
+ expect(masksOverlap(MASK, AX, AY, MASK, AX, AY)).toBe(4);
177
+ });
178
+ it('single corner pixel at diagonal first contact (d=1)', () => {
179
+ // Only (AX+1, AY+1) is shared
180
+ expect(masksOverlap(MASK, AX, AY, MASK, AX + 1, AY + 1)).toBe(1);
181
+ });
182
+ });
183
+ // ── Symmetry ──────────────────────────────────────────────────────────────
184
+ // masksOverlap(A, B) must equal masksOverlap(B, A) — no directional bias.
185
+ describe('symmetry: collision count is order-independent', () => {
186
+ it('A→B count equals B→A count for all horizontal positions', () => {
187
+ const AX = 20, AY = 20;
188
+ for (let bx = AX + 5; bx >= AX - 3; bx--) {
189
+ const ab = masksOverlap(MASK, AX, AY, MASK, bx, AY);
190
+ const ba = masksOverlap(MASK, bx, AY, MASK, AX, AY);
191
+ if (ab !== ba) {
192
+ throw new Error(`Asymmetry at bx=${bx}: A→B=${ab}, B→A=${ba}`);
193
+ }
194
+ }
195
+ });
196
+ });
197
+ });
198
+ //# sourceMappingURL=collision.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collision.test.js","sourceRoot":"","sources":["../../src/__tests__/collision.test.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -6,8 +6,11 @@ export * from './audio.js';
6
6
  export * from './input.js';
7
7
  export * from './ui.js';
8
8
  export * from './tilemap.js';
9
+ export * from './tilescroll.js';
9
10
  export * from './sprite.js';
10
11
  export * from './collision.js';
12
+ export * from './particles.js';
13
+ export * from './rng.js';
11
14
  export * from './animation.js';
12
15
  export * from './camera.js';
13
16
  export * from './scene.js';
@@ -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;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,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,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA"}
package/dist/index.js CHANGED
@@ -6,8 +6,11 @@ export * from './audio.js';
6
6
  export * from './input.js';
7
7
  export * from './ui.js';
8
8
  export * from './tilemap.js';
9
+ export * from './tilescroll.js';
9
10
  export * from './sprite.js';
10
11
  export * from './collision.js';
12
+ export * from './particles.js';
13
+ export * from './rng.js';
11
14
  export * from './animation.js';
12
15
  export * from './camera.js';
13
16
  export * from './scene.js';
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;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,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,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA"}