zx-kit 0.7.0 → 0.9.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 +297 -21
- package/dist/collision.d.ts +87 -0
- package/dist/collision.d.ts.map +1 -0
- package/dist/collision.js +127 -0
- package/dist/collision.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/renderer.d.ts +20 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +31 -0
- package/dist/renderer.js.map +1 -1
- package/dist/sprite.d.ts +81 -0
- package/dist/sprite.d.ts.map +1 -0
- package/dist/sprite.js +91 -0
- package/dist/sprite.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
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.6.7`.
|
|
6
|
-
|
|
7
5
|
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.
|
|
8
6
|
|
|
9
7
|
---
|
|
@@ -660,30 +658,308 @@ console.log(`${gemsLeft} gems remaining`)
|
|
|
660
658
|
|
|
661
659
|
---
|
|
662
660
|
|
|
661
|
+
### `sprite.ts` — Free-roaming entity system
|
|
662
|
+
|
|
663
|
+
Sprites are entities that move freely in pixel space — not locked to the tile grid like `TileMap` cells. Use sprites for players, enemies, bullets, particles: anything that moves at sub-pixel precision or with physics.
|
|
664
|
+
|
|
665
|
+
Sprites use the same 8×8 bitmap format as `drawSprite` and the same `SpectrumColor` palette. They integrate directly with `resolveX` / `resolveY` from `collision.ts` and with `TileMap.isSolid` for platformer-style collision detection.
|
|
666
|
+
|
|
667
|
+
#### `Sprite` interface
|
|
668
|
+
|
|
669
|
+
| Field | Type | Default | Description |
|
|
670
|
+
|-------|------|---------|-------------|
|
|
671
|
+
| `x` | `number` | `0` | Horizontal position in game pixels (float allowed — rounded on render) |
|
|
672
|
+
| `y` | `number` | `0` | Vertical position in game pixels |
|
|
673
|
+
| `vx` | `number` | `0` | Horizontal velocity in pixels per millisecond |
|
|
674
|
+
| `vy` | `number` | `0` | Vertical velocity in pixels per millisecond |
|
|
675
|
+
| `bitmap` | `Uint8Array` | — | 8-byte sprite — one byte per row, bit 7 = leftmost pixel |
|
|
676
|
+
| `ink` | `SpectrumColor` | — | Foreground colour (`C.*` palette value) |
|
|
677
|
+
| `paper` | `SpectrumColor \| null` | `null` | Background colour, or `null` for transparent background |
|
|
678
|
+
| `flipX` | `boolean` | `false` | Render mirrored horizontally. Flipped bitmap is cached — no per-frame allocation |
|
|
679
|
+
| `visible` | `boolean` | `true` | When `false`, `renderSprite` skips this entity entirely |
|
|
680
|
+
|
|
681
|
+
#### `createSprite(bitmap, ink, paper?): Sprite`
|
|
682
|
+
|
|
683
|
+
Creates a `Sprite` at `(0, 0)` with zero velocity. `paper` defaults to `null` (transparent background).
|
|
684
|
+
|
|
685
|
+
```ts
|
|
686
|
+
import { createSprite, C } from 'zx-kit'
|
|
687
|
+
|
|
688
|
+
const PLAYER_BM = new Uint8Array([0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x7E, 0x18, 0x3C])
|
|
689
|
+
const BULLET_BM = new Uint8Array([0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00])
|
|
690
|
+
|
|
691
|
+
const player = createSprite(PLAYER_BM, C.B_CYAN) // transparent bg
|
|
692
|
+
const bullet = createSprite(BULLET_BM, C.B_WHITE, C.BLACK) // opaque bg
|
|
693
|
+
player.x = 16
|
|
694
|
+
player.y = 80
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
#### `moveSprite(sprite, dt): void`
|
|
698
|
+
|
|
699
|
+
Advances `sprite.x` by `vx * dt` and `sprite.y` by `vy * dt`. Call once per frame **before** collision resolution.
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
// Typical frame update order:
|
|
703
|
+
applyGravity(player, 0.003, dt)
|
|
704
|
+
moveSprite(player, dt)
|
|
705
|
+
const rx = resolveX(player, map, player.x)
|
|
706
|
+
const ry = resolveY(player, map, player.y)
|
|
707
|
+
player.x = rx.x
|
|
708
|
+
player.y = ry.y
|
|
709
|
+
if (ry.hitBottom) player.vy = 0
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
#### `applyGravity(sprite, gravity, dt): void`
|
|
713
|
+
|
|
714
|
+
Adds `gravity * dt` to `sprite.vy`. Call once per frame **before** `moveSprite`.
|
|
715
|
+
|
|
716
|
+
- `gravity` is in pixels per millisecond² — typical values: `0.002`–`0.005`
|
|
717
|
+
- Accumulates naturally across frames so the sprite accelerates while airborne
|
|
718
|
+
|
|
719
|
+
```ts
|
|
720
|
+
// Gentle gravity (platformer)
|
|
721
|
+
applyGravity(player, 0.003, dt)
|
|
722
|
+
|
|
723
|
+
// Strong gravity (bullet hell with falling debris)
|
|
724
|
+
applyGravity(debris, 0.008, dt)
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
#### `renderSprite(ctx, sprite): void`
|
|
728
|
+
|
|
729
|
+
Draws the sprite at `(Math.round(sprite.x), Math.round(sprite.y))`.
|
|
730
|
+
Skips if `sprite.visible === false`.
|
|
731
|
+
Respects `flipX` (cached mirror) and `paper: null` (transparent — only ink pixels drawn).
|
|
732
|
+
|
|
733
|
+
Call **after** all physics updates and collision resolution, as the last step before `drawScanlines`.
|
|
734
|
+
|
|
735
|
+
```ts
|
|
736
|
+
// Transparent sprite over a scrolling background
|
|
737
|
+
player.paper = null // default — no background square
|
|
738
|
+
renderSprite(ctx, player)
|
|
739
|
+
|
|
740
|
+
// Opaque sprite (always paints its 8×8 cell background)
|
|
741
|
+
bullet.paper = C.BLACK
|
|
742
|
+
renderSprite(ctx, bullet)
|
|
743
|
+
|
|
744
|
+
// Facing direction
|
|
745
|
+
player.flipX = (player.vx < 0)
|
|
746
|
+
renderSprite(ctx, player)
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
#### Full example — platformer player
|
|
750
|
+
|
|
751
|
+
```ts
|
|
752
|
+
import { createSprite, moveSprite, applyGravity, renderSprite, C, CELL } from 'zx-kit'
|
|
753
|
+
import { resolveX, resolveY } from 'zx-kit'
|
|
754
|
+
import { tickMovement, consumeFlag } from 'zx-kit'
|
|
755
|
+
|
|
756
|
+
const PLAYER_BM = new Uint8Array([0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x7E, 0x24, 0x66])
|
|
757
|
+
|
|
758
|
+
const player = createSprite(PLAYER_BM, C.B_CYAN)
|
|
759
|
+
player.x = 2 * CELL
|
|
760
|
+
player.y = 10 * CELL
|
|
761
|
+
|
|
762
|
+
let onGround = false
|
|
763
|
+
const JUMP_VELOCITY = -0.25 // pixels/ms upward
|
|
764
|
+
const WALK_VELOCITY = 0.08 // pixels/ms horizontal
|
|
765
|
+
const GRAVITY = 0.003 // pixels/ms²
|
|
766
|
+
|
|
767
|
+
function updatePlayer(dt: number, map: TileMap) {
|
|
768
|
+
// Input
|
|
769
|
+
const dir = tickMovement(dt)
|
|
770
|
+
player.vx = dir === 'right' ? WALK_VELOCITY
|
|
771
|
+
: dir === 'left' ? -WALK_VELOCITY
|
|
772
|
+
: 0
|
|
773
|
+
if (dir === 'up' && onGround) player.vy = JUMP_VELOCITY
|
|
774
|
+
|
|
775
|
+
// Flip sprite to face movement direction
|
|
776
|
+
if (player.vx < 0) player.flipX = true
|
|
777
|
+
if (player.vx > 0) player.flipX = false
|
|
778
|
+
|
|
779
|
+
// Physics
|
|
780
|
+
applyGravity(player, GRAVITY, dt)
|
|
781
|
+
moveSprite(player, dt)
|
|
782
|
+
|
|
783
|
+
// Collision
|
|
784
|
+
const rx = resolveX(player, map, player.x)
|
|
785
|
+
player.x = rx.x
|
|
786
|
+
if (rx.hitLeft || rx.hitRight) player.vx = 0
|
|
787
|
+
|
|
788
|
+
const ry = resolveY(player, map, player.y)
|
|
789
|
+
player.y = ry.y
|
|
790
|
+
onGround = ry.hitBottom
|
|
791
|
+
if (ry.hitBottom || ry.hitTop) player.vy = 0
|
|
792
|
+
}
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
### `collision.ts` — AABB collision detection and resolution
|
|
798
|
+
|
|
799
|
+
Axis-aligned bounding box (AABB) helpers for sprite-vs-sprite overlap tests and sprite-vs-`TileMap` collision resolution. All coordinates are in game pixels.
|
|
800
|
+
|
|
801
|
+
#### `Rect` interface
|
|
802
|
+
|
|
803
|
+
| Field | Type | Description |
|
|
804
|
+
|-------|------|-------------|
|
|
805
|
+
| `x` | `number` | Left edge in game pixels |
|
|
806
|
+
| `y` | `number` | Top edge in game pixels |
|
|
807
|
+
| `w` | `number` | Width in game pixels |
|
|
808
|
+
| `h` | `number` | Height in game pixels |
|
|
809
|
+
|
|
810
|
+
#### `spriteRect(sprite): Rect`
|
|
811
|
+
|
|
812
|
+
Returns the bounding `Rect` for a sprite — always `CELL × CELL` at the sprite's current position. Uses the raw float position (no rounding) for accurate same-frame overlap tests.
|
|
813
|
+
|
|
814
|
+
```ts
|
|
815
|
+
const rect = spriteRect(player) // { x: 24.5, y: 80.0, w: 8, h: 8 }
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
#### `rectsOverlap(a, b): boolean`
|
|
819
|
+
|
|
820
|
+
Returns `true` when rectangles share at least one pixel. Touching edges (zero-area overlap) returns `false`.
|
|
821
|
+
|
|
822
|
+
```ts
|
|
823
|
+
rectsOverlap(spriteRect(bullet), spriteRect(enemy)) // hit test
|
|
824
|
+
rectsOverlap({ x: 0, y: 0, w: 32, h: 32 }, { x: 16, y: 16, w: 8, h: 8 }) // contained → true
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
#### `spritesOverlap(a, b): boolean`
|
|
828
|
+
|
|
829
|
+
Shorthand for `rectsOverlap(spriteRect(a), spriteRect(b))`.
|
|
830
|
+
|
|
831
|
+
```ts
|
|
832
|
+
if (spritesOverlap(player, coin)) collectCoin(coin)
|
|
833
|
+
if (bullets.some(b => spritesOverlap(b, player))) loseLife()
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
#### `isSolidAt(map, px, py): boolean`
|
|
837
|
+
|
|
838
|
+
Tests whether the game pixel `(px, py)` falls inside a solid tile. Converts to tile coordinates via `Math.floor(px / CELL)`. Out-of-bounds pixels return `true` — the map boundary is an implicit solid wall.
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
// Is the pixel directly below the player's feet solid?
|
|
842
|
+
if (isSolidAt(map, player.x, player.y + CELL)) onGround = true
|
|
843
|
+
|
|
844
|
+
// Same check via resolveY — usually more convenient
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
#### `resolveX(sprite, map, newX): { x, hitLeft, hitRight }`
|
|
848
|
+
|
|
849
|
+
Resolves a proposed horizontal move against solid tiles. Tests the leading edge of the sprite's bounding box at `newX` and returns the clamped position plus hit flags.
|
|
850
|
+
|
|
851
|
+
- `hitLeft` — the sprite hit a wall while moving left
|
|
852
|
+
- `hitRight` — the sprite hit a wall while moving right
|
|
853
|
+
- Always returns the original `newX` (unclamped) when no collision occurs
|
|
854
|
+
- Out-of-bounds overshoot is clamped correctly — the map boundary acts as an implicit wall
|
|
855
|
+
|
|
856
|
+
```ts
|
|
857
|
+
moveSprite(player, dt)
|
|
858
|
+
const { x, hitLeft, hitRight } = resolveX(player, map, player.x)
|
|
859
|
+
player.x = x
|
|
860
|
+
if (hitLeft || hitRight) player.vx = 0
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
#### `resolveY(sprite, map, newY): { y, hitTop, hitBottom }`
|
|
864
|
+
|
|
865
|
+
Resolves a proposed vertical move against solid tiles. Same contract as `resolveX`.
|
|
866
|
+
|
|
867
|
+
- `hitBottom` — the sprite landed on a floor tile (use to detect "on ground" for jump logic)
|
|
868
|
+
- `hitTop` — the sprite bumped its head on a ceiling tile
|
|
869
|
+
|
|
870
|
+
```ts
|
|
871
|
+
applyGravity(player, 0.003, dt)
|
|
872
|
+
moveSprite(player, dt)
|
|
873
|
+
const { y, hitBottom, hitTop } = resolveY(player, map, player.y)
|
|
874
|
+
player.y = y
|
|
875
|
+
if (hitBottom) { player.vy = 0; onGround = true }
|
|
876
|
+
if (hitTop) { player.vy = 0 }
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
#### Full example — bullet hell enemy swarm
|
|
880
|
+
|
|
881
|
+
```ts
|
|
882
|
+
import { createSprite, moveSprite, renderSprite, spritesOverlap, C, CELL } from 'zx-kit'
|
|
883
|
+
import type { Sprite } from 'zx-kit'
|
|
884
|
+
|
|
885
|
+
const BULLET_BM = new Uint8Array([0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x3C, 0x18, 0x00])
|
|
886
|
+
const ENEMY_BM = new Uint8Array([0x3C, 0x7E, 0xFF, 0xDB, 0xFF, 0x66, 0x42, 0x81])
|
|
887
|
+
|
|
888
|
+
const bullets: Sprite[] = []
|
|
889
|
+
const enemies: Sprite[] = []
|
|
890
|
+
|
|
891
|
+
// Spawn a bullet traveling upward
|
|
892
|
+
function fireBullet(x: number, y: number) {
|
|
893
|
+
const b = createSprite(BULLET_BM, C.B_YELLOW)
|
|
894
|
+
b.x = x; b.y = y; b.vy = -0.3 // pixels/ms upward
|
|
895
|
+
bullets.push(b)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Spawn enemies in a row
|
|
899
|
+
for (let i = 0; i < 8; i++) {
|
|
900
|
+
const e = createSprite(ENEMY_BM, C.B_RED, C.BLACK)
|
|
901
|
+
e.x = i * 2 * CELL + 8
|
|
902
|
+
e.y = 2 * CELL
|
|
903
|
+
e.vx = 0.03 * (i % 2 === 0 ? 1 : -1) // alternate directions
|
|
904
|
+
enemies.push(e)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Per-frame update
|
|
908
|
+
function updateBulletHell(dt: number) {
|
|
909
|
+
// Move all bullets
|
|
910
|
+
for (const b of bullets) moveSprite(b, dt)
|
|
911
|
+
|
|
912
|
+
// Move all enemies
|
|
913
|
+
for (const e of enemies) moveSprite(e, dt)
|
|
914
|
+
|
|
915
|
+
// Hit tests — O(bullets × enemies), fine for dozens of sprites
|
|
916
|
+
for (const b of bullets) {
|
|
917
|
+
for (const e of enemies) {
|
|
918
|
+
if (e.visible && b.visible && spritesOverlap(b, e)) {
|
|
919
|
+
b.visible = false // bullet disappears
|
|
920
|
+
e.visible = false // enemy explodes
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Remove off-screen bullets
|
|
926
|
+
bullets.splice(0, bullets.length, ...bullets.filter(b => b.visible && b.y > -CELL))
|
|
927
|
+
|
|
928
|
+
// Render
|
|
929
|
+
for (const e of enemies) if (e.visible) renderSprite(ctx, e)
|
|
930
|
+
for (const b of bullets) if (b.visible) renderSprite(ctx, b)
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
663
936
|
## File structure
|
|
664
937
|
|
|
665
938
|
```
|
|
666
939
|
zx-kit/
|
|
667
|
-
├── package.json
|
|
668
|
-
├── tsconfig.json
|
|
940
|
+
├── package.json # exports: { ".": "./dist/index.js" }
|
|
941
|
+
├── tsconfig.json # strict, emits to dist/
|
|
669
942
|
├── README.md
|
|
670
|
-
├── src/
|
|
671
|
-
│ ├── index.ts
|
|
672
|
-
│ ├── palette.ts
|
|
673
|
-
│ ├── font.ts
|
|
674
|
-
│ ├── renderer.ts
|
|
675
|
-
│ │
|
|
676
|
-
│ ├── audio.ts
|
|
677
|
-
│ │
|
|
678
|
-
│ │
|
|
679
|
-
│ │
|
|
680
|
-
│ ├── input.ts
|
|
681
|
-
│ │
|
|
682
|
-
│ ├── ui.ts
|
|
683
|
-
│ │
|
|
684
|
-
│ │
|
|
685
|
-
│
|
|
686
|
-
|
|
943
|
+
├── src/ # TypeScript source
|
|
944
|
+
│ ├── index.ts # barrel — re-exports everything
|
|
945
|
+
│ ├── palette.ts # SCALE, CELL, C, SpectrumColor
|
|
946
|
+
│ ├── font.ts # FONT, getCharRow
|
|
947
|
+
│ ├── renderer.ts # setupCanvas, mirrorSprite, drawSprite, drawChar, drawText,
|
|
948
|
+
│ │ # drawTextCentered, flashBorder, drawScanlines
|
|
949
|
+
│ ├── audio.ts # initAudio, resumeAudio, beep, playPattern, Note,
|
|
950
|
+
│ │ # getAudioContext, getMasterGain,
|
|
951
|
+
│ │ # getMasterVolume, setMasterVolume,
|
|
952
|
+
│ │ # increaseVolume, decreaseVolume
|
|
953
|
+
│ ├── input.ts # initInput, tickMovement, consumeFlag/Debug/Pause/AnyKey,
|
|
954
|
+
│ │ # isHeld, resetInput, Direction
|
|
955
|
+
│ ├── ui.ts # drawBox, drawFrame, drawPanelTitle,
|
|
956
|
+
│ │ # drawProgressBar, tickUI, renderUI, resetUI,
|
|
957
|
+
│ │ # BorderOptions, DrawProgressBarOptions
|
|
958
|
+
│ ├── tilemap.ts # createTileMap, Tile, Viewport, TileMap
|
|
959
|
+
│ ├── sprite.ts # Sprite, createSprite, moveSprite, applyGravity, renderSprite
|
|
960
|
+
│ └── collision.ts # Rect, spriteRect, rectsOverlap, spritesOverlap,
|
|
961
|
+
│ # isSolidAt, resolveX, resolveY
|
|
962
|
+
└── dist/ # compiled output (generated by npm run build)
|
|
687
963
|
├── index.js / .d.ts
|
|
688
964
|
└── ...
|
|
689
965
|
```
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Sprite } from './sprite.js';
|
|
2
|
+
import type { TileMap } from './tilemap.js';
|
|
3
|
+
/**
|
|
4
|
+
* Axis-aligned bounding rectangle in game pixels.
|
|
5
|
+
*/
|
|
6
|
+
export interface Rect {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
w: number;
|
|
10
|
+
h: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns the bounding `Rect` for a sprite (always `CELL × CELL`).
|
|
14
|
+
* Position is taken from `sprite.x / sprite.y` without rounding —
|
|
15
|
+
* use this for precise overlap tests during the same frame the sprite moved.
|
|
16
|
+
*/
|
|
17
|
+
export declare function spriteRect(sprite: Sprite): Rect;
|
|
18
|
+
/**
|
|
19
|
+
* Returns `true` when rectangles `a` and `b` overlap (share at least one pixel).
|
|
20
|
+
* Touching edges (shared border, zero overlap area) returns `false`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* if (rectsOverlap(spriteRect(bullet), spriteRect(enemy))) { hitEnemy() }
|
|
24
|
+
*/
|
|
25
|
+
export declare function rectsOverlap(a: Rect, b: Rect): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Shorthand for `rectsOverlap(spriteRect(a), spriteRect(b))`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* if (spritesOverlap(player, coin)) collectCoin()
|
|
31
|
+
*/
|
|
32
|
+
export declare function spritesOverlap(a: Sprite, b: Sprite): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Returns `true` when the game pixel at `(px, py)` falls inside a solid tile.
|
|
35
|
+
* Converts pixel coords to tile coords via integer division by `CELL`.
|
|
36
|
+
* Out-of-bounds pixels are treated as solid (map boundary is an implicit wall).
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* if (isSolidAt(map, player.x, player.y + CELL)) { player.vy = 0 } // on floor
|
|
40
|
+
*/
|
|
41
|
+
export declare function isSolidAt(map: TileMap, px: number, py: number): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Resolves a proposed horizontal movement for a sprite against solid tiles.
|
|
44
|
+
* Tests the leading edge of the sprite's bounding box at the target x position.
|
|
45
|
+
* Returns the clamped x coordinate and collision flags.
|
|
46
|
+
*
|
|
47
|
+
* Call BEFORE `resolveY` so each axis is resolved independently.
|
|
48
|
+
*
|
|
49
|
+
* @param sprite - Sprite being moved (uses current `sprite.y` for vertical extent)
|
|
50
|
+
* @param map - Tile map to test solidity against
|
|
51
|
+
* @param newX - Proposed new x position (after `moveSprite`)
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* moveSprite(player, dt)
|
|
55
|
+
* const rx = resolveX(player, map, player.x)
|
|
56
|
+
* player.x = rx.x
|
|
57
|
+
* if (rx.hitLeft || rx.hitRight) player.vx = 0
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveX(sprite: Sprite, map: TileMap, newX: number): {
|
|
60
|
+
x: number;
|
|
61
|
+
hitLeft: boolean;
|
|
62
|
+
hitRight: boolean;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Resolves a proposed vertical movement for a sprite against solid tiles.
|
|
66
|
+
* Tests the leading edge of the sprite's bounding box at the target y position.
|
|
67
|
+
* Returns the clamped y coordinate and collision flags.
|
|
68
|
+
*
|
|
69
|
+
* Typical platformer pattern: `hitBottom` means the sprite landed on a floor.
|
|
70
|
+
* Zero out `vy` on `hitBottom` and `hitTop` to prevent tunnelling.
|
|
71
|
+
*
|
|
72
|
+
* @param sprite - Sprite being moved (uses current `sprite.x` for horizontal extent)
|
|
73
|
+
* @param map - Tile map to test solidity against
|
|
74
|
+
* @param newY - Proposed new y position (after `moveSprite`)
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const ry = resolveY(player, map, player.y)
|
|
78
|
+
* player.y = ry.y
|
|
79
|
+
* if (ry.hitBottom) { player.vy = 0; onGround = true }
|
|
80
|
+
* if (ry.hitTop) { player.vy = 0 }
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveY(sprite: Sprite, map: TileMap, newY: number): {
|
|
83
|
+
y: number;
|
|
84
|
+
hitTop: boolean;
|
|
85
|
+
hitBottom: boolean;
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=collision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collision.d.ts","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C;;GAEG;AACH,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,GAAG,OAAO,CAKtD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAEvE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAyBpD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAwBpD"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { CELL } from './palette.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the bounding `Rect` for a sprite (always `CELL × CELL`).
|
|
4
|
+
* Position is taken from `sprite.x / sprite.y` without rounding —
|
|
5
|
+
* use this for precise overlap tests during the same frame the sprite moved.
|
|
6
|
+
*/
|
|
7
|
+
export function spriteRect(sprite) {
|
|
8
|
+
return { x: sprite.x, y: sprite.y, w: CELL, h: CELL };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns `true` when rectangles `a` and `b` overlap (share at least one pixel).
|
|
12
|
+
* Touching edges (shared border, zero overlap area) returns `false`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* if (rectsOverlap(spriteRect(bullet), spriteRect(enemy))) { hitEnemy() }
|
|
16
|
+
*/
|
|
17
|
+
export function rectsOverlap(a, b) {
|
|
18
|
+
return a.x < b.x + b.w &&
|
|
19
|
+
a.x + a.w > b.x &&
|
|
20
|
+
a.y < b.y + b.h &&
|
|
21
|
+
a.y + a.h > b.y;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Shorthand for `rectsOverlap(spriteRect(a), spriteRect(b))`.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* if (spritesOverlap(player, coin)) collectCoin()
|
|
28
|
+
*/
|
|
29
|
+
export function spritesOverlap(a, b) {
|
|
30
|
+
return rectsOverlap(spriteRect(a), spriteRect(b));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns `true` when the game pixel at `(px, py)` falls inside a solid tile.
|
|
34
|
+
* Converts pixel coords to tile coords via integer division by `CELL`.
|
|
35
|
+
* Out-of-bounds pixels are treated as solid (map boundary is an implicit wall).
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* if (isSolidAt(map, player.x, player.y + CELL)) { player.vy = 0 } // on floor
|
|
39
|
+
*/
|
|
40
|
+
export function isSolidAt(map, px, py) {
|
|
41
|
+
return map.isSolid(Math.floor(px / CELL), Math.floor(py / CELL));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolves a proposed horizontal movement for a sprite against solid tiles.
|
|
45
|
+
* Tests the leading edge of the sprite's bounding box at the target x position.
|
|
46
|
+
* Returns the clamped x coordinate and collision flags.
|
|
47
|
+
*
|
|
48
|
+
* Call BEFORE `resolveY` so each axis is resolved independently.
|
|
49
|
+
*
|
|
50
|
+
* @param sprite - Sprite being moved (uses current `sprite.y` for vertical extent)
|
|
51
|
+
* @param map - Tile map to test solidity against
|
|
52
|
+
* @param newX - Proposed new x position (after `moveSprite`)
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* moveSprite(player, dt)
|
|
56
|
+
* const rx = resolveX(player, map, player.x)
|
|
57
|
+
* player.x = rx.x
|
|
58
|
+
* if (rx.hitLeft || rx.hitRight) player.vx = 0
|
|
59
|
+
*/
|
|
60
|
+
export function resolveX(sprite, map, newX) {
|
|
61
|
+
const y0 = sprite.y;
|
|
62
|
+
const y1 = sprite.y + CELL - 1;
|
|
63
|
+
let x = newX;
|
|
64
|
+
let hitLeft = false;
|
|
65
|
+
let hitRight = false;
|
|
66
|
+
if (newX < sprite.x) {
|
|
67
|
+
// Moving left — check left edge
|
|
68
|
+
if (isSolidAt(map, newX, y0) || isSolidAt(map, newX, y1)) {
|
|
69
|
+
// Clamp to in-bounds before computing tile column (avoids negative col from OOB overshoot)
|
|
70
|
+
const safeX = Math.max(0, newX);
|
|
71
|
+
x = (Math.floor(safeX / CELL) + 1) * CELL;
|
|
72
|
+
hitLeft = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (newX > sprite.x) {
|
|
76
|
+
// Moving right — check right edge
|
|
77
|
+
if (isSolidAt(map, newX + CELL - 1, y0) || isSolidAt(map, newX + CELL - 1, y1)) {
|
|
78
|
+
const safeEdge = Math.min(map.cols * CELL - 1, newX + CELL - 1);
|
|
79
|
+
x = Math.floor(safeEdge / CELL) * CELL - CELL;
|
|
80
|
+
hitRight = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { x, hitLeft, hitRight };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolves a proposed vertical movement for a sprite against solid tiles.
|
|
87
|
+
* Tests the leading edge of the sprite's bounding box at the target y position.
|
|
88
|
+
* Returns the clamped y coordinate and collision flags.
|
|
89
|
+
*
|
|
90
|
+
* Typical platformer pattern: `hitBottom` means the sprite landed on a floor.
|
|
91
|
+
* Zero out `vy` on `hitBottom` and `hitTop` to prevent tunnelling.
|
|
92
|
+
*
|
|
93
|
+
* @param sprite - Sprite being moved (uses current `sprite.x` for horizontal extent)
|
|
94
|
+
* @param map - Tile map to test solidity against
|
|
95
|
+
* @param newY - Proposed new y position (after `moveSprite`)
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* const ry = resolveY(player, map, player.y)
|
|
99
|
+
* player.y = ry.y
|
|
100
|
+
* if (ry.hitBottom) { player.vy = 0; onGround = true }
|
|
101
|
+
* if (ry.hitTop) { player.vy = 0 }
|
|
102
|
+
*/
|
|
103
|
+
export function resolveY(sprite, map, newY) {
|
|
104
|
+
const x0 = sprite.x;
|
|
105
|
+
const x1 = sprite.x + CELL - 1;
|
|
106
|
+
let y = newY;
|
|
107
|
+
let hitTop = false;
|
|
108
|
+
let hitBottom = false;
|
|
109
|
+
if (newY < sprite.y) {
|
|
110
|
+
// Moving up — check top edge
|
|
111
|
+
if (isSolidAt(map, x0, newY) || isSolidAt(map, x1, newY)) {
|
|
112
|
+
const safeY = Math.max(0, newY);
|
|
113
|
+
y = (Math.floor(safeY / CELL) + 1) * CELL;
|
|
114
|
+
hitTop = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (newY > sprite.y) {
|
|
118
|
+
// Moving down — check bottom edge
|
|
119
|
+
if (isSolidAt(map, x0, newY + CELL - 1) || isSolidAt(map, x1, newY + CELL - 1)) {
|
|
120
|
+
const safeEdge = Math.min(map.rows * CELL - 1, newY + CELL - 1);
|
|
121
|
+
y = Math.floor(safeEdge / CELL) * CELL - CELL;
|
|
122
|
+
hitBottom = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { y, hitTop, hitBottom };
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=collision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collision.js","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
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,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,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;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
|
package/dist/index.js
CHANGED
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;AACvB,cAAc,cAAc,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;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
|
package/dist/renderer.d.ts
CHANGED
|
@@ -17,6 +17,26 @@ import { type SpectrumColor } from './palette.js';
|
|
|
17
17
|
* const ctx = setupCanvas(canvas, 4, 256, 208) // taller canvas for status rows
|
|
18
18
|
*/
|
|
19
19
|
export declare function setupCanvas(canvas: HTMLCanvasElement, scale: number, width?: number, height?: number): CanvasRenderingContext2D;
|
|
20
|
+
/**
|
|
21
|
+
* Applies a CRT monitor curvature effect to the canvas element via CSS.
|
|
22
|
+
*
|
|
23
|
+
* Current implementation: rounded corners (`border-radius`), inset edge shadow,
|
|
24
|
+
* and a subtle perspective tilt. This is a deliberate abstraction — the CSS
|
|
25
|
+
* internals may be replaced by WebGL barrel distortion in a future version
|
|
26
|
+
* without changing this API signature.
|
|
27
|
+
*
|
|
28
|
+
* Call once after `setupCanvas`. Call again with `intensity = 0` to remove.
|
|
29
|
+
*
|
|
30
|
+
* @param canvas - The `<canvas>` element to apply the effect to
|
|
31
|
+
* @param intensity - Strength 0–1 (default `1`; `0` removes all effect)
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const ctx = setupCanvas(canvas, 4)
|
|
35
|
+
* curveDisplay(canvas) // default full intensity
|
|
36
|
+
* curveDisplay(canvas, 0.5) // subtle effect
|
|
37
|
+
* curveDisplay(canvas, 0) // remove effect
|
|
38
|
+
*/
|
|
39
|
+
export declare function curveDisplay(canvas: HTMLCanvasElement, intensity?: number): void;
|
|
20
40
|
/**
|
|
21
41
|
* Flips an 8×8 sprite horizontally. Returns a new `Uint8Array`.
|
|
22
42
|
* Use to derive left-facing sprites from right-facing definitions at module load time.
|
package/dist/renderer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAiB1D;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,iBAAiB,EACzB,KAAK,EAAE,MAAM,EACb,KAAK,SAAM,EACX,MAAM,SAAM,GACX,wBAAwB,CAS1B;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAUxD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,wBAAwB,EAC7B,MAAM,EAAE,UAAU,EAClB,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EACpB,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GACvC,IAAI,CAKN;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EACpB,GAAG,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,aAAa,GACxC,IAAI,CAON;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EACpB,GAAG,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,aAAa,GACxC,IAAI,CAIN;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,aAAa,GACxC,IAAI,CAGN;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,wBAAwB,EAC7B,KAAK,SAAO,GACX,IAAI,CASN;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,aAAuB,GAClC,IAAI,CAWN"}
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAiB1D;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,iBAAiB,EACzB,KAAK,EAAE,MAAM,EACb,KAAK,SAAM,EACX,MAAM,SAAM,GACX,wBAAwB,CAS1B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,EAAE,SAAS,SAAI,GAAG,IAAI,CAW3E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAUxD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,wBAAwB,EAC7B,MAAM,EAAE,UAAU,EAClB,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EACpB,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GACvC,IAAI,CAKN;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EACpB,GAAG,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,aAAa,GACxC,IAAI,CAON;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EACpB,GAAG,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,aAAa,GACxC,IAAI,CAIN;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,aAAa,GACxC,IAAI,CAGN;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,wBAAwB,EAC7B,KAAK,SAAO,GACX,IAAI,CASN;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,UAAU,GAAE,aAAuB,GAClC,IAAI,CAWN"}
|
package/dist/renderer.js
CHANGED
|
@@ -36,6 +36,37 @@ export function setupCanvas(canvas, scale, width = 256, height = 192) {
|
|
|
36
36
|
ctx.scale(scale, scale);
|
|
37
37
|
return ctx;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Applies a CRT monitor curvature effect to the canvas element via CSS.
|
|
41
|
+
*
|
|
42
|
+
* Current implementation: rounded corners (`border-radius`), inset edge shadow,
|
|
43
|
+
* and a subtle perspective tilt. This is a deliberate abstraction — the CSS
|
|
44
|
+
* internals may be replaced by WebGL barrel distortion in a future version
|
|
45
|
+
* without changing this API signature.
|
|
46
|
+
*
|
|
47
|
+
* Call once after `setupCanvas`. Call again with `intensity = 0` to remove.
|
|
48
|
+
*
|
|
49
|
+
* @param canvas - The `<canvas>` element to apply the effect to
|
|
50
|
+
* @param intensity - Strength 0–1 (default `1`; `0` removes all effect)
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const ctx = setupCanvas(canvas, 4)
|
|
54
|
+
* curveDisplay(canvas) // default full intensity
|
|
55
|
+
* curveDisplay(canvas, 0.5) // subtle effect
|
|
56
|
+
* curveDisplay(canvas, 0) // remove effect
|
|
57
|
+
*/
|
|
58
|
+
export function curveDisplay(canvas, intensity = 1) {
|
|
59
|
+
const i = Math.max(0, Math.min(1, intensity));
|
|
60
|
+
if (i === 0) {
|
|
61
|
+
canvas.style.borderRadius = '';
|
|
62
|
+
canvas.style.boxShadow = '';
|
|
63
|
+
canvas.style.transform = '';
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
canvas.style.borderRadius = `${Math.round(18 * i)}px`;
|
|
67
|
+
canvas.style.boxShadow = `inset 0 0 ${Math.round(60 * i)}px rgba(0,0,0,${(0.45 * i).toFixed(2)})`;
|
|
68
|
+
canvas.style.transform = `perspective(${Math.round(800 - 400 * i)}px) rotateX(${(2.5 * i).toFixed(1)}deg)`;
|
|
69
|
+
}
|
|
39
70
|
/**
|
|
40
71
|
* Flips an 8×8 sprite horizontally. Returns a new `Uint8Array`.
|
|
41
72
|
* Use to derive left-facing sprites from right-facing definitions at module load time.
|
package/dist/renderer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}
|
package/dist/sprite.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type SpectrumColor } from './palette.js';
|
|
2
|
+
/**
|
|
3
|
+
* A game entity with position, velocity, and an 8×8 bitmap.
|
|
4
|
+
* Use `createSprite` to create, `moveSprite` / `applyGravity` to update each frame,
|
|
5
|
+
* and `renderSprite` to draw.
|
|
6
|
+
*/
|
|
7
|
+
export interface Sprite {
|
|
8
|
+
/** Horizontal position in game pixels (float allowed — rounded on render). */
|
|
9
|
+
x: number;
|
|
10
|
+
/** Vertical position in game pixels (float allowed — rounded on render). */
|
|
11
|
+
y: number;
|
|
12
|
+
/** Horizontal velocity in pixels per millisecond. */
|
|
13
|
+
vx: number;
|
|
14
|
+
/** Vertical velocity in pixels per millisecond. */
|
|
15
|
+
vy: number;
|
|
16
|
+
/** 8-byte sprite bitmap — one byte per row, bit 7 = leftmost pixel. */
|
|
17
|
+
bitmap: Uint8Array;
|
|
18
|
+
/** Foreground colour (`C.*` palette value). */
|
|
19
|
+
ink: SpectrumColor;
|
|
20
|
+
/**
|
|
21
|
+
* Background colour, or `null` for a transparent background.
|
|
22
|
+
* Transparent sprites draw only ink pixels — useful over scrolling backgrounds.
|
|
23
|
+
*/
|
|
24
|
+
paper: SpectrumColor | null;
|
|
25
|
+
/** When `true` the sprite is rendered mirrored horizontally. Bitmap is cached. */
|
|
26
|
+
flipX: boolean;
|
|
27
|
+
/** When `false` `renderSprite` skips this sprite entirely. */
|
|
28
|
+
visible: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates a `Sprite` at position (0, 0) with zero velocity.
|
|
32
|
+
*
|
|
33
|
+
* @param bitmap - 8-byte sprite definition
|
|
34
|
+
* @param ink - Foreground colour
|
|
35
|
+
* @param paper - Background colour, or `null` for transparent (default `null`)
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const player = createSprite(PLAYER_BITMAP, C.B_CYAN)
|
|
39
|
+
* const bullet = createSprite(BULLET_BITMAP, C.B_WHITE, C.BLACK)
|
|
40
|
+
*/
|
|
41
|
+
export declare function createSprite(bitmap: Uint8Array, ink: SpectrumColor, paper?: SpectrumColor | null): Sprite;
|
|
42
|
+
/**
|
|
43
|
+
* Advances the sprite position by `vx * dt` and `vy * dt`.
|
|
44
|
+
* Call once per frame before collision resolution.
|
|
45
|
+
*
|
|
46
|
+
* @param sprite - Sprite to update
|
|
47
|
+
* @param dt - Elapsed time in milliseconds since last frame
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Game loop
|
|
51
|
+
* moveSprite(player, dt)
|
|
52
|
+
* const { x, hitBottom } = resolveY(player, map, player.y)
|
|
53
|
+
* player.y = y
|
|
54
|
+
* if (hitBottom) player.vy = 0
|
|
55
|
+
*/
|
|
56
|
+
export declare function moveSprite(sprite: Sprite, dt: number): void;
|
|
57
|
+
/**
|
|
58
|
+
* Adds `gravity * dt` to the sprite's vertical velocity.
|
|
59
|
+
* Call once per frame before `moveSprite`.
|
|
60
|
+
*
|
|
61
|
+
* @param sprite - Sprite to affect
|
|
62
|
+
* @param gravity - Acceleration in pixels per millisecond² (e.g. `0.003` for gentle gravity)
|
|
63
|
+
* @param dt - Elapsed time in milliseconds
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* applyGravity(player, 0.003, dt)
|
|
67
|
+
* moveSprite(player, dt)
|
|
68
|
+
*/
|
|
69
|
+
export declare function applyGravity(sprite: Sprite, gravity: number, dt: number): void;
|
|
70
|
+
/**
|
|
71
|
+
* Draws the sprite at its current position (rounded to nearest pixel).
|
|
72
|
+
* Does nothing when `sprite.visible === false`.
|
|
73
|
+
* Respects `sprite.paper` (transparent when `null`) and `sprite.flipX`.
|
|
74
|
+
*
|
|
75
|
+
* Must be called after all position updates and collision resolution for the frame.
|
|
76
|
+
*
|
|
77
|
+
* @param ctx - Canvas 2D context (same one used for the rest of the frame)
|
|
78
|
+
* @param sprite - Sprite to render
|
|
79
|
+
*/
|
|
80
|
+
export declare function renderSprite(ctx: CanvasRenderingContext2D, sprite: Sprite): void;
|
|
81
|
+
//# sourceMappingURL=sprite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sprite.d.ts","sourceRoot":"","sources":["../src/sprite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAGjD;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACrB,8EAA8E;IAC9E,CAAC,EAAE,MAAM,CAAA;IACT,4EAA4E;IAC5E,CAAC,EAAE,MAAM,CAAA;IACT,qDAAqD;IACrD,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAA;IACV,uEAAuE;IACvE,MAAM,EAAE,UAAU,CAAA;IAClB,+CAA+C;IAC/C,GAAG,EAAE,aAAa,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,aAAa,GAAG,IAAI,CAAA;IAC3B,kFAAkF;IAClF,KAAK,EAAE,OAAO,CAAA;IACd,8DAA8D;IAC9D,OAAO,EAAE,OAAO,CAAA;CACjB;AAcD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,aAAa,EAClB,KAAK,GAAE,aAAa,GAAG,IAAW,GACjC,MAAM,CAER;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAG3D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAE9E;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAiBhF"}
|
package/dist/sprite.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {} from './palette.js';
|
|
2
|
+
import { mirrorSprite, drawSprite as _drawSprite } from './renderer.js';
|
|
3
|
+
// Lazy cache: avoids creating a new mirrored Uint8Array every frame
|
|
4
|
+
const _flipCache = new WeakMap();
|
|
5
|
+
function getFlipped(bm) {
|
|
6
|
+
let flipped = _flipCache.get(bm);
|
|
7
|
+
if (!flipped) {
|
|
8
|
+
flipped = mirrorSprite(bm);
|
|
9
|
+
_flipCache.set(bm, flipped);
|
|
10
|
+
}
|
|
11
|
+
return flipped;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates a `Sprite` at position (0, 0) with zero velocity.
|
|
15
|
+
*
|
|
16
|
+
* @param bitmap - 8-byte sprite definition
|
|
17
|
+
* @param ink - Foreground colour
|
|
18
|
+
* @param paper - Background colour, or `null` for transparent (default `null`)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const player = createSprite(PLAYER_BITMAP, C.B_CYAN)
|
|
22
|
+
* const bullet = createSprite(BULLET_BITMAP, C.B_WHITE, C.BLACK)
|
|
23
|
+
*/
|
|
24
|
+
export function createSprite(bitmap, ink, paper = null) {
|
|
25
|
+
return { x: 0, y: 0, vx: 0, vy: 0, bitmap, ink, paper, flipX: false, visible: true };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Advances the sprite position by `vx * dt` and `vy * dt`.
|
|
29
|
+
* Call once per frame before collision resolution.
|
|
30
|
+
*
|
|
31
|
+
* @param sprite - Sprite to update
|
|
32
|
+
* @param dt - Elapsed time in milliseconds since last frame
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Game loop
|
|
36
|
+
* moveSprite(player, dt)
|
|
37
|
+
* const { x, hitBottom } = resolveY(player, map, player.y)
|
|
38
|
+
* player.y = y
|
|
39
|
+
* if (hitBottom) player.vy = 0
|
|
40
|
+
*/
|
|
41
|
+
export function moveSprite(sprite, dt) {
|
|
42
|
+
sprite.x += sprite.vx * dt;
|
|
43
|
+
sprite.y += sprite.vy * dt;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Adds `gravity * dt` to the sprite's vertical velocity.
|
|
47
|
+
* Call once per frame before `moveSprite`.
|
|
48
|
+
*
|
|
49
|
+
* @param sprite - Sprite to affect
|
|
50
|
+
* @param gravity - Acceleration in pixels per millisecond² (e.g. `0.003` for gentle gravity)
|
|
51
|
+
* @param dt - Elapsed time in milliseconds
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* applyGravity(player, 0.003, dt)
|
|
55
|
+
* moveSprite(player, dt)
|
|
56
|
+
*/
|
|
57
|
+
export function applyGravity(sprite, gravity, dt) {
|
|
58
|
+
sprite.vy += gravity * dt;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Draws the sprite at its current position (rounded to nearest pixel).
|
|
62
|
+
* Does nothing when `sprite.visible === false`.
|
|
63
|
+
* Respects `sprite.paper` (transparent when `null`) and `sprite.flipX`.
|
|
64
|
+
*
|
|
65
|
+
* Must be called after all position updates and collision resolution for the frame.
|
|
66
|
+
*
|
|
67
|
+
* @param ctx - Canvas 2D context (same one used for the rest of the frame)
|
|
68
|
+
* @param sprite - Sprite to render
|
|
69
|
+
*/
|
|
70
|
+
export function renderSprite(ctx, sprite) {
|
|
71
|
+
if (!sprite.visible)
|
|
72
|
+
return;
|
|
73
|
+
const x = Math.round(sprite.x);
|
|
74
|
+
const y = Math.round(sprite.y);
|
|
75
|
+
const bm = sprite.flipX ? getFlipped(sprite.bitmap) : sprite.bitmap;
|
|
76
|
+
if (sprite.paper !== null) {
|
|
77
|
+
_drawSprite(ctx, bm, x, y, sprite.ink, sprite.paper);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Transparent background — draw only ink pixels
|
|
81
|
+
ctx.fillStyle = sprite.ink;
|
|
82
|
+
for (let row = 0; row < 8; row++) {
|
|
83
|
+
const byte = bm[row];
|
|
84
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
85
|
+
if (byte & (0x80 >> bit))
|
|
86
|
+
ctx.fillRect(x + bit, y + row, 1, 1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=sprite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sprite.js","sourceRoot":"","sources":["../src/sprite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,UAAU,IAAI,WAAW,EAAE,MAAM,eAAe,CAAA;AA+BvE,oEAAoE;AACpE,MAAM,UAAU,GAAG,IAAI,OAAO,EAA0B,CAAA;AAExD,SAAS,UAAU,CAAC,EAAc;IAChC,IAAI,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAChC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,YAAY,CAAC,EAAE,CAAC,CAAA;QAC1B,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IAC7B,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAkB,EAClB,GAAkB,EAClB,QAA8B,IAAI;IAElC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AACtF,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,EAAU;IACnD,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,EAAE,GAAG,EAAE,CAAA;IAC1B,MAAM,CAAC,CAAC,IAAI,MAAM,CAAC,EAAE,GAAG,EAAE,CAAA;AAC5B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,OAAe,EAAE,EAAU;IACtE,MAAM,CAAC,EAAE,IAAI,OAAO,GAAG,EAAE,CAAA;AAC3B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,GAA6B,EAAE,MAAc;IACxE,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAM;IAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC9B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC9B,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAA;IACnE,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1B,WAAW,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;IACtD,CAAC;SAAM,CAAC;QACN,gDAAgD;QAChD,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC,GAAG,CAAA;QAC1B,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;YACpB,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC;gBACjC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC;oBAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YAChE,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
|