1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/examples/metatiles_demo.ne
Claude 82b3d0d20a
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.
2026-04-19 01:28:17 +00:00

138 lines
5 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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