1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-18 06:36:57 +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

@ -350,6 +350,69 @@ fn lower_debug_frame_overrun_count_emits_peek() {
);
}
#[test]
fn lower_metasprite_draw_expands_to_one_op_per_tile() {
// `draw Hero at: (10, 20)` where Hero is a 4-tile metasprite
// should lower to four `DrawSprite` ops, each with the
// metasprite's underlying sprite name and one tile from the
// declaration's `frame:` array (offset by the sprite's base
// tile index — the runtime smiley occupies tile 0, so a
// single-sprite program starts user tiles at 1).
let ir = lower_ok(
r#"
game "T" { mapper: NROM }
sprite Tile {
pixels: [
"@@@@@@@@",
"@@@@@@@@",
"@@@@@@@@",
"@@@@@@@@",
"@@@@@@@@",
"@@@@@@@@",
"@@@@@@@@",
"@@@@@@@@"
]
}
metasprite Hero {
sprite: Tile
dx: [0, 8, 0, 8]
dy: [0, 0, 8, 8]
frame: [0, 0, 0, 0]
}
on frame {
draw Hero at: (10, 20)
wait_frame
}
start Main
"#,
);
let frame_fn = ir
.functions
.iter()
.find(|f| f.name.contains("frame"))
.unwrap();
let draws: Vec<_> = frame_fn
.blocks
.iter()
.flat_map(|b| &b.ops)
.filter_map(|op| match op {
IrOp::DrawSprite { sprite_name, .. } => Some(sprite_name.clone()),
_ => None,
})
.collect();
assert_eq!(
draws.len(),
4,
"metasprite with 4 tiles should expand to 4 DrawSprite ops"
);
for name in &draws {
assert_eq!(
name, "Tile",
"expanded ops should target the underlying sprite"
);
}
}
#[test]
fn lower_nested_struct_literal_init_expands_to_leaves() {
// A `Hero { pos: Vec2 { x: 1, y: 2 }, hp: 100, inv: [3,4,5,6] }`