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

Merge branch 'main' into claude/test-platformer-game-6S3TX

This commit is contained in:
Jason Hall 2026-04-13 15:55:50 -04:00 committed by GitHub
commit 54910f2498
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 3027 additions and 473 deletions

View file

@ -27,6 +27,7 @@ Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX]
| `sprites_and_palettes.ne` | sprites, scroll, cast | Inline CHR data, PPU scroll writes, type casting |
| `mmc1_banked.ne` | MMC1, banks, multiply | Banked mapper with software multiply |
| `palette_and_background.ne` | palette, background, set_palette, load_background | Reset-time initial load plus vblank-safe runtime swaps |
| `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. |
| `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, enemies, coin pickups, user-declared SFX + music, and a Title → Playing state machine with an autopilot so the headless harness still hits interesting gameplay at frame 180. Regenerate the tile art with `cargo run --bin gen_platformer_tiles`. |
## Emulator Controls

View file

@ -35,43 +35,45 @@ game "Audio Demo" {
// ── User-declared sound effects ──
//
// An `sfx` block is a frame-accurate envelope for pulse 1. `pitch`
// latches the pulse period once on trigger; `volume` runs one entry
// per frame, so the envelope length controls the effect duration.
// `duty` (0-3) picks the pulse waveform shape (2 = 50% square).
// is latched into `$4002` once on trigger — the v1 audio driver
// never updates the pulse period mid-effect — so a single scalar
// byte is the natural form for the current pipeline. `envelope:`
// is a friendlier alias for `volume:`: both describe the per-frame
// volume ramp that shapes the effect. `duty` (0-3) picks the pulse
// waveform (2 = 50% square).
// A gentle rising chirp, longer than the builtin coin.
sfx LongCoin {
duty: 2
pitch: [0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50]
volume: [15, 14, 13, 12, 11, 9, 7, 5, 3, 1]
pitch: 0x50
envelope: [15, 14, 13, 12, 11, 9, 7, 5, 3, 1]
}
// A sharp two-part zap — quick high spike into silence.
sfx Zap {
duty: 3
pitch: [0x20, 0x20, 0x20, 0x20, 0x20]
volume: [15, 12, 8, 4, 1]
pitch: 0x20
envelope: [15, 12, 8, 4, 1]
}
// ── User-declared music tracks ──
//
// A `music` block is a flat list of `(pitch, duration)` note pairs.
// Pitch 0 = rest; 1-60 = period table index (C1..B5, middle C = 37).
// Duration is in frames (so at 60 fps, 30 = half second per note).
// Music loops by default; set `repeat: false` for one-shot cues.
// Notes are written in note-name form (`C4`, `E4`, `G4`, …) with a
// default frames-per-note set by `tempo:`. Any note can override
// the default by trailing the name with an explicit duration
// (e.g. `G4 40` holds twice as long). `rest` (or the alias `_`) is
// a silent beat. Use the legacy `37, 20, 41, 20, ...` pair form by
// leaving `tempo:` out if you'd rather pick pitches by index.
// A cheerful four-note looping theme.
// A cheerful four-note looping theme — every note is one-third of
// a second long, and the whole loop is 120 frames = 2 seconds.
music Theme {
duty: 2
volume: 10
repeat: true
tempo: 20
notes: [
37, 20, // C4
41, 20, // E4
44, 20, // G4
49, 20, // C5
44, 20, // G4
41, 20, // E4
C4, E4, G4, C5, G4, E4
]
}

204
examples/friendly_assets.ne Normal file
View file

@ -0,0 +1,204 @@
// Friendly Assets Demo — showcases NEScript's pleasant-to-author
// asset syntax. Every one of the "raw byte" art forms (palettes,
// CHR tiles, nametables, attribute tables, sfx envelopes, music
// note indices) has a friendlier alternative so you don't have to
// reach for a hex editor to make your game look and sound good.
//
// This demo uses every one of them at once:
//
// 1. Named NES colours (`black`, `sky_blue`, `dk_red`, …) inside
// a grouped `palette` declaration with per-slot fields and a
// shared `universal:` colour — auto-fixes the $3F10 mirror.
// 2. A `sprite` declared with ASCII pixel art rather than 16
// bytes of 2-bitplane CHR.
// 3. A `background` laid out with a `legend` + `map:` tilemap
// and a `palette_map:` grid that auto-packs the 64-byte
// attribute table.
// 4. An `sfx` with a scalar `pitch:` (matching the v1 driver's
// latch-once behaviour) and the friendlier `envelope:` alias.
// 5. A `music` track written with note names (`C4, E4 40, rest 10`)
// plus a default `tempo:` so the common case stays concise.
//
// Build: cargo run --release -- build examples/friendly_assets.ne
// Output: examples/friendly_assets.nes
game "Friendly Assets" {
mapper: NROM
mirroring: horizontal
}
// ── Palette ────────────────────────────────────────────────
//
// Grouped form: one `universal:` field feeds every sub-palette's
// shared index-0 byte (fixing the PPU mirror trap where the last
// four bytes of the 32-byte blob would otherwise clobber the
// background universal colour). Each `bgN` / `spN` field only
// needs three colours — `universal` supplies the fourth.
//
// Every colour here is a named constant; see docs/language-guide.md
// for the full list. Hex byte literals (`0x0F`, `0x21`, …) still
// work if you prefer them.
palette Sunset {
universal: black // shared background colour
bg0: [dk_blue, blue, sky_blue] // horizon
bg1: [dk_red, red, peach] // clouds
bg2: [dk_olive, olive, cream] // ground highlights
bg3: [dk_gray, lt_gray, white] // rocks
sp0: [dk_blue, blue, sky_blue] // player body uses bg0 tones
sp1: [dk_red, red, peach] // enemies
sp2: [dk_green, green, mint] // pickups
sp3: [dk_gray, lt_gray, white] // UI / text
}
// ── Sprite ─────────────────────────────────────────────────
//
// ASCII pixel art. Characters map to 2-bit palette indices:
//
// `.` or ` ` → 0 (transparent)
// `#` or `1` → 1 (darker shade)
// `%` or `2` → 2 (mid shade)
// `@` or `3` → 3 (highlight)
//
// 8x8 for a plain tile; multiples of 8 in either dimension for
// multi-tile sprites (emitted in row-major reading order).
sprite Star {
pixels: [
"...@@...",
"..@##@..",
".@####@.",
"@######@",
".@####@.",
"..@##@..",
"...@@...",
"........"
]
}
// ── Background ─────────────────────────────────────────────
//
// `legend { ... }` names each tile index with a single character;
// `map:` is the 32×30 nametable authored directly as one string
// per row. Short rows are right-padded with tile 0; fewer than 30
// rows pads the bottom with tile 0 as well.
//
// `palette_map:` is a 16×15 grid of sub-palette digits (`0`-`3`)
// where each cell covers one 16×16 metatile. The parser packs it
// into the awkward 8×8 attribute table automatically — no more
// hand-computing `(br<<6)|(bl<<4)|(tr<<2)|tl` by eye.
background Horizon {
legend {
".": 0 // sky (built-in smiley tile as a stand-in)
"S": 0 // star placeholder — same tile
}
// Sparse map: every cell in the first three rows is tile 0.
// This is enough to exercise the tile + attribute pipeline
// without depending on sprite CHR we haven't declared.
map: [
"................................",
"................................",
"................................"
]
// Paint the top two metatile rows (rows 0-1) with sub-palette 1
// so the sky uses the warm cloud colours. The next 13 rows use
// sub-palette 0 (cool blues).
palette_map: [
"1111111111111111",
"1111111111111111",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000"
]
}
// ── SFX ────────────────────────────────────────────────────
//
// Scalar `pitch:` + `envelope:` alias. The v1 audio driver only
// reads the first `pitch` byte (it latches the pulse period on
// trigger and never updates it) so a per-frame array was always
// redundant — a single byte makes the intent obvious.
sfx Chime {
duty: 2
pitch: 0x40 // latched period byte
envelope: [15, 14, 13, 12, 10, 8, 6, 4, 2, 1]
}
// ── Music ──────────────────────────────────────────────────
//
// `tempo:` sets the default frames-per-note; individual notes can
// override it by trailing the name with a frame count. Note names
// are C1..B5 with `Cs4`/`Db4` style accidentals (`#` and `♭` are
// not valid identifier characters so sharp/flat use a letter).
music Waltz {
duty: 2
volume: 10
repeat: true
tempo: 20
notes: [
C4, E4, G4, C5, // rising C major chord
G4 40, // held chord tone
rest 10, // brief pause
E4, C4, // descent
B4, D5, Fs5, B5, // up a fifth with a sharp
A4 30, // landing note
rest 20 // bar break
]
}
// ── Game state ─────────────────────────────────────────────
var px: u8 = 120
var py: u8 = 112
var tick: u8 = 0
var music_on: bool = false
on frame {
tick += 1
// Let the d-pad nudge the star around the screen.
if button.right { px += 1 }
if button.left { px -= 1 }
if button.down { py += 1 }
if button.up { py -= 1 }
// A / B ping the sfx so the envelope is audible under Mesen.
if button.a { play Chime }
if button.b { play Chime }
// Auto-start the waltz once on the first frame so the jsnes
// golden-capture run (which doesn't simulate input) still
// hits the music driver's start path.
if tick == 10 {
if music_on {
// Already playing — nothing to do.
} else {
start_music Waltz
music_on = true
}
}
// Keep a baseline sfx chirp so the audio golden is non-silent
// even when the music is between notes.
if tick == 120 {
tick = 0
play Chime
}
draw Star at: (px, py)
}
start Main

Binary file not shown.

View file

@ -24,39 +24,39 @@ game "Palette + BG Demo" {
// ── Palettes ────────────────────────────────────────────────
//
// Each palette is 32 bytes laid out in the standard NES order:
// $00-$0F background palettes 0-3 (4 colours each)
// $10-$1F sprite palettes 0-3 (4 colours each, $10/$14/$18/$1C
// mirror the bg slots)
// Both palettes are authored in grouped form: one shared
// `universal:` colour feeds every sub-palette's index-0 slot,
// which auto-fixes the `$3F10/$3F14/$3F18/$3F1C` PPU mirror
// trap (the parser handles the packing). Each `bgN` / `spN`
// field only needs three colours — the universal is prepended.
//
// `CoolBlues` uses deep blue background tiles with pale highlights;
// `WarmReds` swaps them for red/orange tones. Every 4 bytes is a
// sub-palette.
// Named NES colours resolve to master-palette indices via the
// curated name table; see `docs/language-guide.md` for the full
// list. Hex byte literals (`0x01`, `0x14`, …) still work if you
// need a colour that doesn't have a name.
palette CoolBlues {
colors: [
0x0F, 0x01, 0x11, 0x21, // bg palette 0: black, deep blue, sky, pale
0x0F, 0x02, 0x12, 0x22, // bg palette 1
0x0F, 0x0C, 0x1C, 0x2C, // bg palette 2
0x0F, 0x0B, 0x1B, 0x2B, // bg palette 3
0x0F, 0x01, 0x11, 0x21, // sprite palette 0
0x0F, 0x16, 0x27, 0x30, // sprite palette 1
0x0F, 0x14, 0x24, 0x34, // sprite palette 2
0x0F, 0x0B, 0x1B, 0x2B // sprite palette 3
]
universal: black
bg0: [dk_blue, blue, sky_blue] // deep blue highlights
bg1: [indigo, royal_blue, periwinkle] // cool purples
bg2: [dk_cyan, cyan, lt_cyan] // aquas
bg3: [dk_teal, teal, lt_teal] // teal accents
sp0: [dk_blue, blue, sky_blue] // matches bg0
sp1: [red, orange, white] // warm accent sprites
sp2: [magenta, lt_magenta, pale_pink] // magenta sprites
sp3: [dk_teal, teal, lt_teal] // matches bg3
}
palette WarmReds {
colors: [
0x0F, 0x06, 0x16, 0x26, // bg palette 0: black, dark red, red, peach
0x0F, 0x07, 0x17, 0x27, // bg palette 1
0x0F, 0x08, 0x18, 0x28, // bg palette 2
0x0F, 0x09, 0x19, 0x29, // bg palette 3
0x0F, 0x06, 0x16, 0x26, // sprite palette 0
0x0F, 0x16, 0x27, 0x30, // sprite palette 1
0x0F, 0x14, 0x24, 0x34, // sprite palette 2
0x0F, 0x0B, 0x1B, 0x2B // sprite palette 3
]
universal: black
bg0: [dk_red, red, lt_red] // fiery warm bg
bg1: [brown, dk_orange, orange] // autumnal
bg2: [dk_olive, olive, yellow] // muted yellows
bg3: [dk_green, green, lt_green] // greens for contrast
sp0: [dk_red, red, lt_red] // matches bg0
sp1: [red, orange, white] // highlight sprites
sp2: [magenta, lt_magenta, pale_pink] // pinks
sp3: [dk_teal, teal, lt_teal] // cool accent
}
// ── Backgrounds ─────────────────────────────────────────────
@ -74,40 +74,46 @@ palette WarmReds {
// top-left to bottom-right so the swap is visually obvious.
background TitleScreen {
tiles: [
// Row 14 (offset 14*32 = 448): a ribbon of tile 0
// across columns 4..28.
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// Row 14
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
// Both backgrounds resolve to an all-zero nametable for this
// demo — the visual difference between them comes from the
// palette swap triggered alongside `load_background`. We still
// author them with a `legend` + `map:` block here so the demo
// exercises the pleasant syntax the same way a real game
// would. The '.' legend entry alone is enough to fill an
// all-tile-0 background; every row is padded to 32 columns
// automatically by the parser.
legend { ".": 0 }
map: [
"................................", // row 0
"................................", // row 1
"................................", // row 2
"................................", // row 3
"................................", // row 4
"................................", // row 5
"................................", // row 6
"................................", // row 7
"................................", // row 8
"................................", // row 9
"................................", // row 10
"................................", // row 11
"................................", // row 12
"................................", // row 13
"................................", // row 14 (ribbon)
"................................" // row 15
]
}
background StageOne {
tiles: [
// A few scattered tile-0s across the first 4 rows so the
// background visibly differs from TitleScreen after the
// swap. Remaining rows are zero-padded and render as
// tile 0 anyway — the visual difference comes from the
// palette swap triggered alongside.
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
// StageOne's visual difference comes from the palette swap —
// the tile grid is still the built-in smiley everywhere. We
// only declare 4 rows to prove the parser's zero-padding
// still works with the new tilemap form.
legend { ".": 0 }
map: [
"................................",
"................................",
"................................",
"................................"
]
}

View file

@ -32,54 +32,34 @@ game "Platformer" {
}
// ── Palette ──────────────────────────────────────────────────
// The background palette drives the sky / hill / ground look.
// Each sub-palette reserves index 0 for the shared background
// colour (sky blue $22), plus three working colours.
// bg 0 (sky region): sky blue, white, light gray, black
// bg 1 (air/blocks row): sky blue, red, peach, dark red
// bg 2 (ground row): sky blue, lime green, dark green, dark brown
// bg 3 (unused reserve): sky blue, yellow, orange, gold
// The sprite sub-palette 0 (used by every `draw` statement — the
// OAM attribute byte is always 0 in the current NEScript
// runtime) carries the hero's red/peach/white body colours.
// NOTE: the first byte of every sub-palette must match. The PPU
// mirrors $3F10/$3F14/$3F18/$3F1C onto $3F00/$3F04/$3F08/$3F0C, so
// writing 8 distinct "index 0" bytes ends up clobbering the
// background universal colour with the last write. The canonical
// fix — and what every NES game does — is to use the universal
// background colour ($22 sky blue here) in all 8 slots. For sprite
// palettes, index 0 is always transparent regardless of the stored
// value, so this costs nothing visually.
// Authored in grouped form: one shared `universal:` colour fills
// every sub-palette's index-0 slot automatically. That single
// declaration is enough to dodge the notorious `$3F10/$3F14/$3F18/
// $3F1C` PPU mirror trap — when a program writes 8 distinct
// "index 0" bytes sequentially, the last write clobbers the
// shared background colour and the screen turns the wrong colour
// (or all-black). Using one shared universal everywhere is what
// every shipped NES game does; the parser handles it for us.
//
// `0x22` is the lavender-blue NES master palette slot Super Mario
// Bros uses for its iconic sky. The colour names map to the
// curated set documented in `docs/language-guide.md`.
palette Main {
colors: [
// bg palette 0 — sky / clouds
// 0 = sky blue (universal bg)
// 1 = white
// 2 = light gray
// 3 = black
0x22, 0x30, 0x10, 0x0F,
// bg palette 1 — bricks, Q-blocks
// 1 = red, 2 = peach, 3 = dark red
0x22, 0x16, 0x27, 0x06,
// bg palette 2 — grass, hills, bushes, dirt
// 1 = light green
// 2 = light brown
// 3 = dark brown
0x22, 0x1A, 0x17, 0x07,
// bg palette 3 — unused but the mirroring still writes here
0x22, 0x28, 0x27, 0x17,
// sprite palette 0 — hero / enemy / coin
// (OAM attribute is always 0 in the current runtime so
// every sprite uses this one sub-palette)
// 1 = red cap, 2 = peach skin, 3 = white
0x22, 0x16, 0x27, 0x30,
// sprite palette 1 — reserved
0x22, 0x07, 0x17, 0x27,
// sprite palette 2 — reserved
0x22, 0x2A, 0x1A, 0x30,
// sprite palette 3 — reserved
0x22, 0x28, 0x18, 0x38
]
universal: 0x22 // sky blue (NES $22)
// Background sub-palettes
bg0: [white, gray, black] // sky / clouds
bg1: [red, orange, dk_red] // bricks, Q-blocks
bg2: [bright_green, dk_orange, brown] // grass, hills, bushes, dirt
bg3: [yellow, orange, dk_orange] // reserved
// Sprite sub-palette 0 — every `draw` uses this one slot
// because the OAM attribute byte is always 0 in v0.1.
// 1 = red cap, 2 = orange skin, 3 = white highlight.
sp0: [red, orange, white]
sp1: [brown, dk_orange, orange] // reserved
sp2: [neon_green, bright_green, white] // reserved
sp3: [yellow, olive, cream] // reserved
}
// ── Tileset ──────────────────────────────────────────────────
@ -89,38 +69,149 @@ palette Main {
// same declaration drives the player, enemies, coins, and every
// background tile after tile 0. Tile 0 is reserved by the linker
// for the built-in smiley — the background leaves it unreferenced.
//
// All 15 tiles are authored as 8×8 ASCII pixel art and stacked
// vertically (1 tile wide × 15 tiles tall) so the parser splits
// them into consecutive CHR tile indices in reading order. The
// art uses the `.abc` vocabulary (`. = 0`, `a = 1`, `b = 2`,
// `c = 3`); regenerate with `cargo run --bin gen_platformer_tiles`.
sprite Tileset {
chr: [
pixels: [
// tile 1: Player head L
0x3F, 0x7F, 0x70, 0x61, 0xCF, 0xCD, 0xC0, 0xC0, 0x00, 0x00, 0x0F, 0x1F, 0x3F, 0x3F, 0x3F, 0x3F,
"..aaaaaa",
".aaaaaaa",
".aaabbbb",
".aabbbbc",
"aabbcccc",
"aabbccbc",
"aabbbbbb",
"aabbbbbb",
// tile 2: Player head R
0xFC, 0xFE, 0x0E, 0x83, 0xF8, 0x4C, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF,
"aaaaaa..",
"aaaaaaa.",
"bbbbaaa.",
"cbbbbbaa",
"cccccbbb",
"bcbbccbb",
"bbbbbbbb",
"bbbbbbbb",
// tile 3: Player body L
0x7F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x10, 0x18, 0x00, 0x7F, 0x7F, 0x67, 0x07,
".aaaaaaa",
"aaacaaaa",
"aaaccaaa",
"aaaaaaaa",
".bbbbbbb",
".bbbbbbb",
".bb..bbb",
"aaa..bbb",
// tile 4: Player body R
0xFE, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x07, 0x00, 0x08, 0x18, 0x00, 0xFE, 0xFE, 0xE6, 0xE0,
"aaaaaaa.",
"aaaacaaa",
"aaaccaaa",
"aaaaaaaa",
"bbbbbbb.",
"bbbbbbb.",
"bbb..bb.",
"bbb..aaa",
// tile 5: Enemy
0x3C, 0x7E, 0x76, 0xFF, 0x99, 0x7E, 0xBD, 0x99, 0x00, 0x00, 0x18, 0x38, 0x7E, 0x00, 0x00, 0x00,
"..aaaa..",
".aaaaaa.",
".aacbaa.",
"aacccaaa",
"abbccbba",
".aaaaaa.",
"a.aaaa.a",
"a..aa..a",
// tile 6: Coin
0x00, 0x00, 0x18, 0x24, 0x24, 0x18, 0x00, 0x00, 0x18, 0x3C, 0x7E, 0x7E, 0x7E, 0x7E, 0x3C, 0x18,
"...bb...",
"..bbbb..",
".bbccbb.",
".bcbbcb.",
".bcbbcb.",
".bbccbb.",
"..bbbb..",
"...bb...",
// tile 7: Grass top
0xFF, 0xBB, 0xFF, 0xFF, 0xFF, 0xFF, 0xBD, 0xFF, 0x00, 0x00, 0x00, 0x6A, 0xF7, 0xFF, 0xFF, 0xFF,
"aaaaaaaa",
"a.aaa.aa",
"aaaaaaaa",
"accacaca",
"ccccaccc",
"cccccccc",
"cbccccbc",
"cccccccc",
// tile 8: Dirt
0xFF, 0xBD, 0xFF, 0xF7, 0xFF, 0xBD, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
"cccccccc",
"cbccccbc",
"cccccccc",
"ccccbccc",
"cccccccc",
"cbccccbc",
"ccccbccc",
"cccccccc",
// tile 9: Brick
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x82, 0x82, 0x82, 0xFF, 0x10, 0x10, 0x10,
"cccccccc",
"caaaaaca",
"caaaaaca",
"caaaaaca",
"cccccccc",
"aaacaaaa",
"aaacaaaa",
"aaacaaaa",
// tile 10: Cloud L
0x00, 0x1C, 0x3E, 0x7F, 0x7F, 0x1F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x3E, 0x07,
"........",
"...aaa..",
"..aaaaa.",
".aaaaaaa",
".aaaaaaa",
".bbaaaaa",
"..bbbbba",
".....bbb",
// tile 11: Cloud R
0x00, 0x70, 0xF8, 0xFE, 0xFE, 0xF8, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x7C, 0xE0,
"........",
".aaa....",
"aaaaa...",
"aaaaaaa.",
"aaaaaaa.",
"aaaaabb.",
"abbbbb..",
"bbb.....",
// tile 12: Hill
0x00, 0x0C, 0x1E, 0x3F, 0x2F, 0x4B, 0x75, 0xC9, 0x00, 0x00, 0x00, 0x00, 0x10, 0x34, 0x0A, 0x36,
"........",
"....aa..",
"...aaaa.",
"..aaaaaa",
"..abaaaa",
".abbabaa",
".aaababa",
"aabbabba",
// tile 13: Bush
0x00, 0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x02, 0x06, 0x06, 0x55,
"........",
"...aa...",
"..aaaa..",
".aaaaac.",
"aaaaaaca",
"aaaaacca",
"aaaaacca",
"acacacac",
// tile 14: Q Block
0xFF, 0xFF, 0xC3, 0xDB, 0xD3, 0xC3, 0xFF, 0xFF, 0xFF, 0x81, 0xBD, 0xBD, 0xBD, 0xBD, 0x81, 0xFF,
"cccccccc",
"caaaaaac",
"cabbbbac",
"cabccbac",
"cabcbbac",
"cabbbbac",
"caaaaaac",
"cccccccc",
// tile 15: Sky (blank)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
"........",
"........",
"........",
"........",
"........",
"........",
"........",
"........"
]
}
@ -133,142 +224,121 @@ const TILE_ENEMY: u8 = 5
const TILE_COIN: u8 = 6
// ── Background ──────────────────────────────────────────────
// Full-screen 32×30 nametable showing a static level view that
// horizontal scrolling pans across. The attribute table splits
// the screen into three palette regions (sky/blocks/ground) so
// the grass and hills pick up the right colours even without
// per-tile attribute churn.
// The 32×30 nametable is authored as ASCII art with a `legend`
// block naming each tile by a single character. Horizontal
// scrolling pans this single nametable via `scroll()` so it only
// needs to look interesting at any X offset.
//
// `palette_map:` is the friendlier alternative to a 64-byte
// hand-packed attribute table: 16×15 metatile entries, one
// digit per 16×16 metatile, and the parser packs the 2-bit
// `(br<<6)|(bl<<4)|(tr<<2)|tl` quadrants automatically. The
// three palette regions are sky (`0`), brick row (`1`), and
// ground (`2`).
background Level {
tiles: [
// rows 0-4: clean sky
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
// row 5: clouds at x=4..5 and x=20..21
15, 15, 15, 15, 10, 11, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 10, 11, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
// row 6: cloud at x=12..13
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 10, 11, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
// rows 7-14: more clean sky
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
// row 15: brick/Q-block platform left, brick platform right
15, 15, 15, 15, 15, 15, 9, 9, 14, 9, 15, 15, 15, 15, 15, 15,
15, 15, 9, 9, 9, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
// rows 16-19: sky
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
// row 20: hills (3 on left, 4 on right)
15, 15, 12, 12, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
15, 15, 15, 15, 15, 15, 12, 12, 12, 12, 15, 15, 15, 15, 15, 15,
// row 21: bushes above the grass line
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 15, 15, 15,
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 15, 15, 15,
// row 22: grass top (full width)
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
// rows 23-29: dirt with a few buried bricks for texture
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 8,
8, 8, 8, 8, 8, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 9, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8
legend {
".": 15 // sky (blank)
"<": 10 // cloud left half
">": 11 // cloud right half
"#": 9 // brick
"Q": 14 // question block
"^": 12 // hill silhouette
"*": 13 // bush
"=": 7 // grass top
"%": 8 // dirt
}
map: [
"................................",
"................................",
"................................",
"................................",
"................................",
"....<>..............<>..........",
"............<>..................",
"................................",
"................................",
"................................",
"................................",
"................................",
"................................",
"................................",
"................................",
"......##Q#........###...........",
"................................",
"................................",
"................................",
"................................",
"..^^^.................^^^^......",
"..........***..............**...",
"================================",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%",
"%%%%%#%%%%%%%%%%%%#%%%%%%%%%%%%%",
"%%%%%%%%%%%#%%%%%%%%%%%%#%%%%%%%",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
]
attributes: [
// 8×8 attribute grid, one byte per 32×32-px metatile group.
// Each byte packs 4 identical 2-bit quadrants selecting a
// sub-palette (0..3):
// 0x00 = sub-palette 0 (sky / clouds)
// 0x55 = sub-palette 1 (bricks / Q-blocks)
// 0xAA = sub-palette 2 (grass / hills / dirt)
//
// Row mapping: ay 0-2 (nt rows 0-11) = sky, ay 3 (nt rows
// 12-15) = brick row, ay 4 (nt rows 16-19) = more sky, ay
// 5-7 (nt rows 20-29) = ground.
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA
palette_map: [
"0000000000000000", // metatile rows 0-5 → sky sub-palette
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"1111111111111111", // metatile rows 6-7 → brick sub-palette
"1111111111111111",
"0000000000000000", // metatile rows 8-9 → sky again
"0000000000000000",
"2222222222222222", // metatile rows 10-15 → ground sub-palette
"2222222222222222", // (rows 15 sits off-screen but the PPU
"2222222222222222", // still reads its attribute byte, so
"2222222222222222", // we emit it explicitly to match the
"2222222222222222", // hand-packed attribute table the
"2222222222222222" // old form was using.)
]
}
// ── Audio ────────────────────────────────────────────────────
//
// SFX: scalar `pitch:` matches the v1 driver's latch-once
// behaviour — pulse 1's period is sampled exactly once when the
// effect triggers and never updated, so a single byte is the
// natural form for the current pipeline. `envelope:` is the
// friendlier alias for `volume:`.
// Custom boingy jump sound — descending pitch arc on pulse 1.
// Custom boingy jump sound — fixed-pitch arc on pulse 1.
sfx Boing {
duty: 2
pitch: [0x30, 0x38, 0x40, 0x48, 0x50, 0x58, 0x60]
volume: [12, 11, 10, 9, 7, 5, 2]
pitch: 0x30
envelope: [12, 11, 10, 9, 7, 5, 2]
}
// Custom coin ding — short rising blip.
sfx Ding {
duty: 2
pitch: [0x20, 0x20, 0x20, 0x20]
volume: [14, 12, 8, 3]
pitch: 0x20
envelope: [14, 12, 8, 3]
}
// Custom looping underworld theme — playful four-bar loop on
// pulse 2 that plays while the player is alive.
// pulse 2 that plays while the player is alive. Note names
// (C4 / E4 / etc.) plus a default `tempo:` so each note only
// has to spell its frame count when it deviates from the beat.
music Theme {
duty: 2
volume: 9
repeat: true
tempo: 10
notes: [
37, 10, // C4
41, 10, // E4
44, 10, // G4
49, 10, // C5
44, 10, // G4
41, 10, // E4
39, 15, // D4
0, 5, // rest
36, 10, // B3
41, 10, // E4
44, 10, // G4
49, 20, // C5
0, 10 // rest
C4, E4, G4, C5, G4, E4,
D4 15,
rest 5,
B3, E4, G4,
C5 20,
rest 10
]
}

View file

@ -9,17 +9,38 @@ game "Asset Demo" {
mapper: NROM
}
// Define a sprite with inline CHR tile data (16 bytes = one 8x8 tile)
// This is a simple arrow pointing right
// Define a sprite with ASCII pixel art. Each character maps to a
// 2-bit palette index: `.` = 0 (transparent), `#` = 1, `%` = 2,
// `@` = 3. The parser handles the 2-bitplane CHR encoding, so we
// never touch hex bytes by hand.
//
// Arrow — a right-facing arrow in palette-index 1.
sprite Arrow {
chr: [0x18, 0x1C, 0xFE, 0xFF, 0xFF, 0xFE, 0x1C, 0x18,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
pixels: [
"...##...",
"...###..",
"#######.",
"########",
"########",
"#######.",
"...###..",
"...##..."
]
}
// Define a sprite with a heart shape
// Heart — a full-colour heart in palette-index 3 (the brightest
// shade, `@`).
sprite Heart {
chr: [0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00,
0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00]
pixels: [
".@@..@@.",
"@@@@@@@@",
"@@@@@@@@",
"@@@@@@@@",
".@@@@@@.",
"..@@@@..",
"...@@...",
"........"
]
}
var px: u8 = 128