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

205 lines
7.4 KiB
Text
Raw Normal View History

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