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:
parent
9878b7d87d
commit
6b080316a4
17 changed files with 619 additions and 0 deletions
|
|
@ -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`. |
|
||||
|
||||
|
|
|
|||
86
examples/metasprite_demo.ne
Normal file
86
examples/metasprite_demo.ne
Normal 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
|
||||
BIN
examples/metasprite_demo.nes
Normal file
BIN
examples/metasprite_demo.nes
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue