mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
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
This commit is contained in:
parent
59905147b4
commit
48832ccb13
12 changed files with 2365 additions and 134 deletions
|
|
@ -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
|
||||
|
|
|
|||
204
examples/friendly_assets.ne
Normal file
204
examples/friendly_assets.ne
Normal 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
|
||||
BIN
examples/friendly_assets.nes
Normal file
BIN
examples/friendly_assets.nes
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue