zx-kit 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Reusable ZX Spectrum primitives for browser games built with Vite + TypeScript + Canvas + Web Audio API.
4
4
 
5
+ Current npm package: `zx-kit@0.5.1`.
6
+
5
7
  Extracted from [Minefield](https://github.com/zrebec/minefield) — a ZX Spectrum-style minesweeper game. All modules enforce strict Spectrum authenticity: 8×8 pixel grid, 15-color palette, 1-bit square-wave audio, bitmap font.
6
8
 
7
9
  ---
@@ -469,6 +471,195 @@ appPhase = 'intro'
469
471
 
470
472
  ---
471
473
 
474
+ ### `tilemap.ts` — Scrollable tile map
475
+
476
+ A scrollable, queryable `TileMap` backed by an O(1) id-index. Tiles use the same 8×8 sprite format as `drawSprite`. Supports seasonal background swapping, viewport-clipped rendering, collision queries, and fast id-based lookups.
477
+ Tile colours are palette-typed: `ink` and `paper` must be `SpectrumColor` values from `C`.
478
+
479
+ #### Types
480
+
481
+ **`Tile`**
482
+
483
+ | Field | Type | Description |
484
+ |-------|------|-------------|
485
+ | `sprite` | `Uint8Array` | 8-byte bitmap — same format as `drawSprite()` |
486
+ | `ink` | `SpectrumColor` | Foreground colour (`C.*` palette value) |
487
+ | `paper` | `SpectrumColor` | Background colour (`C.*` palette value) |
488
+ | `solid` | `boolean` | `true` = blocks movement (walls, rocks, closed doors) |
489
+ | `id` | `string \| number` | Stable identifier for game logic and background swapping |
490
+ | `metadata?` | `Record<string, unknown>` | Optional game-specific payload (points, next level, …) |
491
+
492
+ **`Viewport`**
493
+
494
+ | Field | Type | Description |
495
+ |-------|------|-------------|
496
+ | `x` | `number` | First visible column (tile units) |
497
+ | `y` | `number` | First visible row (tile units) |
498
+ | `cols` | `number` | Number of columns to render |
499
+ | `rows` | `number` | Number of rows to render |
500
+
501
+ #### `createTileMap(cols, rows): TileMap`
502
+
503
+ Creates an empty map of `cols × rows` tiles — all cells start `null`. Returns a plain object implementing the `TileMap` interface (factory pattern, consistent with the rest of zx-kit).
504
+
505
+ #### Method reference
506
+
507
+ | Method | Description |
508
+ |--------|-------------|
509
+ | `setTile(x, y, tile)` | Store a shallow copy of `tile`. Out-of-bounds is a silent no-op. |
510
+ | `getTile(x, y)` | Return the tile at `(x, y)`, or `null`. Never throws. |
511
+ | `clearTile(x, y)` | Remove the tile (e.g. collect gem, break wall). Out-of-bounds is a no-op. |
512
+ | `fill(tile)` | Fill every cell with independent shallow copies of `tile`. |
513
+ | `fillRect(x, y, w, h, tile)` | Fill a rectangle; regions outside the map are silently clipped. |
514
+ | `setBackground(tile)` | Register or swap the background tile (see below). |
515
+ | `render(ctx, viewport?)` | Render the map or viewport via `drawSprite`. Empty cells are skipped. |
516
+ | `isSolid(x, y)` | `true` when the tile is solid, or when the position is out-of-bounds. |
517
+ | `findById(id)` | Return `{ x, y, tile }[]` for all tiles with the given `id` — O(1). |
518
+
519
+ #### Smart background swapping (`setBackground`)
520
+
521
+ `setBackground` has two modes depending on whether a background has been registered before:
522
+
523
+ - **First call** — registers the tile as the current background. The map is not modified; call `fill` or `fillRect` first to actually place the background tiles.
524
+ - **Subsequent calls (smart swap)** — replaces every cell whose `id` still matches the previous background with a fresh copy of the new tile. Cells with any other `id` (player, gems, rocks, modified terrain) are left completely untouched.
525
+
526
+ Comparison is by `id` value, so it works correctly after shallow copies.
527
+
528
+ ```ts
529
+ map.fill(TILE_GRASS)
530
+ map.setBackground(TILE_GRASS) // register — map unchanged
531
+
532
+ map.setTile(5, 3, TILE_PLAYER) // player placed on grass
533
+
534
+ map.setBackground(TILE_SNOW) // TILE_GRASS → TILE_SNOW everywhere
535
+ // TILE_PLAYER at (5, 3) — untouched
536
+
537
+ map.setBackground(TILE_NIGHT) // TILE_SNOW → TILE_NIGHT
538
+ // TILE_PLAYER — still untouched
539
+ ```
540
+
541
+ ---
542
+
543
+ #### Boulder Dash-style level
544
+
545
+ A complete setup showing map construction, collision detection, item collection, and seasonal background swap — the typical usage pattern for a scrollable ZX Spectrum-style game.
546
+
547
+ ```ts
548
+ import { createTileMap, C, CELL } from 'zx-kit'
549
+ import type { Tile, Viewport } from 'zx-kit'
550
+
551
+ // ── Tile definitions ──────────────────────────────────────────────────────────
552
+
553
+ const TILE_DIRT: Tile = {
554
+ sprite: new Uint8Array([0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA]),
555
+ ink: C.YELLOW, paper: C.BLACK,
556
+ solid: false, id: 'dirt',
557
+ }
558
+ const TILE_WALL: Tile = {
559
+ sprite: new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
560
+ ink: C.WHITE, paper: C.BLACK,
561
+ solid: true, id: 'wall',
562
+ }
563
+ const TILE_ROCK: Tile = {
564
+ sprite: new Uint8Array([0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C]),
565
+ ink: C.B_WHITE, paper: C.BLACK,
566
+ solid: true, id: 'rock',
567
+ }
568
+ const TILE_GEM: Tile = {
569
+ sprite: new Uint8Array([0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x7E, 0x3C, 0x18]),
570
+ ink: C.B_CYAN, paper: C.BLACK,
571
+ solid: false, id: 'gem',
572
+ metadata: { points: 10 },
573
+ }
574
+ const TILE_EXIT: Tile = {
575
+ sprite: new Uint8Array([0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3C]),
576
+ ink: C.B_YELLOW, paper: C.BLACK,
577
+ solid: false, id: 'exit',
578
+ metadata: { nextLevel: 2 },
579
+ }
580
+
581
+ // ── Map setup ─────────────────────────────────────────────────────────────────
582
+
583
+ const COLS = 64
584
+ const ROWS = 32
585
+ const map = createTileMap(COLS, ROWS)
586
+
587
+ // Fill with dirt and register it as the seasonal background
588
+ map.fill(TILE_DIRT)
589
+ map.setBackground(TILE_DIRT)
590
+
591
+ // Perimeter walls
592
+ map.fillRect(0, 0, COLS, 1, TILE_WALL) // top
593
+ map.fillRect(0, ROWS - 1, COLS, 1, TILE_WALL) // bottom
594
+ map.fillRect(0, 0, 1, ROWS, TILE_WALL) // left
595
+ map.fillRect(COLS - 1, 0, 1, ROWS, TILE_WALL) // right
596
+
597
+ // Objects
598
+ map.setTile(10, 5, TILE_ROCK)
599
+ map.setTile(20, 14, TILE_GEM)
600
+ map.setTile(35, 10, TILE_GEM)
601
+ map.setTile(60, 15, TILE_EXIT)
602
+
603
+ // ── Seasonal swap ─────────────────────────────────────────────────────────────
604
+
605
+ const TILE_SNOW: Tile = {
606
+ sprite: new Uint8Array([0x00, 0x18, 0x3C, 0xFF, 0x3C, 0x18, 0x00, 0x00]),
607
+ ink: C.B_WHITE, paper: C.BLACK,
608
+ solid: false, id: 'snow',
609
+ }
610
+
611
+ // Winter: only dirt tiles become snow — walls, rocks, gems, exit are untouched
612
+ map.setBackground(TILE_SNOW)
613
+
614
+ // ── Game loop ─────────────────────────────────────────────────────────────────
615
+
616
+ const SCREEN_COLS = 32
617
+ const SCREEN_ROWS = 24
618
+ let playerX = 2
619
+ let playerY = 2
620
+ let score = 0
621
+
622
+ function gameLoop(ctx: CanvasRenderingContext2D) {
623
+ // Clamp camera so it doesn't scroll past map edges
624
+ const camX = Math.max(0, Math.min(playerX - Math.floor(SCREEN_COLS / 2), COLS - SCREEN_COLS))
625
+ const camY = Math.max(0, Math.min(playerY - Math.floor(SCREEN_ROWS / 2), ROWS - SCREEN_ROWS))
626
+
627
+ map.render(ctx, { x: camX, y: camY, cols: SCREEN_COLS, rows: SCREEN_ROWS })
628
+ }
629
+
630
+ // ── Collision & interaction ───────────────────────────────────────────────────
631
+
632
+ function tryMove(dx: number, dy: number) {
633
+ const nx = playerX + dx
634
+ const ny = playerY + dy
635
+
636
+ if (map.isSolid(nx, ny)) return // wall, rock, or map boundary
637
+
638
+ const target = map.getTile(nx, ny)
639
+ if (target?.id === 'gem') {
640
+ score += target.metadata!['points'] as number
641
+ map.clearTile(nx, ny)
642
+ }
643
+
644
+ playerX = nx
645
+ playerY = ny
646
+ }
647
+
648
+ // ── Level completion ──────────────────────────────────────────────────────────
649
+
650
+ const exits = map.findById('exit') // O(1) — no map scan
651
+ if (exits.some(e => e.x === playerX && e.y === playerY)) {
652
+ const next = exits[0].tile.metadata!['nextLevel'] as number
653
+ console.log(`Loading level ${next}`)
654
+ }
655
+
656
+ // Count remaining gems
657
+ const gemsLeft = map.findById('gem').length
658
+ console.log(`${gemsLeft} gems remaining`)
659
+ ```
660
+
661
+ ---
662
+
472
663
  ## File structure
473
664
 
474
665
  ```
@@ -488,9 +679,10 @@ zx-kit/
488
679
  │ │ # increaseVolume, decreaseVolume
489
680
  │ ├── input.ts # initInput, tickMovement, consumeFlag/Debug/Pause/AnyKey,
490
681
  │ │ # isHeld, resetInput, Direction
491
- └── ui.ts # drawBox, drawFrame, drawPanelTitle,
492
- # drawProgressBar, tickUI, renderUI, resetUI,
493
- # BorderOptions, DrawProgressBarOptions
682
+ ├── ui.ts # drawBox, drawFrame, drawPanelTitle,
683
+ # drawProgressBar, tickUI, renderUI, resetUI,
684
+ # BorderOptions, DrawProgressBarOptions
685
+ │ └── tilemap.ts # createTileMap, Tile, Viewport, TileMap
494
686
  └── dist/ # compiled output (generated by npm run build)
495
687
  ├── index.js / .d.ts
496
688
  └── ...
@@ -503,6 +695,7 @@ zx-kit/
503
695
  - **Compiled distribution** — ships compiled JS + `.d.ts` in `dist/`. No bundler configuration needed in the consuming project.
504
696
  - **No runtime dependencies** — only Web platform APIs (`CanvasRenderingContext2D`, `AudioContext`, `KeyboardEvent`).
505
697
  - **Strict TypeScript** — `strict: true`, `noUnusedLocals`, `noUnusedParameters`. No `any`.
698
+ - **Palette-typed game data** — UI colours and tile `ink` / `paper` use `SpectrumColor`, so consumers stay inside the Spectrum palette at compile time.
506
699
  - **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.
507
700
  - **ZX Spectrum authenticity** — palette values, cell size, and font bytes are constants, not configuration. The library is deliberately opinionated.
508
701
 
package/dist/index.d.ts CHANGED
@@ -4,4 +4,5 @@ export * from './renderer.js';
4
4
  export * from './audio.js';
5
5
  export * from './input.js';
6
6
  export * from './ui.js';
7
+ export * from './tilemap.js';
7
8
  //# 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,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA"}
package/dist/index.js CHANGED
@@ -4,4 +4,5 @@ export * from './renderer.js';
4
4
  export * from './audio.js';
5
5
  export * from './input.js';
6
6
  export * from './ui.js';
7
+ export * from './tilemap.js';
7
8
  //# 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,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA"}
@@ -0,0 +1,134 @@
1
+ import { type SpectrumColor } from './palette.js';
2
+ /**
3
+ * A single 8×8 tile placed in a `TileMap`.
4
+ * Sprite format is identical to `drawSprite()` — one byte per row, bit 7 = leftmost pixel.
5
+ */
6
+ export interface Tile {
7
+ /** 8-byte sprite bitmap (one byte per row, bit 7 = leftmost pixel). */
8
+ sprite: Uint8Array;
9
+ /** Foreground colour — a `C.*` palette value. */
10
+ ink: SpectrumColor;
11
+ /** Background colour — a `C.*` palette value. */
12
+ paper: SpectrumColor;
13
+ /** When `true` the tile blocks movement (walls, solid objects, map boundary). */
14
+ solid: boolean;
15
+ /** Stable identifier used for game logic and smart background swapping. */
16
+ id: string | number;
17
+ /** Arbitrary game-specific data attached to this tile instance. */
18
+ metadata?: Record<string, unknown>;
19
+ }
20
+ /** Defines the visible region of a map to render. All values are in tile units, not pixels. */
21
+ export interface Viewport {
22
+ /** First visible column (tile units). */
23
+ x: number;
24
+ /** First visible row (tile units). */
25
+ y: number;
26
+ /** Number of tile columns to render. */
27
+ cols: number;
28
+ /** Number of tile rows to render. */
29
+ rows: number;
30
+ }
31
+ /**
32
+ * A scrollable, queryable tile map with O(1) id-based lookup.
33
+ * Obtain an instance via `createTileMap(cols, rows)`.
34
+ *
35
+ * All mutating methods store **shallow copies** of tile objects — the caller
36
+ * may safely reuse or mutate tile definitions without affecting placed tiles.
37
+ */
38
+ export interface TileMap {
39
+ /** Map width in tile columns. */
40
+ readonly cols: number;
41
+ /** Map height in tile rows. */
42
+ readonly rows: number;
43
+ /**
44
+ * Registers or swaps the background tile.
45
+ *
46
+ * - **First call:** stores `tile` as the current background; the map is left unchanged.
47
+ * - **Subsequent calls (smart swap):** every cell whose `id` still matches the previously
48
+ * registered background is replaced with a shallow copy of the new tile. Cells with
49
+ * any other `id` (player, objects, modified terrain) are not touched.
50
+ *
51
+ * Comparison is by `id` value, so it works correctly after shallow copies.
52
+ *
53
+ * @example
54
+ * map.fill(TILE_GRASS)
55
+ * map.setBackground(TILE_GRASS) // registers background; map unchanged
56
+ * map.setTile(3, 5, TILE_PLAYER)
57
+ * map.setBackground(TILE_SNOW) // grass → snow; TILE_PLAYER untouched
58
+ */
59
+ setBackground(tile: Tile): void;
60
+ /**
61
+ * Stores a shallow copy of `tile` at `(x, y)`.
62
+ * Out-of-bounds coordinates are silently ignored.
63
+ */
64
+ setTile(x: number, y: number, tile: Tile): void;
65
+ /**
66
+ * Returns the tile at `(x, y)`, or `null` if the cell is empty or out of bounds.
67
+ * Never throws.
68
+ */
69
+ getTile(x: number, y: number): Tile | null;
70
+ /**
71
+ * Removes the tile at `(x, y)`, making the cell empty.
72
+ * Out-of-bounds coordinates are silently ignored.
73
+ */
74
+ clearTile(x: number, y: number): void;
75
+ /** Fills every cell in the map with a separate shallow copy of `tile`. */
76
+ fill(tile: Tile): void;
77
+ /**
78
+ * Fills the rectangle `(x, y) – (x+w, y+h)` with shallow copies of `tile`.
79
+ * Areas outside the map boundary are silently clipped.
80
+ */
81
+ fillRect(x: number, y: number, w: number, h: number, tile: Tile): void;
82
+ /**
83
+ * Renders the map (or the given `viewport`) to `ctx` via `drawSprite`.
84
+ * Empty cells are skipped. Tiles partially outside the viewport are not drawn.
85
+ * Canvas pixel position: `canvasX = (tileX - viewport.x) * CELL`.
86
+ *
87
+ * When `viewport` is omitted the entire map is rendered starting at canvas origin.
88
+ */
89
+ render(ctx: CanvasRenderingContext2D, viewport?: Viewport): void;
90
+ /**
91
+ * Returns `true` when the tile at `(x, y)` has `solid === true`.
92
+ * Out-of-bounds coordinates return `true` — the map boundary is implicitly solid.
93
+ */
94
+ isSolid(x: number, y: number): boolean;
95
+ /**
96
+ * Returns every cell whose tile has the given `id`.
97
+ * Uses an internal index — O(1) lookup, does not iterate the full map.
98
+ * Returns an empty array when no matching tile exists.
99
+ */
100
+ findById(id: string | number): {
101
+ x: number;
102
+ y: number;
103
+ tile: Tile;
104
+ }[];
105
+ }
106
+ /**
107
+ * Creates a `TileMap` of `cols × rows` tiles. All cells start empty.
108
+ * Use `fill` / `fillRect` / `setTile` to populate terrain, and `setBackground`
109
+ * to register a background tile for later smart swapping.
110
+ *
111
+ * @param cols - Map width in tiles
112
+ * @param rows - Map height in tiles
113
+ *
114
+ * @example
115
+ * import { createTileMap, C } from 'zx-kit'
116
+ *
117
+ * const map = createTileMap(64, 32)
118
+ * map.fill(TILE_GRASS)
119
+ * map.setBackground(TILE_GRASS)
120
+ * map.fillRect(0, 0, 64, 1, TILE_WALL) // ceiling
121
+ * map.fillRect(0, 31, 64, 1, TILE_WALL) // floor
122
+ * map.setTile(60, 15, TILE_EXIT)
123
+ *
124
+ * // In game loop — camera follows the player
125
+ * map.render(ctx, { x: camX - 16, y: camY - 12, cols: 32, rows: 24 })
126
+ *
127
+ * // Collision
128
+ * if (!map.isSolid(playerX, playerY + 1)) playerY++
129
+ *
130
+ * // Season swap — player, gems, modified cells are untouched
131
+ * map.setBackground(TILE_SNOW)
132
+ */
133
+ export declare function createTileMap(cols: number, rows: number): TileMap;
134
+ //# sourceMappingURL=tilemap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tilemap.d.ts","sourceRoot":"","sources":["../src/tilemap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAGvD;;;GAGG;AACH,MAAM,WAAW,IAAI;IACnB,uEAAuE;IACvE,MAAM,EAAE,UAAU,CAAA;IAClB,iDAAiD;IACjD,GAAG,EAAE,aAAa,CAAA;IAClB,iDAAiD;IACjD,KAAK,EAAE,aAAa,CAAA;IACpB,iFAAiF;IACjF,KAAK,EAAE,OAAO,CAAA;IACd,2EAA2E;IAC3E,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,+FAA+F;AAC/F,MAAM,WAAW,QAAQ;IACvB,yCAAyC;IACzC,CAAC,EAAE,MAAM,CAAA;IACT,sCAAsC;IACtC,CAAC,EAAE,MAAM,CAAA;IACT,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB,iCAAiC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;;;;;;;;;;;;;OAeG;IACH,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IAE/B;;;OAGG;IACH,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IAE/C;;;OAGG;IACH,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAE1C;;;OAGG;IACH,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAErC,0EAA0E;IAC1E,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IAEtB;;;OAGG;IACH,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;IAEtE;;;;;;OAMG;IACH,MAAM,CAAC,GAAG,EAAE,wBAAwB,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IAEhE;;;OAGG;IACH,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAEtC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,EAAE,CAAA;CACtE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAyIjE"}
@@ -0,0 +1,166 @@
1
+ import { CELL } from './palette.js';
2
+ import { drawSprite } from './renderer.js';
3
+ /**
4
+ * Creates a `TileMap` of `cols × rows` tiles. All cells start empty.
5
+ * Use `fill` / `fillRect` / `setTile` to populate terrain, and `setBackground`
6
+ * to register a background tile for later smart swapping.
7
+ *
8
+ * @param cols - Map width in tiles
9
+ * @param rows - Map height in tiles
10
+ *
11
+ * @example
12
+ * import { createTileMap, C } from 'zx-kit'
13
+ *
14
+ * const map = createTileMap(64, 32)
15
+ * map.fill(TILE_GRASS)
16
+ * map.setBackground(TILE_GRASS)
17
+ * map.fillRect(0, 0, 64, 1, TILE_WALL) // ceiling
18
+ * map.fillRect(0, 31, 64, 1, TILE_WALL) // floor
19
+ * map.setTile(60, 15, TILE_EXIT)
20
+ *
21
+ * // In game loop — camera follows the player
22
+ * map.render(ctx, { x: camX - 16, y: camY - 12, cols: 32, rows: 24 })
23
+ *
24
+ * // Collision
25
+ * if (!map.isSolid(playerX, playerY + 1)) playerY++
26
+ *
27
+ * // Season swap — player, gems, modified cells are untouched
28
+ * map.setBackground(TILE_SNOW)
29
+ */
30
+ export function createTileMap(cols, rows) {
31
+ const cells = new Array(cols * rows).fill(null);
32
+ const idIndex = new Map();
33
+ let currentBackground = null;
34
+ function inBounds(x, y) {
35
+ return x >= 0 && y >= 0 && x < cols && y < rows;
36
+ }
37
+ function cellKey(x, y) {
38
+ return `${x},${y}`;
39
+ }
40
+ function indexRemove(x, y) {
41
+ const existing = cells[y * cols + x];
42
+ if (existing === null)
43
+ return;
44
+ const set = idIndex.get(existing.id);
45
+ if (!set)
46
+ return;
47
+ set.delete(cellKey(x, y));
48
+ if (set.size === 0)
49
+ idIndex.delete(existing.id);
50
+ }
51
+ function indexAdd(x, y, tile) {
52
+ let set = idIndex.get(tile.id);
53
+ if (!set) {
54
+ set = new Set();
55
+ idIndex.set(tile.id, set);
56
+ }
57
+ set.add(cellKey(x, y));
58
+ }
59
+ return {
60
+ get cols() { return cols; },
61
+ get rows() { return rows; },
62
+ setBackground(tile) {
63
+ if (currentBackground === null) {
64
+ currentBackground = { ...tile };
65
+ return;
66
+ }
67
+ const oldId = currentBackground.id;
68
+ for (let y = 0; y < rows; y++) {
69
+ for (let x = 0; x < cols; x++) {
70
+ const cell = cells[y * cols + x];
71
+ if (cell !== null && cell.id === oldId) {
72
+ indexRemove(x, y);
73
+ const copy = { ...tile };
74
+ cells[y * cols + x] = copy;
75
+ indexAdd(x, y, copy);
76
+ }
77
+ }
78
+ }
79
+ currentBackground = { ...tile };
80
+ },
81
+ setTile(x, y, tile) {
82
+ if (!inBounds(x, y))
83
+ return;
84
+ indexRemove(x, y);
85
+ const copy = { ...tile };
86
+ cells[y * cols + x] = copy;
87
+ indexAdd(x, y, copy);
88
+ },
89
+ getTile(x, y) {
90
+ if (!inBounds(x, y))
91
+ return null;
92
+ return cells[y * cols + x];
93
+ },
94
+ clearTile(x, y) {
95
+ if (!inBounds(x, y))
96
+ return;
97
+ indexRemove(x, y);
98
+ cells[y * cols + x] = null;
99
+ },
100
+ fill(tile) {
101
+ idIndex.clear();
102
+ for (let y = 0; y < rows; y++) {
103
+ for (let x = 0; x < cols; x++) {
104
+ const copy = { ...tile };
105
+ cells[y * cols + x] = copy;
106
+ indexAdd(x, y, copy);
107
+ }
108
+ }
109
+ },
110
+ fillRect(x, y, w, h, tile) {
111
+ const x0 = Math.max(0, x);
112
+ const y0 = Math.max(0, y);
113
+ const x1 = Math.min(cols, x + w);
114
+ const y1 = Math.min(rows, y + h);
115
+ for (let ty = y0; ty < y1; ty++) {
116
+ for (let tx = x0; tx < x1; tx++) {
117
+ indexRemove(tx, ty);
118
+ const copy = { ...tile };
119
+ cells[ty * cols + tx] = copy;
120
+ indexAdd(tx, ty, copy);
121
+ }
122
+ }
123
+ },
124
+ render(ctx, viewport) {
125
+ const vx = viewport?.x ?? 0;
126
+ const vy = viewport?.y ?? 0;
127
+ const vcols = viewport?.cols ?? cols;
128
+ const vrows = viewport?.rows ?? rows;
129
+ for (let row = 0; row < vrows; row++) {
130
+ const ty = vy + row;
131
+ if (ty < 0 || ty >= rows)
132
+ continue;
133
+ for (let col = 0; col < vcols; col++) {
134
+ const tx = vx + col;
135
+ if (tx < 0 || tx >= cols)
136
+ continue;
137
+ const tile = cells[ty * cols + tx];
138
+ if (tile === null)
139
+ continue;
140
+ drawSprite(ctx, tile.sprite, col * CELL, row * CELL, tile.ink, tile.paper);
141
+ }
142
+ }
143
+ },
144
+ isSolid(x, y) {
145
+ if (!inBounds(x, y))
146
+ return true;
147
+ return cells[y * cols + x]?.solid ?? false;
148
+ },
149
+ findById(id) {
150
+ const set = idIndex.get(id);
151
+ if (!set)
152
+ return [];
153
+ const result = [];
154
+ for (const k of set) {
155
+ const comma = k.indexOf(',');
156
+ const tx = parseInt(k.slice(0, comma), 10);
157
+ const ty = parseInt(k.slice(comma + 1), 10);
158
+ const tile = cells[ty * cols + tx];
159
+ if (tile !== null)
160
+ result.push({ x: tx, y: ty, tile });
161
+ }
162
+ return result;
163
+ },
164
+ };
165
+ }
166
+ //# sourceMappingURL=tilemap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tilemap.js","sourceRoot":"","sources":["../src/tilemap.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zx-kit",
3
- "version": "0.4.0",
4
- "description": "Reusable ZX Spectrum primitives: font, palette, renderer, audio, input, ui",
3
+ "version": "0.5.1",
4
+ "description": "Reusable ZX Spectrum primitives: font, palette, renderer, audio, input, ui, tilemap",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -18,7 +18,8 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "build": "tsc",
21
- "prepublishOnly": "npm run build"
21
+ "prepublishOnly": "npm run build",
22
+ "test": "vitest run"
22
23
  },
23
24
  "keywords": [
24
25
  "zx-spectrum",
@@ -28,6 +29,8 @@
28
29
  ],
29
30
  "license": "MIT",
30
31
  "devDependencies": {
31
- "typescript": "^6.0.3"
32
+ "jsdom": "^29.1.1",
33
+ "typescript": "^6.0.3",
34
+ "vitest": "^4.1.5"
32
35
  }
33
36
  }