1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00

parser/lowering: declarative metasprites for multi-tile sprite groups

Multi-tile sprites used to require one hand-written `draw` per tile,
e.g. the four-call sequence in `examples/platformer.ne`'s
`draw_player()`. The new `metasprite Name { ... }` declaration
collects parallel `dx`/`dy`/`frame` arrays plus a reference to the
underlying sprite, and `draw Name at: (x, y)` expands to one OAM
slot per tile in the IR lowering — the codegen sees N regular
DrawSprite ops, so the runtime OAM cursor allocator picks them up
without any metasprite-specific awareness.

The metasprite's `frame:` array is interpreted *relative to the
underlying sprite's base tile*: index 0 means "the first tile this
sprite owns", which is the natural reading for a 16×16 hero whose
pixel art the asset resolver split into four consecutive tiles.
The lowering walks `program.sprites` to compute base tile indices
the same way `assets::resolve_sprites` would, then folds the base
into each frame entry before storing the metasprite info. Sprites
sourced from external `@chr(...)` / `@binary(...)` files whose
bytes aren't available at parse time fall back to a one-tile
assumption — those programs are rare and can declare metasprites
against pixel-art sprites instead.

The new `examples/metasprite_demo.ne` declares a 16×16 hero sprite
and arranges its four tiles into a metasprite, then sweeps the
hero across the screen so the harness captures it mid-motion.
The new keyword is added to the lexer/token list, and the parser
accepts `sprite:` (the otherwise-keyword) as a property name in
metasprite bodies so the natural spelling parses.

https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
This commit is contained in:
Claude 2026-04-15 03:13:30 +00:00
parent 9878b7d87d
commit 6b080316a4
No known key found for this signature in database
17 changed files with 619 additions and 0 deletions

View file

@ -32,6 +32,7 @@ Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX]
| `friendly_assets.ne` | named colours, grouped palette, pixel art, tilemap+legend, palette_map, scalar sfx pitch, note-name music | Exercises every "friendlier" asset syntax at once — the `palette` uses `bg0..sp3` + a shared `universal:`, the sprite is authored as ASCII pixel art, the background uses a `legend { ... } + map:` tilemap with a `palette_map:` for attributes, the sfx uses a scalar `pitch:` + `envelope:` alias, and the music uses note names (`C4, E4 40, rest 10`) with a `tempo:` default. |
| `noise_triangle_sfx.ne` | `channel: noise`, `channel: triangle` on `sfx` blocks | Demonstrates the noise and triangle sfx channels. Declares one noise burst and one triangle bass note, plays each on a timer so the emulator harness captures both the pixel output and the APU state. |
| `sfx_pitch_envelope.ne` | varying-pitch pulse SFX | A 16-frame frequency sweep written as a per-frame `pitch:` array on a Pulse-1 sfx. The compiler emits a separate `__sfx_pitch_<name>` blob and gates the audio tick's pitch update path on the `__sfx_pitch_used` marker, so programs that stick to the scalar `pitch:` form still get byte-identical ROM output. |
| `metasprite_demo.ne` | declarative multi-tile sprites | A 16×16 hero sprite split into a `metasprite Hero { sprite: Hero16, dx: [...], dy: [...], frame: [...] }` declaration. `draw Hero at: (px, py)` then expands to one `DrawSprite` op per tile in the IR lowering, each with its dx/dy added to the user's anchor point and the frame offset by the underlying sprite's base tile. The codegen needs no metasprite-specific support — it sees N regular draws and the OAM cursor allocator handles the slots. |
| `nested_structs.ne` | nested struct fields, array struct fields, chained literals | Two `Hero` instances each carry a `Vec2` position and a `u8[4]` inventory. Exercises `hero.pos.x` chained access, `hero.inv[i]` array-field access, and chained struct-literal initializers (`Hero { pos: Vec2 { x: ..., y: ... }, inv: [...] }`). |
| `platformer.ne` | **every subsystem** | End-to-end side-scrolling demo: custom CHR tileset, full 32×30 nametable with per-region attribute palettes, 2×2 metasprite hero with gravity/jump physics, wrap-around horizontal scrolling, stomp-or-die enemy collisions with a live stomp-count HUD, coin pickups, user-declared SFX + music, and a Title → Playing → GameOver state machine with a proximity-based autopilot so the headless harness cycles through stomp, stomp, die, and retry inside six seconds. Regenerate the tile art with `cargo run --bin gen_platformer_tiles`. |

View file

@ -0,0 +1,86 @@
// Metasprite demo — declarative multi-tile sprite groups.
//
// The `metasprite` keyword bundles a list of (dx, dy, frame)
// offsets into one named entity. `draw Hero at: (x, y)` then
// expands to one OAM slot per tile, so user code stops having
// to hand-write the four `draw Tileset at: (x+8, y+8) frame:
// TILE_PLAYER_BR` calls that platformer.ne uses today. Each
// tile inherits the OAM-cursor allocator from the runtime, so
// metasprites compose with regular `draw` statements without
// any extra wiring.
//
// Build: cargo run -- build examples/metasprite_demo.ne
game "Metasprite Demo" {
mapper: NROM
}
// 16×16 hero sprite drawn as ASCII pixel art. The parser splits
// it into four 8×8 tiles in row-major order: tile 0 = top-left,
// tile 1 = top-right, tile 2 = bottom-left, tile 3 = bottom-right.
// We refer to those four tiles by base offset below.
sprite Hero16 {
pixels: [
"..####....####..",
".######..######.",
"###@##....##@###",
"###@##....##@###",
"################",
"################",
".##############.",
"..############..",
"...##########...",
"....########....",
"....##....##....",
"....##....##....",
"....##....##....",
"....##....##....",
"...####..####...",
"..######..######"
]
}
// Arrange Hero16's four tiles into a 16×16 metasprite. Each
// row in the metasprite block is logically one OAM slot; the
// `dx` / `dy` arrays carry the per-tile offset from the
// metasprite's anchor point and `frame` carries the tile index.
//
// `sprite: Hero16` resolves to the base tile index reserved by
// the asset resolver; the four entries below pick consecutive
// tiles starting at that base — i.e. the four 8×8 tiles the
// pixel-art block was sliced into.
metasprite Hero {
sprite: Hero16
dx: [0, 8, 0, 8]
dy: [0, 0, 8, 8]
frame: [0, 1, 2, 3]
}
var px: u8 = 120
var py: u8 = 96
var dir: u8 = 0 // 0 = right, 1 = left
on frame {
// Bounce horizontally between two rails so the harness
// captures movement at frame 180 regardless of which
// direction we started in.
if dir == 0 {
px += 1
if px == 200 {
dir = 1
}
} else {
px -= 1
if px == 32 {
dir = 0
}
}
// One `draw Hero` expands to four `draw Hero16` calls under
// the hood. Compare with the four-line sequence in
// platformer.ne's `draw_player()` — same OAM layout, much
// less typing.
draw Hero at: (px, py)
}
start Main

Binary file not shown.