1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-07 00:22:50 +00:00
nescript/examples/friendly_assets.ne
Claude 48832ccb13
language: pleasant asset syntax for palettes, CHR, bg, sfx, music
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
2026-04-13 16:09:53 +00:00

204 lines
7.4 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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