1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-10 09:42:37 +00:00

metatiles + collision: metatileset, room, paint_room, collides_at

Closes §H. 2×2 metatiles and a parallel collision map are now a
first-class construct. `metatileset Name { metatiles: [{ id, tiles,
collide }, ...] }` declares a library of 2×2 tile bundles. `room Name
{ metatileset: M, layout: [...] }` lays them out on a 16×15 grid. The
compiler expands each room at compile time into:

- a 960-byte nametable (`__room_tiles_<name>`)
- a 64-byte attribute table (`__room_attrs_<name>`)
- a 240-byte collision bitmap (`__room_col_<name>`)

`paint_room Name` reuses the vblank-safe `load_background` update
machinery for the nametable blit and installs the collision bitmap
pointer into `ZP_ROOM_COL_LO`/`ZP_ROOM_COL_HI` (ZP $18/$19).
`collides_at(x, y)` JSRs into a small runtime helper that reads
`(room_col),Y` with `Y = (y & 0xF0) | (x >> 4)` and returns 0/1.
The helper links in only when the `__collides_at_used` marker is
emitted, so programs that declare a room but never query it pay
zero bytes for the subroutine.

`parse_byte_array` grows a `[value; count]` shortcut — 240-entry
`layout` arrays are unwieldy to spell out a byte at a time.

See `examples/metatiles_demo.ne` for the end-to-end flow: a probe
sprite bounces off walls via `collides_at` and lands on the left
side of the playfield at frame 180 — direct evidence that the
collision query works.

Also defers the register-allocator work from §"Code quality /
tooling" and documents the audio-goldens constraint in future-work
so the next agent sees it.
This commit is contained in:
Claude 2026-04-19 01:28:17 +00:00
parent 9719dc4111
commit 82b3d0d20a
No known key found for this signature in database
23 changed files with 1344 additions and 60 deletions

View file

@ -52,6 +52,7 @@ Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX]
| `sprite_0_split_demo.ne` | `sprite_0_split(x, y)` | Mid-frame scroll change driven by the PPU's sprite-0 hit flag (`$2002` bit 6), so the effect works on any mapper — NROM, UxROM, MMC1 — not just MMC3 via `on_scanline(N)`. Two-phase busy-wait (wait for clear, then wait for set) guarantees the hit we're responding to came from the current frame. Requires a sprite in OAM slot 0 that overlaps opaque background pixels; this demo uses a full smiley background so every frame's sprite-0 hit fires deterministically. |
| `i16_demo.ne` | `i16` signed 16-bit type | Negative literals fold to wide two's complement (`-10``$FFF6`), so `var vy: i16 = -10` stores the right bytes instead of the zero-extended `$00F6`. The companion `i16_negative_literal_sign_extends_to_wide_store` integration test guards the literal-fold path. |
| `signed_compare.ne` | signed `<` / `<=` / `>` / `>=` on `i8` and `i16` | Bounces a marker between X = 32 and X = 224 driven by signed `i16` compares against negative deltas, plus four pip sprites at the top of the screen that gate on directly-negative compares (`i8_neg < 0`, `i16_minus_one < i16_one`, etc.). The signed lowering uses the canonical `CMP / SBC / BVC / EOR #$80` overflow-correction idiom in `gen_cmp_signed_set_n` so the N flag reflects the true sign of the difference. The fourth pip is intentionally dark — it would only light if the lowering fell back to unsigned semantics. The companion integration tests `signed_i16_lt_emits_overflow_corrected_branch` and `signed_i8_lt_emits_overflow_corrected_branch` enforce the asm shape. |
| `metatiles_demo.ne` | `metatileset`, `room`, `paint_room`, `collides_at(x, y)` | 2×2 metatile level format plus a runtime collision query. The `metatileset Blocks` declaration carries two metatile IDs (floor, wall); the `room Dungeon` lays them out as a 16×15 grid the compiler expands to a 32×30 nametable + 240-byte collision bitmap at compile time. `paint_room Dungeon` reuses the existing `load_background` vblank-safe blit machinery and additionally installs the room's collision-bitmap pointer. A probe sprite walks right at 2 px/frame, bounces off the right wall when `collides_at(probe_x + 8, probe_y)` returns `true`, and is back on the left side of the playfield by the golden frame — direct evidence that the collision query works. |
| `sram_demo.ne` | `save { var ... }` | Battery-backed save block. The analyzer allocates `high_score` and `coins` at `$6000+` (cartridge SRAM window) instead of main RAM, and the linker flips iNES header byte-6 bit-1 so emulators (FCEUX, Mesen, Nestopia) load and persist the region from a `.sav` file alongside the ROM. SRAM is uninitialized at first power-on; production games should reserve a magic-byte sentinel and validate it before trusting the rest of the data — the compiler doesn't auto-initialize and emits W0111 if you try. |
| `vram_buffer_demo.ne` | `nt_set`, `nt_attr`, `nt_fill_h` | Minimal VRAM update buffer exercise — three single-tile writes, a 16-tile horizontal fill, and an attribute write firing every frame. Useful as a test case; see `hud_demo.ne` for a realistic usage pattern. |
| `hud_demo.ne` | VRAM buffer driving a classic status bar | A bouncing ball playfield with a HUD across the top: a 5-cell lives indicator that ticks down once per second via `nt_fill_h`, a score counter at the right edge that bumps on every wall hit via `nt_set`, and a one-shot `nt_attr` call at startup that flips the top-left metatile group to a red "UI chrome" palette. Shadow-comparing `score` / `lives` to their `last_*` copies keeps the buffer empty on the ~58-of-60 frames when nothing changed — per-frame cost scales with what actually moved. This is the pattern every nesdoug scoreboard / dialog box / destroyed-metatile animation is built on. |

138
examples/metatiles_demo.ne Normal file
View file

@ -0,0 +1,138 @@
// Metatiles + collision demo — shows the `metatileset` /
// `room` declarations, `paint_room` at reset, and
// `collides_at(x, y)` as a runtime query that actually
// changes observable behaviour.
//
// What the program does:
//
// - Declares a `metatileset Blocks` with two 2×2 metatiles:
// `id 0` = floor (CHR tile 0 everywhere, non-colliding)
// `id 1` = wall (CHR tile 0 everywhere, colliding)
//
// Both metatiles use tile 0 in CHR (the built-in smiley) so
// we don't need a sprite declaration just to author tile
// data. The palette swap below is what visually distinguishes
// floor from wall.
//
// - Declares a `room Dungeon` whose 16×15 layout frames the
// playfield with wall metatiles and leaves the interior as
// floor. `paint_room Dungeon` at reset blits the expanded
// 32×30 nametable into NT 0 and installs the room's
// collision bitmap pointer so `collides_at` can answer
// queries against this room.
//
// - A probe sprite marches right along row 9 starting at x=120.
// Every frame the probe advances two pixels. Before drawing
// the sprite we query `collides_at(probe_x + 8, probe_y)` —
// the +8 puts the test point near the sprite's right edge.
// When the query returns true (we've hit the right wall)
// we flip `dx`, so the probe bounces back.
//
// With dx=2 and start x=120, the right-edge hit fires around
// frame 56 (probe_x reaches 232). The probe then bounces
// back at -2 per frame, so by frame 180 (the harness golden
// frame) it's well inside the playfield. A regression that
// silently returned 0 from `collides_at` would leave the
// probe stuck against the wall or wrapping the u8 x
// coordinate — either way, the committed golden wouldn't
// match.
//
// Build: cargo run --release -- build examples/metatiles_demo.ne
game "Metatiles Demo" {
mapper: NROM
}
metatileset Blocks {
metatiles: [
{ id: 0, tiles: [0, 0, 0, 0], collide: false },
{ id: 1, tiles: [0, 0, 0, 0], collide: true },
],
}
// 16×15 grid. `1` = wall (colliding), `0` = floor (free).
//
// ┌────────────────┐ row 0
// │wwwwwwwwwwwwwwww│ row 1-12 frame walls on the edges
// │w..............w│ and leave the interior as floor
// │w..............w│ tiles. Row 9 (the middle-ish)
// │w..............w│ is where the probe walks.
// │w..............w│
// │w..............w│
// │w..............w│
// │w..............w│
// │w..............w│
// │w..............w│
// │w..............w│
// │wwwwwwwwwwwwwwww│
room Dungeon {
metatileset: Blocks,
layout: [
// row 0 — top wall
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// rows 1..13 — side walls bracket 14 floor cells
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
// row 14 — bottom wall
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
],
}
var probe_x: u8 = 120
var probe_y: u8 = 144 // roughly row 9 of the metatile grid
var dx: i8 = 2
var painted: u8 = 0
on frame {
// Paint the room once on the first frame. Doing this inside
// `on frame` (rather than an `on enter` handler) keeps the
// example minimal and avoids a separate state.
if painted == 0 {
paint_room Dungeon
painted = 1
}
// Move the probe two pixels per frame in whichever direction
// `dx` points. Two explicit branches because i8 runtime
// expressions don't compose directly with u8 assignment —
// keeping the sign logic out of the IR is cleaner than
// rigging up a cast chain.
if dx == 2 {
probe_x += 2
}
if dx == -2 {
probe_x -= 2
}
// Check the pixel just ahead of the probe's facing side.
// The probe is 8 pixels wide; testing at `probe_x + 8` is
// the right-edge probe and at `probe_x - 1` is the
// left-edge. Combined with the sprite width this means the
// probe flips direction one pixel before it would visually
// overlap the wall.
if dx == 2 {
if collides_at(probe_x + 8, probe_y) {
dx = -2
}
}
if dx == -2 {
if collides_at(probe_x - 1, probe_y) {
dx = 2
}
}
draw Probe at: (probe_x, probe_y)
}
start Main

BIN
examples/metatiles_demo.nes Normal file

Binary file not shown.