zx-kit 0.20.0 → 0.22.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
@@ -27,7 +27,7 @@ The goal is simple: **it should look and sound like a Spectrum, but run like a m
27
27
  - **Canvas renderer** — pixel-perfect scaled rendering, sprite flipping, text drawing, CRT scanline overlay, animated border flashing
28
28
  - **Tile map engine** — scrollable maps, O(1) id-index, smart seasonal background swapping, solid-tile collision queries
29
29
  - **Free-roaming sprites** — position, velocity, gravity, `flipX` caching, transparent or opaque background
30
- - **AABB collision resolution** — sprite vs. sprite overlap, sprite vs. tile map wall resolution with directional hit flags
30
+ - **Three-tier collision** — AABB overlap tests, generic rect-vs-tile wall resolution (any sprite size), and pixel-precise mask overlap with O(pixels) sorted-merge intersection — no allocations per frame
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
@@ -539,7 +539,7 @@ requestAnimationFrame(loop)
539
539
  | [`ui.ts`](#uits--ui-widgets) | Boxes, frames, panel titles, progress bars + instrumentation widgets (dotted grids, segmented bars, fluid tanks, dials, text compass) |
540
540
  | [`input.ts`](#inputts--keyboard-input) | Movement with key-repeat, action flags, state reset |
541
541
  | [`sprite.ts`](#spritets--free-roaming-sprites) | Sprites: position, velocity, gravity, flip, render |
542
- | [`collision.ts`](#collisionts--aabb-collision) | AABB overlap tests, tile-map wall resolution |
542
+ | [`collision.ts`](#collisionts--collision-detection) | AABB overlap + rect-based tile resolution, pixel-precise mask overlap and tile checks |
543
543
  | [`animation.ts`](#animationts--frame-timer--tween) | Frame-timer for sprite strips, position tween between two points |
544
544
  | [`camera.ts`](#camerats--scrolling-camera) | Viewport that follows a target with lerp + deadzone, world-bounds clamping |
545
545
  | [`scene.ts`](#scenets--scene-manager) | Stack-based scene manager with onEnter/onExit/onPause/onResume hooks |
@@ -1412,9 +1412,17 @@ renderSprite(ctx, player)
1412
1412
 
1413
1413
  ---
1414
1414
 
1415
- ## `collision.ts` — AABB Collision
1415
+ ## `collision.ts` — Collision Detection
1416
1416
 
1417
- Axis-aligned bounding box overlap tests and sprite-vs-tile-map wall resolution.
1417
+ Three tiers of collision detection, from broad-phase to pixel-precise:
1418
+
1419
+ | Tier | Functions | Use case |
1420
+ |------|-----------|----------|
1421
+ | **AABB** | `rectsOverlap`, `spritesOverlap`, `spriteRect`, `bitmapRect` | Fast broad-phase hit tests |
1422
+ | **Rect-vs-tile** | `isSolidAt`, `resolveRectX`, `resolveRectY`, `resolveX`, `resolveY` | Wall resolution for any sprite size |
1423
+ | **Pixel-precise** | `bitmapPixelMask`, `masksOverlap`, `pixelSolidCount` | Exact opaque-pixel overlap tests |
1424
+
1425
+ Pick the tier that matches your accuracy need. AABB is O(1) and correct for most cases. Pixel-precise costs O(opaque pixels) but eliminates false positives for non-rectangular sprites.
1418
1426
 
1419
1427
  ### `Rect` interface
1420
1428
 
@@ -1422,19 +1430,36 @@ Axis-aligned bounding box overlap tests and sprite-vs-tile-map wall resolution.
1422
1430
  interface Rect { x: number; y: number; w: number; h: number }
1423
1431
  ```
1424
1432
 
1425
- ### `spriteRect(sprite): Rect`
1433
+ An axis-aligned bounding rectangle in game pixels.
1434
+
1435
+ ---
1436
+
1437
+ ### AABB overlap tests
1438
+
1439
+ #### `spriteRect(sprite): Rect`
1426
1440
 
1427
1441
  Returns the `CELL × CELL` bounding box of a sprite at its current position.
1428
1442
 
1429
- ### `rectsOverlap(a, b): boolean`
1443
+ #### `bitmapRect(bitmap, x, y): Rect`
1444
+
1445
+ Returns the bounding box for any `Bitmap` at `(x, y)`. Correct for bitmaps of any size — 16×24, 32×32, 96×128 — not just `CELL × CELL`.
1446
+
1447
+ ```ts
1448
+ const heroRect = bitmapRect(HERO_BMP, hero.x, hero.y)
1449
+ const enemyRect = bitmapRect(ENEMY_BMP, enemy.x, enemy.y)
1450
+ if (rectsOverlap(heroRect, enemyRect)) damage(hero)
1451
+ ```
1452
+
1453
+ #### `rectsOverlap(a, b): boolean`
1430
1454
 
1431
- Returns `true` when two rectangles share at least one pixel. Touching edges return `false`.
1455
+ Returns `true` when two rectangles share at least one pixel. Touching edges (zero-area overlap) return `false`.
1432
1456
 
1433
1457
  ```ts
1434
1458
  rectsOverlap(spriteRect(bullet), spriteRect(enemy)) // hit test
1459
+ rectsOverlap(bitmapRect(HERO_BMP, hx, hy), bitmapRect(SWORD_BMP, sx, sy))
1435
1460
  ```
1436
1461
 
1437
- ### `spritesOverlap(a, b): boolean`
1462
+ #### `spritesOverlap(a, b): boolean`
1438
1463
 
1439
1464
  Shorthand: `rectsOverlap(spriteRect(a), spriteRect(b))`.
1440
1465
 
@@ -1443,13 +1468,46 @@ if (spritesOverlap(player, coin)) collectCoin()
1443
1468
  if (enemies.some(e => spritesOverlap(player, e))) loseLife()
1444
1469
  ```
1445
1470
 
1446
- ### `isSolidAt(map, px, py): boolean`
1471
+ ---
1472
+
1473
+ ### Rect-vs-tile resolution
1474
+
1475
+ #### `isSolidAt(map, px, py): boolean`
1476
+
1477
+ Tests whether the game-pixel `(px, py)` falls inside a solid tile. Out-of-bounds pixels return `true` (implicit solid boundary). Converts to tile coords via `Math.floor(px / CELL)`.
1447
1478
 
1448
- Tests whether the game-pixel `(px, py)` falls inside a solid tile. Out-of-bounds pixels return `true` (implicit solid boundary).
1479
+ ```ts
1480
+ if (isSolidAt(map, player.x, player.y + CELL)) { player.vy = 0 } // on floor
1481
+ ```
1482
+
1483
+ #### `resolveRectX(rect, map, newX): { x, hitLeft, hitRight }`
1449
1484
 
1450
- ### `resolveX(sprite, map, newX): { x, hitLeft, hitRight }`
1485
+ Generic horizontal resolver for any axis-aligned rectangle. Checks every tile row the rectangle spans — so a 16×24 hero correctly detects walls in the middle rows, not just the top and bottom corners.
1451
1486
 
1452
- Resolves a proposed horizontal move against solid tiles. Returns the clamped position and directional hit flags. No collision returns `newX` unchanged.
1487
+ Returns the clamped x and collision flags. On collision, the rectangle is placed flush against the wall.
1488
+
1489
+ ```ts
1490
+ const rect = bitmapRect(HERO_BMP, hero.x, hero.y)
1491
+ const r = resolveRectX(rect, map, hero.x + dx)
1492
+ hero.x = r.x
1493
+ if (r.hitLeft || r.hitRight) hero.vx = 0
1494
+ ```
1495
+
1496
+ #### `resolveRectY(rect, map, newY): { y, hitTop, hitBottom }`
1497
+
1498
+ Generic vertical resolver for any axis-aligned rectangle. Checks every tile column the rectangle spans — so a wide wagon detects the floor across its full footprint.
1499
+
1500
+ ```ts
1501
+ const rect = bitmapRect(HERO_BMP, hero.x, hero.y)
1502
+ const r = resolveRectY(rect, map, hero.y + dy)
1503
+ hero.y = r.y
1504
+ if (r.hitBottom) { hero.vy = 0; onGround = true }
1505
+ if (r.hitTop) { hero.vy = 0 }
1506
+ ```
1507
+
1508
+ #### `resolveX(sprite, map, newX): { x, hitLeft, hitRight }`
1509
+
1510
+ Resolves a proposed horizontal move for an 8×8 sprite. Thin wrapper over `resolveRectX` — preserved for the common CELL-sized sprite case.
1453
1511
 
1454
1512
  ```ts
1455
1513
  const { x, hitLeft, hitRight } = resolveX(player, map, player.x)
@@ -1457,9 +1515,9 @@ player.x = x
1457
1515
  if (hitLeft || hitRight) player.vx = 0
1458
1516
  ```
1459
1517
 
1460
- ### `resolveY(sprite, map, newY): { y, hitTop, hitBottom }`
1518
+ #### `resolveY(sprite, map, newY): { y, hitTop, hitBottom }`
1461
1519
 
1462
- Resolves a proposed vertical move against solid tiles.
1520
+ Resolves a proposed vertical move for an 8×8 sprite. Thin wrapper over `resolveRectY`.
1463
1521
 
1464
1522
  - `hitBottom` — landed on a floor (use for jump ground detection)
1465
1523
  - `hitTop` — bumped a ceiling
@@ -1473,6 +1531,114 @@ if (hitTop) { player.vy = 0 }
1473
1531
 
1474
1532
  ---
1475
1533
 
1534
+ ### Pixel-precise collision
1535
+
1536
+ AABB and rect-vs-tile use the full bounding box. This is almost always correct — but it fails for non-rectangular sprites in edge cases: a circular character standing on a ledge, a diamond-shaped projectile grazing a corner, a tall hero with narrow feet that shouldn't trigger floor detection when hanging over a gap.
1537
+
1538
+ Pixel-precise collision solves this by working with the actual opaque pixels of a bitmap, not its enclosing rectangle.
1539
+
1540
+ ```
1541
+ AABB (16px wide): ████████████████ → fires when any part of the box overlaps
1542
+ a tile, even if the sprite itself clears it
1543
+ pixelSolidCount: ···██····██···· → only the real foot pixels are checked —
1544
+ Dizzy hanging over a platform edge doesn't
1545
+ feel magically glued to empty air
1546
+ ```
1547
+
1548
+ #### `PixelMask` interface
1549
+
1550
+ ```ts
1551
+ interface PixelMask {
1552
+ readonly width: number
1553
+ readonly height: number
1554
+ readonly rows: readonly (readonly number[])[] // per-row sorted opaque column indices
1555
+ readonly totalPixels: number
1556
+ }
1557
+ ```
1558
+
1559
+ Pre-computed per-row opaque pixel data for a `Bitmap`. Build once with `bitmapPixelMask`; reuse every frame. Each `rows[r]` is a sorted array of column indices where that row has a set bit. Empty rows have zero-length arrays — never `undefined`.
1560
+
1561
+ ```
1562
+ // Example: 16×16 circular sprite
1563
+ mask.rows[0] → [6, 7, 8, 9] // narrow top
1564
+ mask.rows[7] → [0, 1, 2, ..., 15] // full-width middle
1565
+ mask.rows[11] → [3, 4, 10, 11] // only feet
1566
+ mask.rows[14] → [] // below feet, empty
1567
+ ```
1568
+
1569
+ The immutability guarantee matters: the mask is derived from immutable `Bitmap` data. Pre-compute once and store alongside the bitmap definition.
1570
+
1571
+ #### `bitmapPixelMask(bitmap): PixelMask`
1572
+
1573
+ Extracts a pixel mask from a `Bitmap`. Reads each row's bit data (bit 7 = leftmost pixel) and collects column indices of set (opaque) pixels into sorted arrays. Counts `totalPixels` for overlap severity calculations.
1574
+
1575
+ ```ts
1576
+ // Pre-compute at module load time — not inside the game loop
1577
+ const HERO_MASK = bitmapPixelMask(HERO_BMP)
1578
+ const ENEMY_MASK = bitmapPixelMask(ENEMY_BMP)
1579
+ const BULLET_MASK = bitmapPixelMask(BULLET_BMP)
1580
+ ```
1581
+
1582
+ **Bitmap width must be a multiple of 8.** Bitmaps with width `w` require `w * height / 8` bytes of data — standard `createBitmap` enforces this.
1583
+
1584
+ #### `masksOverlap(a, ax, ay, b, bx, by): number`
1585
+
1586
+ Counts opaque pixels of mask `a` at `(ax, ay)` that overlap with opaque pixels of mask `b` at `(bx, by)`.
1587
+
1588
+ Returns **0** when there is no pixel-level overlap. Any value **> 0** is a pixel-perfect collision. The count itself carries semantic meaning: use it for overlap severity — damage scaling, knock-back strength, or as a threshold to ignore grazing touches.
1589
+
1590
+ Uses sorted-merge intersection per row: O(opaque pixels), no allocations per call. Safe to call every frame for multiple pairs.
1591
+
1592
+ ```ts
1593
+ // Simple hit test
1594
+ if (masksOverlap(BULLET_MASK, bullet.x, bullet.y, ENEMY_MASK, enemy.x, enemy.y) > 0) {
1595
+ destroyEnemy()
1596
+ }
1597
+
1598
+ // Severity — require a real overlap, not just a 1-pixel graze
1599
+ const overlap = masksOverlap(SWORD_MASK, sx, sy, HERO_MASK, hx, hy)
1600
+ if (overlap >= 3) {
1601
+ takeDamage(Math.round(overlap / SWORD_MASK.totalPixels * 10))
1602
+ }
1603
+ ```
1604
+
1605
+ Masks of different sizes work without any special handling — the overlap window is clipped to the intersection region automatically.
1606
+
1607
+ #### `pixelSolidCount(mask, mx, my, map): number`
1608
+
1609
+ Counts opaque pixels of a mask at `(mx, my)` that sit on solid tiles in a `TileMap`. Pixel-precise replacement for AABB-based ground / wall checks.
1610
+
1611
+ Solves the "character standing on a platform edge" problem: a round sprite with narrow feet can hang over the edge — only the real foot pixels are checked against the tile map, not the full bounding box.
1612
+
1613
+ ```ts
1614
+ const HERO_MASK = bitmapPixelMask(HERO_BMP)
1615
+
1616
+ // Check if standing — test 1 px below current foot position
1617
+ const standing = pixelSolidCount(HERO_MASK, hero.x, hero.y + 1, map) > 0
1618
+
1619
+ // Check wall to the right — test 1 px past right edge
1620
+ const wallRight = pixelSolidCount(HERO_MASK, hero.x + 1, hero.y, map) > 0
1621
+
1622
+ // How many foot pixels are actually on solid ground? Use as grip factor
1623
+ const groundContact = pixelSolidCount(HERO_MASK, hero.x, hero.y + 1, map)
1624
+ ```
1625
+
1626
+ When `groundContact` is 0, a circle-shaped hero hanging over a tile edge won't trigger `hitBottom` in `resolveRectY` — the pixel-check and AABB-check intentionally disagree, and you pick which one to trust for each gameplay mechanic.
1627
+
1628
+ ---
1629
+
1630
+ ### Choosing the right tier
1631
+
1632
+ | Situation | Recommended tier |
1633
+ |-----------|-----------------|
1634
+ | Player touches any part of a coin | `spritesOverlap` — AABB is exact when both sprites are `CELL × CELL` |
1635
+ | Large hero (16×24) walks into a wall | `resolveRectX` / `resolveRectY` — checks all tile rows the sprite spans |
1636
+ | Round sprite on a platform edge | `pixelSolidCount` — only real foot pixels count |
1637
+ | Bullet vs. irregular boss sprite | `masksOverlap` — pixel-precise, returns overlap count for damage |
1638
+ | Off-road detection for a truck with a bumpy silhouette | `pixelSolidCount` / custom mask loop — checks each opaque pixel against road boundary |
1639
+
1640
+ ---
1641
+
1476
1642
  ## `animation.ts` — Frame Timer & Tween
1477
1643
 
1478
1644
  Two small primitives for time-based animation:
@@ -2057,8 +2223,10 @@ zx-kit/
2057
2223
  │ ├── tilemap.ts # createTileMap, Tile, Viewport, TileMap
2058
2224
  │ ├── sprite.ts # createSprite, moveSprite, applyGravity,
2059
2225
  │ │ # renderSprite, Sprite
2060
- │ └── collision.ts # spriteRect, rectsOverlap, spritesOverlap,
2061
- │ # isSolidAt, resolveX, resolveY, Rect
2226
+ │ └── collision.ts # spriteRect, bitmapRect, rectsOverlap, spritesOverlap,
2227
+ │ # isSolidAt, resolveRectX, resolveRectY, resolveX, resolveY,
2228
+ │ # bitmapPixelMask, masksOverlap, pixelSolidCount,
2229
+ │ # Rect, PixelMask
2062
2230
  └── dist/ # compiled output (npm run build)
2063
2231
  ├── index.js
2064
2232
  ├── index.d.ts
@@ -85,6 +85,88 @@ export declare function resolveRectY(rect: Rect, map: TileMap, newY: number): {
85
85
  hitTop: boolean;
86
86
  hitBottom: boolean;
87
87
  };
88
+ /**
89
+ * Pre-computed per-row opaque pixel data for a {@link Bitmap}.
90
+ * Build once with {@link bitmapPixelMask}, reuse every frame.
91
+ *
92
+ * Each row is a sorted array of column indices where the bitmap has a set bit.
93
+ * Empty rows have zero-length arrays — never `undefined`.
94
+ *
95
+ * @example
96
+ * ```
97
+ * // 16×16 circular sprite:
98
+ * mask.rows[0] → [6, 7, 8, 9] // narrow top
99
+ * mask.rows[7] → [0, 1, 2, ..., 15] // full-width middle
100
+ * mask.rows[11] → [3, 4, 10, 11] // only feet
101
+ * mask.rows[14] → [] // below feet, empty
102
+ * ```
103
+ */
104
+ export interface PixelMask {
105
+ readonly width: number;
106
+ readonly height: number;
107
+ readonly rows: readonly (readonly number[])[];
108
+ readonly totalPixels: number;
109
+ }
110
+ /**
111
+ * Extract a pixel mask from a {@link Bitmap}.
112
+ * Reads each row's bit data (bit 7 = leftmost pixel) and collects column
113
+ * indices of set (opaque) pixels into sorted arrays.
114
+ *
115
+ * Pre-compute once per sprite definition — the result is immutable and
116
+ * derived from immutable bitmap data.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * const HERO_MASK = bitmapPixelMask(HERO_BMP)
121
+ * // Now use with masksOverlap() or pixelSolidCount()
122
+ * ```
123
+ */
124
+ export declare function bitmapPixelMask(bitmap: Bitmap): PixelMask;
125
+ /**
126
+ * Count opaque pixels of mask `a` at `(ax, ay)` that overlap with
127
+ * opaque pixels of mask `b` at `(bx, by)`.
128
+ *
129
+ * Returns 0 when no overlap. Any value > 0 means pixel-perfect collision.
130
+ * The count itself is useful for overlap severity — e.g. damage scaling.
131
+ *
132
+ * Uses sorted-merge intersection per row — O(pixels) total, no allocations.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const BULLET = bitmapPixelMask(BULLET_BMP)
137
+ * const ENEMY = bitmapPixelMask(ENEMY_BMP)
138
+ *
139
+ * if (masksOverlap(BULLET, bx, by, ENEMY, ex, ey) > 0) {
140
+ * destroyEnemy()
141
+ * }
142
+ * ```
143
+ */
144
+ export declare function masksOverlap(a: PixelMask, ax: number, ay: number, b: PixelMask, bx: number, by: number): number;
145
+ /**
146
+ * Count opaque pixels of a mask at `(mx, my)` that sit on solid tiles
147
+ * in a {@link TileMap}. Pixel-precise replacement for AABB-based checks.
148
+ *
149
+ * Solves the "character standing on a platform edge" problem: a round
150
+ * sprite with narrow feet can hang over the edge — only real foot pixels
151
+ * are checked, not the full bounding box.
152
+ *
153
+ * ```
154
+ * AABB (16px wide): ████████████████ → full-width overlap check
155
+ * pixelSolidCount: ···██····██···· → only feet matter
156
+ * ```
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * const HERO_MASK = bitmapPixelMask(HERO_BMP)
161
+ *
162
+ * // Check if standing: test 1px below current position
163
+ * const standing = pixelSolidCount(HERO_MASK, hero.x, hero.y + 1, map) > 0
164
+ *
165
+ * // Check wall to the right
166
+ * const wallRight = pixelSolidCount(HERO_MASK, hero.x + 1, hero.y, map) > 0
167
+ * ```
168
+ */
169
+ export declare function pixelSolidCount(mask: PixelMask, mx: number, my: number, map: TileMap): number;
88
170
  /**
89
171
  * Resolves a proposed horizontal movement for an 8×8 sprite against solid tiles.
90
172
  * Thin wrapper over {@link resolveRectX} preserved for backward compatibility.
@@ -1 +1 @@
1
- {"version":3,"file":"collision.d.ts","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C;;GAEG;AACH,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,GAAG,OAAO,CAKtD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAErE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAEvE;AAwBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAuBpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAuBpD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAEpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAEpD"}
1
+ {"version":3,"file":"collision.d.ts","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"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"}
package/dist/collision.js CHANGED
@@ -144,6 +144,123 @@ export function resolveRectY(rect, map, newY) {
144
144
  }
145
145
  return { y, hitTop, hitBottom };
146
146
  }
147
+ /**
148
+ * Extract a pixel mask from a {@link Bitmap}.
149
+ * Reads each row's bit data (bit 7 = leftmost pixel) and collects column
150
+ * indices of set (opaque) pixels into sorted arrays.
151
+ *
152
+ * Pre-compute once per sprite definition — the result is immutable and
153
+ * derived from immutable bitmap data.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * const HERO_MASK = bitmapPixelMask(HERO_BMP)
158
+ * // Now use with masksOverlap() or pixelSolidCount()
159
+ * ```
160
+ */
161
+ export function bitmapPixelMask(bitmap) {
162
+ const bytesPerRow = bitmap.width / 8;
163
+ const rows = [];
164
+ let total = 0;
165
+ for (let row = 0; row < bitmap.height; row++) {
166
+ const cols = [];
167
+ for (let col = 0; col < bitmap.width; col++) {
168
+ const byteIdx = row * bytesPerRow + Math.floor(col / 8);
169
+ const bitIdx = 7 - (col % 8);
170
+ if (bitmap.data[byteIdx] & (1 << bitIdx))
171
+ cols.push(col);
172
+ }
173
+ rows.push(cols);
174
+ total += cols.length;
175
+ }
176
+ return { width: bitmap.width, height: bitmap.height, rows, totalPixels: total };
177
+ }
178
+ /**
179
+ * Count opaque pixels of mask `a` at `(ax, ay)` that overlap with
180
+ * opaque pixels of mask `b` at `(bx, by)`.
181
+ *
182
+ * Returns 0 when no overlap. Any value > 0 means pixel-perfect collision.
183
+ * The count itself is useful for overlap severity — e.g. damage scaling.
184
+ *
185
+ * Uses sorted-merge intersection per row — O(pixels) total, no allocations.
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * const BULLET = bitmapPixelMask(BULLET_BMP)
190
+ * const ENEMY = bitmapPixelMask(ENEMY_BMP)
191
+ *
192
+ * if (masksOverlap(BULLET, bx, by, ENEMY, ex, ey) > 0) {
193
+ * destroyEnemy()
194
+ * }
195
+ * ```
196
+ */
197
+ export function masksOverlap(a, ax, ay, b, bx, by) {
198
+ const top = Math.max(ay, by);
199
+ const bot = Math.min(ay + a.height, by + b.height);
200
+ if (top >= bot)
201
+ return 0;
202
+ if (ax >= bx + b.width || ax + a.width <= bx)
203
+ return 0;
204
+ let count = 0;
205
+ for (let y = top; y < bot; y++) {
206
+ const rowA = a.rows[y - ay];
207
+ const rowB = b.rows[y - by];
208
+ if (rowA.length === 0 || rowB.length === 0)
209
+ continue;
210
+ let i = 0, j = 0;
211
+ while (i < rowA.length && j < rowB.length) {
212
+ const ca = rowA[i] + ax;
213
+ const cb = rowB[j] + bx;
214
+ if (ca === cb) {
215
+ count++;
216
+ i++;
217
+ j++;
218
+ }
219
+ else if (ca < cb)
220
+ i++;
221
+ else
222
+ j++;
223
+ }
224
+ }
225
+ return count;
226
+ }
227
+ /**
228
+ * Count opaque pixels of a mask at `(mx, my)` that sit on solid tiles
229
+ * in a {@link TileMap}. Pixel-precise replacement for AABB-based checks.
230
+ *
231
+ * Solves the "character standing on a platform edge" problem: a round
232
+ * sprite with narrow feet can hang over the edge — only real foot pixels
233
+ * are checked, not the full bounding box.
234
+ *
235
+ * ```
236
+ * AABB (16px wide): ████████████████ → full-width overlap check
237
+ * pixelSolidCount: ···██····██···· → only feet matter
238
+ * ```
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * const HERO_MASK = bitmapPixelMask(HERO_BMP)
243
+ *
244
+ * // Check if standing: test 1px below current position
245
+ * const standing = pixelSolidCount(HERO_MASK, hero.x, hero.y + 1, map) > 0
246
+ *
247
+ * // Check wall to the right
248
+ * const wallRight = pixelSolidCount(HERO_MASK, hero.x + 1, hero.y, map) > 0
249
+ * ```
250
+ */
251
+ export function pixelSolidCount(mask, mx, my, map) {
252
+ let count = 0;
253
+ for (let row = 0; row < mask.height; row++) {
254
+ const worldY = my + row;
255
+ const tileY = Math.floor(worldY / CELL);
256
+ for (const col of mask.rows[row]) {
257
+ if (map.isSolid(Math.floor((mx + col) / CELL), tileY))
258
+ count++;
259
+ }
260
+ }
261
+ return count;
262
+ }
263
+ // ─── AABB sprite wrappers (backward-compatible) ─────────────────────────────
147
264
  /**
148
265
  * Resolves a proposed horizontal movement for an 8×8 sprite against solid tiles.
149
266
  * Thin wrapper over {@link resolveRectX} preserved for backward compatibility.
@@ -1 +1 @@
1
- {"version":3,"file":"collision.js","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collision.js","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAenC;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAA;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,CAAO,EAAE,CAAO;IAC3C,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACf,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACf,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACf,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AACxB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,CAAS,EAAE,CAAS;IACjD,OAAO,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;AACnD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,CAAS,EAAE,CAAS;IAC7D,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAA;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,GAAY,EAAE,EAAU,EAAE,EAAU;IAC5D,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;AAClE,CAAC;AAED,gFAAgF;AAEhF,SAAS,uBAAuB,CAAC,GAAY,EAAE,KAAa,EAAE,EAAU,EAAE,EAAU;IAClF,mFAAmF;IACnF,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;IACrC,KAAK,IAAI,EAAE,GAAG,OAAO,EAAE,EAAE,IAAI,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC;QAC3C,IAAI,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,GAAG,IAAI,CAAC;YAAE,OAAO,IAAI,CAAA;IACnD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,yBAAyB,CAAC,GAAY,EAAE,KAAa,EAAE,EAAU,EAAE,EAAU;IACpF,sFAAsF;IACtF,MAAM,QAAQ,GAAI,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;IACvC,KAAK,IAAI,EAAE,GAAG,QAAQ,EAAE,EAAE,IAAI,SAAS,EAAE,EAAE,EAAE,EAAE,CAAC;QAC9C,IAAI,SAAS,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;IACnD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAU,EACV,GAAY,EACZ,IAAY;IAEZ,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;IACjB,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG,IAAI,CAAA;IACZ,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,IAAI,QAAQ,GAAG,KAAK,CAAA;IAEpB,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;QAClB,IAAI,uBAAuB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;YAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;YAC/B,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAA;YACzC,OAAO,GAAG,IAAI,CAAA;QAChB,CAAC;IACH,CAAC;SAAM,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,uBAAuB,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;YACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC,EAAE,SAAS,CAAC,CAAA;YACzD,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,CAAA;YAC/C,QAAQ,GAAG,IAAI,CAAA;QACjB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;AACjC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAU,EACV,GAAY,EACZ,IAAY;IAEZ,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;IACjB,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG,IAAI,CAAA;IACZ,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,IAAI,SAAS,GAAG,KAAK,CAAA;IAErB,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;QAClB,IAAI,yBAAyB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;YACjD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;YAC/B,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAA;YACzC,MAAM,GAAG,IAAI,CAAA;QACf,CAAC;IACH,CAAC;SAAM,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;QACpC,IAAI,yBAAyB,CAAC,GAAG,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;YACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC,EAAE,UAAU,CAAC,CAAA;YAC1D,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,CAAA;YAC/C,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;AACjC,CAAC;AA2BD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,GAAG,CAAC,CAAA;IACpC,MAAM,IAAI,GAAe,EAAE,CAAA;IAC3B,IAAI,KAAK,GAAG,CAAC,CAAA;IAEb,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAa,EAAE,CAAA;QACzB,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC;YAC5C,MAAM,OAAO,GAAG,GAAG,GAAG,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;YACvD,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;YAC5B,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAE,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC3D,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACf,KAAK,IAAI,IAAI,CAAC,MAAM,CAAA;IACtB,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;AACjF,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,YAAY,CAC1B,CAAY,EAAE,EAAU,EAAE,EAAU,EACpC,CAAY,EAAE,EAAU,EAAE,EAAU;IAEpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,CAAA;IAClD,IAAI,GAAG,IAAI,GAAG;QAAE,OAAO,CAAC,CAAA;IACxB,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE;QAAE,OAAO,CAAC,CAAA;IAEtD,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,CAAE,CAAA;QAC5B,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,CAAE,CAAA;QAC5B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAQ;QAEpD,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;QAChB,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAC1C,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAE,GAAG,EAAE,CAAA;YACxB,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAE,GAAG,EAAE,CAAA;YACxB,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBAAC,KAAK,EAAE,CAAC;gBAAC,CAAC,EAAE,CAAC;gBAAC,CAAC,EAAE,CAAA;YAAC,CAAC;iBAC/B,IAAI,EAAE,GAAG,EAAE;gBAAE,CAAC,EAAE,CAAA;;gBAChB,CAAC,EAAE,CAAA;QACV,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAe,EACf,EAAU,EAAE,EAAU,EACtB,GAAY;IAEZ,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,EAAE,GAAG,GAAG,CAAA;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;QACvC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAE,EAAE,CAAC;YAClC,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,KAAK,CAAC;gBAAE,KAAK,EAAE,CAAA;QAChE,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,+EAA+E;AAE/E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,QAAQ,CACtB,MAAc,EACd,GAAY,EACZ,IAAY;IAEZ,OAAO,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;AACpD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,QAAQ,CACtB,MAAc,EACd,GAAY,EACZ,IAAY;IAEZ,OAAO,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;AACpD,CAAC"}
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * i18n.ts — locale selection helper.
3
+ *
4
+ * A tiny, dependency-free utility for picking a string pack at runtime
5
+ * based on a language code. Designed so any zx-kit game can ship with
6
+ * multiple translations and switch them via a single config flag.
7
+ *
8
+ * USAGE
9
+ * ─────
10
+ * Each locale exports the same shape — usually as named string constants
11
+ * and template functions (e.g. `STR_DEPTH = (m) => \`D:${m}M\``).
12
+ * Import each locale module as a namespace and hand them to `pickLocale`:
13
+ *
14
+ * import * as en from './strings.ts' // default English
15
+ * import * as sk from './strings.sk.ts' // Slovak
16
+ * import * as ru from './strings.ru.ts' // Russian
17
+ * import { LANGUAGE_CODE } from './config.ts'
18
+ *
19
+ * export const L = pickLocale(en, { sk, ru }, LANGUAGE_CODE)
20
+ *
21
+ * Consumers then read `L.STR_DEPTH(120)` etc. — same name, swapped values.
22
+ *
23
+ * SELECTION RULES
24
+ * ───────────────
25
+ * - `code` null / undefined / empty → returns `defaultLocale`
26
+ * - `code` matches a key in `locales` → returns that locale
27
+ * (case-insensitive: 'SK' === 'sk')
28
+ * - `code` doesn't match anything → returns `defaultLocale`
29
+ *
30
+ * The default key (typically 'en') doesn't need to live in the `locales`
31
+ * map — passing 'en' simply falls through to `defaultLocale`. This keeps
32
+ * the English source-of-truth file at the conventional `strings.ts` path
33
+ * without forcing a `strings.en.ts` rename.
34
+ */
35
+ /**
36
+ * Pick a locale object from a map by code, with fallback to a default.
37
+ *
38
+ * Generic over `T` so every entry in `locales` is type-checked against the
39
+ * shape of `defaultLocale` — adding a new translation that misses a key
40
+ * (or has a wrong signature) becomes a compile error.
41
+ */
42
+ export declare function pickLocale<T>(defaultLocale: T, locales: Record<string, T>, code: string | null | undefined): T;
43
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAC1B,aAAa,EAAE,CAAC,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,CAAC,CAIH"}
package/dist/i18n.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * i18n.ts — locale selection helper.
3
+ *
4
+ * A tiny, dependency-free utility for picking a string pack at runtime
5
+ * based on a language code. Designed so any zx-kit game can ship with
6
+ * multiple translations and switch them via a single config flag.
7
+ *
8
+ * USAGE
9
+ * ─────
10
+ * Each locale exports the same shape — usually as named string constants
11
+ * and template functions (e.g. `STR_DEPTH = (m) => \`D:${m}M\``).
12
+ * Import each locale module as a namespace and hand them to `pickLocale`:
13
+ *
14
+ * import * as en from './strings.ts' // default English
15
+ * import * as sk from './strings.sk.ts' // Slovak
16
+ * import * as ru from './strings.ru.ts' // Russian
17
+ * import { LANGUAGE_CODE } from './config.ts'
18
+ *
19
+ * export const L = pickLocale(en, { sk, ru }, LANGUAGE_CODE)
20
+ *
21
+ * Consumers then read `L.STR_DEPTH(120)` etc. — same name, swapped values.
22
+ *
23
+ * SELECTION RULES
24
+ * ───────────────
25
+ * - `code` null / undefined / empty → returns `defaultLocale`
26
+ * - `code` matches a key in `locales` → returns that locale
27
+ * (case-insensitive: 'SK' === 'sk')
28
+ * - `code` doesn't match anything → returns `defaultLocale`
29
+ *
30
+ * The default key (typically 'en') doesn't need to live in the `locales`
31
+ * map — passing 'en' simply falls through to `defaultLocale`. This keeps
32
+ * the English source-of-truth file at the conventional `strings.ts` path
33
+ * without forcing a `strings.en.ts` rename.
34
+ */
35
+ /**
36
+ * Pick a locale object from a map by code, with fallback to a default.
37
+ *
38
+ * Generic over `T` so every entry in `locales` is type-checked against the
39
+ * shape of `defaultLocale` — adding a new translation that misses a key
40
+ * (or has a wrong signature) becomes a compile error.
41
+ */
42
+ export function pickLocale(defaultLocale, locales, code) {
43
+ if (!code)
44
+ return defaultLocale;
45
+ const normalised = code.toLowerCase();
46
+ return locales[normalised] ?? defaultLocale;
47
+ }
48
+ //# sourceMappingURL=i18n.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,aAAgB,EAChB,OAA0B,EAC1B,IAA+B;IAE/B,IAAI,CAAC,IAAI;QAAE,OAAO,aAAa,CAAA;IAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;IACrC,OAAO,OAAO,CAAC,UAAU,CAAC,IAAI,aAAa,CAAA;AAC7C,CAAC"}
package/dist/index.d.ts CHANGED
@@ -12,4 +12,5 @@ export * from './animation.js';
12
12
  export * from './camera.js';
13
13
  export * from './scene.js';
14
14
  export * from './save.js';
15
+ export * from './i18n.js';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,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,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"}
package/dist/index.js CHANGED
@@ -12,4 +12,5 @@ export * from './animation.js';
12
12
  export * from './camera.js';
13
13
  export * from './scene.js';
14
14
  export * from './save.js';
15
+ export * from './i18n.js';
15
16
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,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,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zx-kit",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zrebec/zx-kit.git"