1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-10 17:52:51 +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

@ -607,6 +607,7 @@ fn collect_source_temps(op: &IrOp, used: &mut HashSet<IrTemp>) {
| IrOp::Rand16(_, _)
| IrOp::SetPalette(_)
| IrOp::LoadBackground(_)
| IrOp::PaintRoom(_)
| IrOp::SourceLoc(_) => {}
IrOp::SeedRand(lo, hi) => {
used.insert(*lo);
@ -638,6 +639,10 @@ fn collect_source_temps(op: &IrOp, used: &mut HashSet<IrTemp>) {
used.insert(*len);
used.insert(*tile);
}
IrOp::CollidesAt { x, y, .. } => {
used.insert(*x);
used.insert(*y);
}
}
}
@ -672,6 +677,7 @@ fn op_dest(op: &IrOp) -> Option<IrTemp> {
IrOp::ReadInput(d, _) => Some(*d),
IrOp::ReadInputEdge { dest, .. } => Some(*dest),
IrOp::Peek(d, _) => Some(*d),
IrOp::CollidesAt { dest, .. } => Some(*dest),
// Rand8 / Rand16 have an observable side effect — advancing
// the PRNG state — so a statement-level call like
// `rand8()` (result discarded) must NOT be dropped by DCE.
@ -705,6 +711,7 @@ fn op_dest(op: &IrOp) -> Option<IrTemp> {
| IrOp::NtFillH { .. }
| IrOp::SetPalette(_)
| IrOp::LoadBackground(_)
| IrOp::PaintRoom(_)
| IrOp::SourceLoc(_) => None,
// 16-bit ops have two destinations; the simple single-dest
// DCE below would incorrectly drop a 16-bit op whose low