zx-kit 0.23.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
@@ -546,6 +546,9 @@ requestAnimationFrame(loop)
546
546
  | [`scene.ts`](#scenets--scene-manager) | Stack-based scene manager with onEnter/onExit/onPause/onResume hooks |
547
547
  | [`save.ts`](#savets--typed-save--load) | Typed save/load via callbacks, versioning + migrations, slot enumeration, throttling, Result types |
548
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 |
549
552
  | [`palette.ts`](#palettets--color-constants) | 15 Spectrum colors, `SpectrumColor` type, `CELL`, `SCALE` |
550
553
  | [`font.ts`](#fontts--rom-bitmap-font) | 96-character ROM font, raw bitmap access |
551
554
  | [`i18n.ts`](#i18nts--runtime-locale-selection) | Type-safe runtime locale selection for translated string packs |
@@ -2277,6 +2280,150 @@ The default language does not need its own `strings.en.ts` file. Keep the source
2277
2280
 
2278
2281
  ---
2279
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
+
2280
2427
  ## Architecture
2281
2428
 
2282
2429
  ### Module structure
@@ -2304,9 +2451,13 @@ zx-kit/
2304
2451
  │ ├── ui.ts # drawBox, drawFrame, drawPanelTitle,
2305
2452
  │ │ # instrumentation widgets, progress bars
2306
2453
  │ ├── tilemap.ts # createTileMap, Tile, Viewport, TileMap
2454
+ │ ├── tilescroll.ts # drawTileMapAt, tileMapWorldSize (sub-pixel scroll)
2307
2455
  │ ├── sprite.ts # createSprite, moveSprite, applyGravity,
2308
2456
  │ │ # renderSprite, Sprite
2309
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)
2310
2461
  │ ├── animation.ts # frame timers, tweens, blinkers
2311
2462
  │ ├── camera.ts # scrolling viewport, lerp, deadzone, bounds
2312
2463
  │ ├── scene.ts # stack-based scene manager
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"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @module particles
3
+ *
4
+ * **Allocation-free particle pool** for ZX-style pixel effects: carrot-shot
5
+ * sparks, landing dust, an enemy curling into a puff, glowing motes around a
6
+ * crystal. All particles live in a fixed-capacity pool created once at startup;
7
+ * emitting and ticking never allocates, so it is safe to run every frame.
8
+ *
9
+ * Particles are plain coloured squares drawn in the {@link "palette" | Spectrum
10
+ * palette}. For deterministic effects (replays, seeded worlds) pass an `rng`
11
+ * function to {@link emitParticles}; otherwise it uses `Math.random`.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const sparks = createParticleSystem(128)
16
+ *
17
+ * // On carrot impact:
18
+ * emitParticles(sparks, {
19
+ * x: hit.x, y: hit.y, count: 12,
20
+ * color: [C.B_YELLOW, C.B_WHITE],
21
+ * speed: [0.02, 0.06], angle: -Math.PI / 2, spread: Math.PI,
22
+ * life: [200, 400], size: 1,
23
+ * })
24
+ *
25
+ * // Each frame:
26
+ * tickParticles(sparks, dt, 0.0004) // gentle gravity
27
+ * renderParticles(ctx, sparks, cam.x, cam.y) // world → screen offset
28
+ * ```
29
+ */
30
+ import type { SpectrumColor } from './palette.js';
31
+ /** A single pooled particle. `active === false` slots are free for reuse. */
32
+ export interface Particle {
33
+ /** World X in pixels. */
34
+ x: number;
35
+ /** World Y in pixels. */
36
+ y: number;
37
+ /** Horizontal velocity in pixels per millisecond. */
38
+ vx: number;
39
+ /** Vertical velocity in pixels per millisecond. */
40
+ vy: number;
41
+ /** Remaining lifetime in milliseconds. */
42
+ life: number;
43
+ /** Lifetime the particle was spawned with (for fade math by the caller). */
44
+ maxLife: number;
45
+ /** Particle colour (Spectrum palette value). */
46
+ color: SpectrumColor;
47
+ /** Square side length in pixels (≥ 1). */
48
+ size: number;
49
+ /** Whether this slot is alive and should be ticked/rendered. */
50
+ active: boolean;
51
+ }
52
+ /** A fixed-capacity particle pool. Create once with {@link createParticleSystem}. */
53
+ export interface ParticleSystem {
54
+ /** Backing pool. Length always equals {@link ParticleSystem.capacity}. */
55
+ readonly particles: Particle[];
56
+ /** Maximum number of simultaneously-alive particles. */
57
+ readonly capacity: number;
58
+ /** Number of currently-alive particles. */
59
+ activeCount: number;
60
+ }
61
+ /** A scalar value or an inclusive `[min, max]` range to sample uniformly. */
62
+ export type Ranged = number | readonly [number, number];
63
+ /** Options for a single {@link emitParticles} burst. */
64
+ export interface EmitOptions {
65
+ /** Spawn X in world pixels. */
66
+ x: number;
67
+ /** Spawn Y in world pixels. */
68
+ y: number;
69
+ /** How many particles to emit (clamped to free pool slots). */
70
+ count: number;
71
+ /** Colour, or a palette to pick from per particle. */
72
+ color: SpectrumColor | readonly SpectrumColor[];
73
+ /** Speed in px/ms — scalar or `[min, max]`. Default `0.03`. */
74
+ speed?: Ranged;
75
+ /** Base emission direction in radians (0 = right, −π/2 = up). Default `0`. */
76
+ angle?: number;
77
+ /** Angular jitter in radians, centred on `angle`. Default `0` (no spread). */
78
+ spread?: number;
79
+ /** Lifetime in ms — scalar or `[min, max]`. Default `300`. */
80
+ life?: Ranged;
81
+ /** Square size in pixels. Default `1`. */
82
+ size?: number;
83
+ /** Random source for determinism. Default `Math.random`. */
84
+ rng?: () => number;
85
+ }
86
+ /**
87
+ * Creates a particle pool of fixed `capacity`. All slots start inactive.
88
+ * Throws when `capacity` is not a positive integer.
89
+ *
90
+ * @example
91
+ * const ps = createParticleSystem(256)
92
+ */
93
+ export declare function createParticleSystem(capacity: number): ParticleSystem;
94
+ /**
95
+ * Emits up to `opts.count` particles from the pool. Returns the number actually
96
+ * emitted — fewer than requested when the pool has no free slots left.
97
+ * Throws when `count` is negative or not an integer.
98
+ *
99
+ * @example
100
+ * const n = emitParticles(ps, { x, y, count: 10, color: C.B_WHITE })
101
+ */
102
+ export declare function emitParticles(ps: ParticleSystem, opts: EmitOptions): number;
103
+ /**
104
+ * Advances every active particle by `dtMs`, applying optional `gravity`
105
+ * (px/ms²) to vertical velocity. Particles whose lifetime expires are
106
+ * deactivated and returned to the pool.
107
+ *
108
+ * @param gravity - Downward acceleration in px/ms². Default `0` (no gravity).
109
+ *
110
+ * @example
111
+ * tickParticles(ps, dt) // floaty
112
+ * tickParticles(ps, dt, 0.0006) // falling sparks
113
+ */
114
+ export declare function tickParticles(ps: ParticleSystem, dtMs: number, gravity?: number): void;
115
+ /**
116
+ * Draws every active particle as a filled square. Subtract camera world
117
+ * position via `offsetX` / `offsetY` to convert world coords to screen coords.
118
+ * Coordinates are rounded to whole pixels for a crisp ZX look.
119
+ *
120
+ * @example
121
+ * renderParticles(ctx, ps) // world == screen
122
+ * renderParticles(ctx, ps, cam.x, cam.y) // scrolled world
123
+ */
124
+ export declare function renderParticles(ctx: CanvasRenderingContext2D, ps: ParticleSystem, offsetX?: number, offsetY?: number): void;
125
+ /**
126
+ * Deactivates all particles immediately (e.g. on room change).
127
+ *
128
+ * @example
129
+ * clearParticles(ps)
130
+ */
131
+ export declare function clearParticles(ps: ParticleSystem): void;
132
+ //# sourceMappingURL=particles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles.d.ts","sourceRoot":"","sources":["../src/particles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAEjD,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,yBAAyB;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,yBAAyB;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,qDAAqD;IACrD,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAA;IACV,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAA;IACf,gDAAgD;IAChD,KAAK,EAAE,aAAa,CAAA;IACpB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,gEAAgE;IAChE,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAA;IAC9B,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,6EAA6E;AAC7E,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEvD,wDAAwD;AACxD,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,CAAC,EAAE,MAAM,CAAA;IACT,+BAA+B;IAC/B,CAAC,EAAE,MAAM,CAAA;IACT,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,KAAK,EAAE,aAAa,GAAG,SAAS,aAAa,EAAE,CAAA;IAC/C,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4DAA4D;IAC5D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAMD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAOrE;AAQD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CA6C3E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,SAAI,GAAG,IAAI,CAcjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,wBAAwB,EAC7B,EAAE,EAAE,cAAc,EAClB,OAAO,SAAI,EACX,OAAO,SAAI,GACV,IAAI,CAQN;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI,CAIvD"}
@@ -0,0 +1,138 @@
1
+ function makeParticle() {
2
+ return { x: 0, y: 0, vx: 0, vy: 0, life: 0, maxLife: 0, color: '#000000', size: 1, active: false };
3
+ }
4
+ /**
5
+ * Creates a particle pool of fixed `capacity`. All slots start inactive.
6
+ * Throws when `capacity` is not a positive integer.
7
+ *
8
+ * @example
9
+ * const ps = createParticleSystem(256)
10
+ */
11
+ export function createParticleSystem(capacity) {
12
+ if (!Number.isInteger(capacity) || capacity <= 0) {
13
+ throw new Error(`createParticleSystem: capacity must be a positive integer, got ${capacity}`);
14
+ }
15
+ const particles = new Array(capacity);
16
+ for (let i = 0; i < capacity; i++)
17
+ particles[i] = makeParticle();
18
+ return { particles, capacity, activeCount: 0 };
19
+ }
20
+ function sampleRanged(v, fallback, rand) {
21
+ if (v === undefined)
22
+ return fallback;
23
+ if (typeof v === 'number')
24
+ return v;
25
+ return v[0] + rand() * (v[1] - v[0]);
26
+ }
27
+ /**
28
+ * Emits up to `opts.count` particles from the pool. Returns the number actually
29
+ * emitted — fewer than requested when the pool has no free slots left.
30
+ * Throws when `count` is negative or not an integer.
31
+ *
32
+ * @example
33
+ * const n = emitParticles(ps, { x, y, count: 10, color: C.B_WHITE })
34
+ */
35
+ export function emitParticles(ps, opts) {
36
+ if (!Number.isInteger(opts.count) || opts.count < 0) {
37
+ throw new Error(`emitParticles: count must be a non-negative integer, got ${opts.count}`);
38
+ }
39
+ const rand = opts.rng ?? Math.random;
40
+ const colors = Array.isArray(opts.color)
41
+ ? opts.color
42
+ : [opts.color];
43
+ const angle = opts.angle ?? 0;
44
+ const spread = opts.spread ?? 0;
45
+ const size = opts.size ?? 1;
46
+ const pool = ps.particles;
47
+ let emitted = 0;
48
+ let cursor = 0;
49
+ for (let n = 0; n < opts.count; n++) {
50
+ // Find next free slot from where we left off.
51
+ let slot = -1;
52
+ while (cursor < ps.capacity) {
53
+ if (!pool[cursor].active) {
54
+ slot = cursor;
55
+ break;
56
+ }
57
+ cursor++;
58
+ }
59
+ if (slot === -1)
60
+ break;
61
+ const speed = sampleRanged(opts.speed, 0.03, rand);
62
+ const life = sampleRanged(opts.life, 300, rand);
63
+ const dir = angle + (rand() - 0.5) * spread;
64
+ const p = pool[slot];
65
+ p.x = opts.x;
66
+ p.y = opts.y;
67
+ p.vx = Math.cos(dir) * speed;
68
+ p.vy = Math.sin(dir) * speed;
69
+ p.life = life;
70
+ p.maxLife = life;
71
+ p.color = colors[Math.floor(rand() * colors.length)];
72
+ p.size = size;
73
+ p.active = true;
74
+ emitted++;
75
+ cursor++;
76
+ }
77
+ ps.activeCount += emitted;
78
+ return emitted;
79
+ }
80
+ /**
81
+ * Advances every active particle by `dtMs`, applying optional `gravity`
82
+ * (px/ms²) to vertical velocity. Particles whose lifetime expires are
83
+ * deactivated and returned to the pool.
84
+ *
85
+ * @param gravity - Downward acceleration in px/ms². Default `0` (no gravity).
86
+ *
87
+ * @example
88
+ * tickParticles(ps, dt) // floaty
89
+ * tickParticles(ps, dt, 0.0006) // falling sparks
90
+ */
91
+ export function tickParticles(ps, dtMs, gravity = 0) {
92
+ const pool = ps.particles;
93
+ for (let i = 0; i < pool.length; i++) {
94
+ const p = pool[i];
95
+ if (!p.active)
96
+ continue;
97
+ p.vy += gravity * dtMs;
98
+ p.x += p.vx * dtMs;
99
+ p.y += p.vy * dtMs;
100
+ p.life -= dtMs;
101
+ if (p.life <= 0) {
102
+ p.active = false;
103
+ ps.activeCount--;
104
+ }
105
+ }
106
+ }
107
+ /**
108
+ * Draws every active particle as a filled square. Subtract camera world
109
+ * position via `offsetX` / `offsetY` to convert world coords to screen coords.
110
+ * Coordinates are rounded to whole pixels for a crisp ZX look.
111
+ *
112
+ * @example
113
+ * renderParticles(ctx, ps) // world == screen
114
+ * renderParticles(ctx, ps, cam.x, cam.y) // scrolled world
115
+ */
116
+ export function renderParticles(ctx, ps, offsetX = 0, offsetY = 0) {
117
+ const pool = ps.particles;
118
+ for (let i = 0; i < pool.length; i++) {
119
+ const p = pool[i];
120
+ if (!p.active)
121
+ continue;
122
+ ctx.fillStyle = p.color;
123
+ ctx.fillRect(Math.round(p.x - offsetX), Math.round(p.y - offsetY), p.size, p.size);
124
+ }
125
+ }
126
+ /**
127
+ * Deactivates all particles immediately (e.g. on room change).
128
+ *
129
+ * @example
130
+ * clearParticles(ps)
131
+ */
132
+ export function clearParticles(ps) {
133
+ const pool = ps.particles;
134
+ for (let i = 0; i < pool.length; i++)
135
+ pool[i].active = false;
136
+ ps.activeCount = 0;
137
+ }
138
+ //# sourceMappingURL=particles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"particles.js","sourceRoot":"","sources":["../src/particles.ts"],"names":[],"mappings":"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"}
package/dist/rng.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @module rng
3
+ *
4
+ * **Seeded deterministic pseudo-random number generator.** Same seed → same
5
+ * sequence, on every machine and every run — exactly what procedural world
6
+ * generation needs ("the same seed must create the same world").
7
+ *
8
+ * Built on **mulberry32**: a fast, allocation-free 32-bit generator with good
9
+ * statistical quality for games. It is *not* cryptographically secure — never
10
+ * use it for security tokens.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const rng = createRng('cave-level-7') // string or number seed
15
+ * rng.next() // float in [0, 1)
16
+ * rng.int(8) // int in [0, 8)
17
+ * rng.range(2, 6) // int in [2, 6)
18
+ * rng.float(0.5, 1.5) // float in [0.5, 1.5)
19
+ * rng.chance(0.25) // true ~25% of the time
20
+ * rng.pick(['a','b','c']) // one element
21
+ * rng.shuffle([1,2,3,4]) // in-place Fisher–Yates
22
+ *
23
+ * const branch = rng.fork() // independent deterministic sub-stream
24
+ * ```
25
+ */
26
+ /**
27
+ * A seeded random source. All methods advance the same internal stream, so the
28
+ * order of calls is part of the determinism contract — call them in the same
29
+ * order to reproduce a world.
30
+ */
31
+ export interface Rng {
32
+ /** Next float in `[0, 1)`. */
33
+ next(): number;
34
+ /** Next integer in `[0, maxExclusive)`. Throws when `maxExclusive` is not a positive integer. */
35
+ int(maxExclusive: number): number;
36
+ /** Next integer in `[minInclusive, maxExclusive)`. Throws when `maxExclusive <= minInclusive` or bounds are non-integer. */
37
+ range(minInclusive: number, maxExclusive: number): number;
38
+ /** Next float in `[min, max)`. Throws when `max < min`. */
39
+ float(min: number, max: number): number;
40
+ /** `true` with probability `p` (0–1). Throws when `p` is outside `[0, 1]`. */
41
+ chance(p: number): boolean;
42
+ /** Returns a random element of `items`. Throws when `items` is empty. */
43
+ pick<T>(items: readonly T[]): T;
44
+ /** Shuffles `items` in place (Fisher–Yates) and returns the same array. */
45
+ shuffle<T>(items: T[]): T[];
46
+ /** Derives an independent generator seeded from this stream (advances this stream by one step). */
47
+ fork(): Rng;
48
+ }
49
+ /**
50
+ * Hashes a string to an unsigned 32-bit integer using FNV-1a.
51
+ * Deterministic and dependency-free — used internally to turn string seeds
52
+ * into numeric state, but exported because it is handy for keying sub-streams.
53
+ *
54
+ * @example
55
+ * hashSeed('room:3') // → stable uint32
56
+ */
57
+ export declare function hashSeed(seed: string): number;
58
+ /**
59
+ * Creates a seeded {@link Rng}. The seed may be a string (hashed via
60
+ * {@link hashSeed}) or a finite number (coerced to uint32).
61
+ *
62
+ * @param seed - String or finite number. Throws on a non-finite number.
63
+ *
64
+ * @example
65
+ * const a = createRng('alpha')
66
+ * const b = createRng('alpha')
67
+ * a.next() === b.next() // → true (deterministic)
68
+ */
69
+ export declare function createRng(seed: number | string): Rng;
70
+ //# sourceMappingURL=rng.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;GAIG;AACH,MAAM,WAAW,GAAG;IAClB,8BAA8B;IAC9B,IAAI,IAAI,MAAM,CAAA;IACd,iGAAiG;IACjG,GAAG,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;IACjC,4HAA4H;IAC5H,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;IACzD,2DAA2D;IAC3D,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;IACvC,8EAA8E;IAC9E,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC1B,yEAAyE;IACzE,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,CAAA;IAC/B,2EAA2E;IAC3E,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;IAC3B,mGAAmG;IACnG,IAAI,IAAI,GAAG,CAAA;CACZ;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO7C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,GAAG,CA2EpD"}
package/dist/rng.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @module rng
3
+ *
4
+ * **Seeded deterministic pseudo-random number generator.** Same seed → same
5
+ * sequence, on every machine and every run — exactly what procedural world
6
+ * generation needs ("the same seed must create the same world").
7
+ *
8
+ * Built on **mulberry32**: a fast, allocation-free 32-bit generator with good
9
+ * statistical quality for games. It is *not* cryptographically secure — never
10
+ * use it for security tokens.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const rng = createRng('cave-level-7') // string or number seed
15
+ * rng.next() // float in [0, 1)
16
+ * rng.int(8) // int in [0, 8)
17
+ * rng.range(2, 6) // int in [2, 6)
18
+ * rng.float(0.5, 1.5) // float in [0.5, 1.5)
19
+ * rng.chance(0.25) // true ~25% of the time
20
+ * rng.pick(['a','b','c']) // one element
21
+ * rng.shuffle([1,2,3,4]) // in-place Fisher–Yates
22
+ *
23
+ * const branch = rng.fork() // independent deterministic sub-stream
24
+ * ```
25
+ */
26
+ /**
27
+ * Hashes a string to an unsigned 32-bit integer using FNV-1a.
28
+ * Deterministic and dependency-free — used internally to turn string seeds
29
+ * into numeric state, but exported because it is handy for keying sub-streams.
30
+ *
31
+ * @example
32
+ * hashSeed('room:3') // → stable uint32
33
+ */
34
+ export function hashSeed(seed) {
35
+ let h = 2166136261 >>> 0;
36
+ for (let i = 0; i < seed.length; i++) {
37
+ h ^= seed.charCodeAt(i);
38
+ h = Math.imul(h, 16777619);
39
+ }
40
+ return h >>> 0;
41
+ }
42
+ /**
43
+ * Creates a seeded {@link Rng}. The seed may be a string (hashed via
44
+ * {@link hashSeed}) or a finite number (coerced to uint32).
45
+ *
46
+ * @param seed - String or finite number. Throws on a non-finite number.
47
+ *
48
+ * @example
49
+ * const a = createRng('alpha')
50
+ * const b = createRng('alpha')
51
+ * a.next() === b.next() // → true (deterministic)
52
+ */
53
+ export function createRng(seed) {
54
+ let state;
55
+ if (typeof seed === 'string') {
56
+ state = hashSeed(seed);
57
+ }
58
+ else {
59
+ if (!Number.isFinite(seed)) {
60
+ throw new Error(`createRng: numeric seed must be finite, got ${seed}`);
61
+ }
62
+ state = seed >>> 0;
63
+ }
64
+ const next = () => {
65
+ state = (state + 0x6d2b79f5) >>> 0;
66
+ let t = Math.imul(state ^ (state >>> 15), 1 | state);
67
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
68
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
69
+ };
70
+ const rng = {
71
+ next,
72
+ int(maxExclusive) {
73
+ if (!Number.isInteger(maxExclusive) || maxExclusive <= 0) {
74
+ throw new Error(`Rng.int: maxExclusive must be a positive integer, got ${maxExclusive}`);
75
+ }
76
+ return Math.floor(next() * maxExclusive);
77
+ },
78
+ range(minInclusive, maxExclusive) {
79
+ if (!Number.isInteger(minInclusive) || !Number.isInteger(maxExclusive)) {
80
+ throw new Error(`Rng.range: bounds must be integers, got [${minInclusive}, ${maxExclusive})`);
81
+ }
82
+ if (maxExclusive <= minInclusive) {
83
+ throw new Error(`Rng.range: maxExclusive must be greater than minInclusive, got [${minInclusive}, ${maxExclusive})`);
84
+ }
85
+ return minInclusive + Math.floor(next() * (maxExclusive - minInclusive));
86
+ },
87
+ float(min, max) {
88
+ if (max < min) {
89
+ throw new Error(`Rng.float: max must be >= min, got [${min}, ${max})`);
90
+ }
91
+ return min + next() * (max - min);
92
+ },
93
+ chance(p) {
94
+ if (p < 0 || p > 1) {
95
+ throw new Error(`Rng.chance: probability must be in [0, 1], got ${p}`);
96
+ }
97
+ return next() < p;
98
+ },
99
+ pick(items) {
100
+ if (items.length === 0) {
101
+ throw new Error('Rng.pick: cannot pick from an empty array');
102
+ }
103
+ return items[Math.floor(next() * items.length)];
104
+ },
105
+ shuffle(items) {
106
+ for (let i = items.length - 1; i > 0; i--) {
107
+ const j = Math.floor(next() * (i + 1));
108
+ const tmp = items[i];
109
+ items[i] = items[j];
110
+ items[j] = tmp;
111
+ }
112
+ return items;
113
+ },
114
+ fork() {
115
+ return createRng((next() * 4294967296) >>> 0);
116
+ },
117
+ };
118
+ return rng;
119
+ }
120
+ //# sourceMappingURL=rng.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng.js","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { TileMap } from './tilemap.js';
2
+ /**
3
+ * Returns the map's full size in pixels — handy for `worldW`/`worldH` when
4
+ * constructing a {@link "camera" | Camera}.
5
+ *
6
+ * @example
7
+ * const { width, height } = tileMapWorldSize(map)
8
+ */
9
+ export declare function tileMapWorldSize(map: TileMap): {
10
+ width: number;
11
+ height: number;
12
+ };
13
+ /**
14
+ * Renders `map` with the viewport's top-left at world pixel `(camX, camY)`.
15
+ * The camera position is rounded to whole pixels for crisp output, then the
16
+ * visible tile range (plus one overscan row/column) is drawn at
17
+ * `tileX * CELL - camX`, giving smooth scrolling at any pixel offset.
18
+ *
19
+ * @param viewW - Visible width in pixels (default `256`). Must be positive.
20
+ * @param viewH - Visible height in pixels (default `192`). Must be positive.
21
+ *
22
+ * @example
23
+ * drawTileMapAt(ctx, map, cam.x, cam.y) // standard 256×192
24
+ * drawTileMapAt(ctx, map, cam.x, cam.y, 256, 176) // shorter play area
25
+ */
26
+ export declare function drawTileMapAt(ctx: CanvasRenderingContext2D, map: TileMap, camX: number, camY: number, viewW?: number, viewH?: number): void;
27
+ //# sourceMappingURL=tilescroll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tilescroll.d.ts","sourceRoot":"","sources":["../src/tilescroll.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAEhF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,wBAAwB,EAC7B,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,SAAM,EACX,KAAK,SAAM,GACV,IAAI,CAuBN"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @module tilescroll
3
+ *
4
+ * **Pixel-smooth tile-map scrolling.** The built-in {@link "tilemap" |
5
+ * TileMap.render} takes a viewport in *whole tiles*, so a camera can only move
6
+ * in 8-pixel steps — fine for grid games, visibly steppy for a platformer where
7
+ * the player moves sub-pixel-smooth while jumping.
8
+ *
9
+ * {@link drawTileMapAt} renders the map at an arbitrary pixel camera position by
10
+ * drawing one extra row/column and offsetting every tile by `camX % CELL` /
11
+ * `camY % CELL`. Off-screen and empty cells are skipped; the leading/trailing
12
+ * partial tiles are drawn and naturally clipped by the canvas bounds.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const cam = createCamera({ viewW: 256, viewH: 192, ...tileMapWorldSize(map) })
17
+ * // game loop:
18
+ * setCameraTarget(cam, player.x, player.y)
19
+ * tickCamera(cam, dt)
20
+ * drawTileMapAt(ctx, map, cam.x, cam.y) // smooth background
21
+ * // …draw entities at (e.x - cam.x, e.y - cam.y)…
22
+ * ```
23
+ */
24
+ import { CELL } from './palette.js';
25
+ import { drawSprite } from './renderer.js';
26
+ /**
27
+ * Returns the map's full size in pixels — handy for `worldW`/`worldH` when
28
+ * constructing a {@link "camera" | Camera}.
29
+ *
30
+ * @example
31
+ * const { width, height } = tileMapWorldSize(map)
32
+ */
33
+ export function tileMapWorldSize(map) {
34
+ return { width: map.cols * CELL, height: map.rows * CELL };
35
+ }
36
+ /**
37
+ * Renders `map` with the viewport's top-left at world pixel `(camX, camY)`.
38
+ * The camera position is rounded to whole pixels for crisp output, then the
39
+ * visible tile range (plus one overscan row/column) is drawn at
40
+ * `tileX * CELL - camX`, giving smooth scrolling at any pixel offset.
41
+ *
42
+ * @param viewW - Visible width in pixels (default `256`). Must be positive.
43
+ * @param viewH - Visible height in pixels (default `192`). Must be positive.
44
+ *
45
+ * @example
46
+ * drawTileMapAt(ctx, map, cam.x, cam.y) // standard 256×192
47
+ * drawTileMapAt(ctx, map, cam.x, cam.y, 256, 176) // shorter play area
48
+ */
49
+ export function drawTileMapAt(ctx, map, camX, camY, viewW = 256, viewH = 192) {
50
+ if (viewW <= 0 || viewH <= 0) {
51
+ throw new Error(`drawTileMapAt: viewW and viewH must be positive, got ${viewW}×${viewH}`);
52
+ }
53
+ const ox = Math.round(camX);
54
+ const oy = Math.round(camY);
55
+ const startCol = Math.floor(ox / CELL);
56
+ const startRow = Math.floor(oy / CELL);
57
+ const cols = Math.ceil(viewW / CELL) + 1;
58
+ const rows = Math.ceil(viewH / CELL) + 1;
59
+ for (let ry = 0; ry < rows; ry++) {
60
+ const ty = startRow + ry;
61
+ if (ty < 0 || ty >= map.rows)
62
+ continue;
63
+ for (let rx = 0; rx < cols; rx++) {
64
+ const tx = startCol + rx;
65
+ if (tx < 0 || tx >= map.cols)
66
+ continue;
67
+ const tile = map.getTile(tx, ty);
68
+ if (tile === null)
69
+ continue;
70
+ drawSprite(ctx, tile.sprite, tx * CELL - ox, ty * CELL - oy, tile.ink, tile.paper);
71
+ }
72
+ }
73
+ }
74
+ //# sourceMappingURL=tilescroll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tilescroll.js","sourceRoot":"","sources":["../src/tilescroll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAG1C;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,GAAG,IAAI,EAAE,CAAA;AAC5D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAC3B,GAA6B,EAC7B,GAAY,EACZ,IAAY,EACZ,IAAY,EACZ,KAAK,GAAG,GAAG,EACX,KAAK,GAAG,GAAG;IAEX,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,wDAAwD,KAAK,IAAI,KAAK,EAAE,CAAC,CAAA;IAC3F,CAAC;IAED,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;IACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;IAExC,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC;QACjC,MAAM,EAAE,GAAG,QAAQ,GAAG,EAAE,CAAA;QACxB,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,IAAI;YAAE,SAAQ;QACtC,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC;YACjC,MAAM,EAAE,GAAG,QAAQ,GAAG,EAAE,CAAA;YACxB,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,IAAI;gBAAE,SAAQ;YACtC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;YAChC,IAAI,IAAI,KAAK,IAAI;gBAAE,SAAQ;YAC3B,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,GAAG,EAAE,EAAE,EAAE,GAAG,IAAI,GAAG,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;QACpF,CAAC;IACH,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zx-kit",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zrebec/zx-kit.git"