mirror of
https://github.com/imjasonh/nescript
synced 2026-07-07 00:22:50 +00:00
Adds six NES-friendly authoring shortcuts so programs don't have to
hand-pack hex bytes for every kind of art asset. Every new syntax is
strictly additive — existing examples keep their byte-identical ROMs
and goldens.
* palette: ~50 named NES colours (`black`, `sky_blue`, `dk_red`, …)
usable anywhere a colour byte is expected, plus a grouped-form
`bg0..sp3` + `universal:` shape that auto-fills every sub-
palette's first byte (fixing the `$3F10` mirror trap).
* sprite: `pixels:` ASCII-art alternative to 16-byte CHR, supporting
multi-tile sprites split in row-major reading order.
* sfx: scalar `pitch:` matching the v1 driver's latch-once behaviour,
plus `envelope:` as a friendlier alias for `volume:`.
* music: `tempo:` default duration + note-name notes (`C4, Eb4,
rest 10`) alongside the existing `pitch, duration` pair form.
* background: `legend { '.': 0, '#': 1 }` + `map:` string rows,
plus `palette_map:` grids that auto-pack the 64-byte attribute
table from 16×15 sub-palette digits.
A new `examples/friendly_assets.ne` exercises every shortcut at once
with a matching pixel + audio golden; the other 22 golden tests still
match byte-for-byte.
https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
204 lines
7.4 KiB
Text
204 lines
7.4 KiB
Text
// 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
|