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 +151 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/particles.d.ts +132 -0
- package/dist/particles.d.ts.map +1 -0
- package/dist/particles.js +138 -0
- package/dist/particles.js.map +1 -0
- package/dist/rng.d.ts +70 -0
- package/dist/rng.d.ts.map +1 -0
- package/dist/rng.js +120 -0
- package/dist/rng.js.map +1 -0
- package/dist/tilescroll.d.ts +27 -0
- package/dist/tilescroll.d.ts.map +1 -0
- package/dist/tilescroll.js +74 -0
- package/dist/tilescroll.js.map +1 -0
- package/package.json +1 -1
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
package/dist/rng.js.map
ADDED
|
@@ -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"}
|