mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45:38 +00:00
Rewrites every example with non-trivial asset declarations to use
the pleasant QoL syntax introduced in the previous commit. Every
example still compiles to a byte-identical ROM (verified by a
temp-path diff before committing), so the committed `.nes` files
and the 23 emulator goldens are unchanged.
* platformer.ne — the centerpiece end-to-end demo:
- `palette Main` goes to grouped form with a shared
`universal: 0x22` (sky blue), one shared colour per
sub-palette, and named NES colours throughout; the
long-standing `$3F10` mirror-trap warning is now handled
by the parser and the manual pitfall comment is gone.
- `sprite Tileset` is 15 tiles of ASCII pixel art instead
of 240 bytes of inline hex.
- `background Level` uses a `legend { '.': 15, '#': 9, ... }`
block plus `map:` strings for the 32×30 nametable, and
`palette_map:` rows for the attribute table. The map
reads top-down like the rendered screen.
- SFX latch-once `pitch: 0x30` scalars + `envelope:` alias.
- `music Theme` uses note names + `tempo: 10` default.
* audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a
note-name `C4, E4, G4, ...` music track.
* palette_and_background.ne — grouped CoolBlues / WarmReds
palettes with `universal: black` + named colours, plus
`legend` + `map:` tilemaps for the two backgrounds.
* sprites_and_palettes.ne — Arrow and Heart sprites rewritten
as `pixels:` ASCII art.
Along the way, two small parser extensions support the rewrites:
- `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`,
matching the vocabulary every NES editor (and our own
gen_platformer_tiles.rs generator) uses.
- `palette_map_to_attrs` allows up to 16 metatile rows (the
full attribute-table coverage, including the off-screen
bottom half) and auto-replicates row 14 → row 15 when only
15 rows are supplied so the visible bottom of the screen
gets consistent sub-palette assignments by default. The old
15-row cap couldn't match a hand-packed `0xAA` attribute
table for the last row; the platformer required this to
stay byte-identical.
`scripts/gen_platformer_tiles.rs` is updated to emit the new
syntax directly (pixel-art `pixels:` block + `legend`/`map:`/
`palette_map:` for the background), so regenerating the
platformer tiles stays a one-liner.
474 lib tests + 64 integration tests pass (3 new parser tests
for `palette_map:` 15/16/17 rows and the `abc` alias). All 23
emulator goldens still match pixel- and sample-for-sample.
https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
124 lines
3.8 KiB
Text
124 lines
3.8 KiB
Text
// Audio Demo — showcases the full audio subsystem.
|
|
//
|
|
// NEScript has three audio statements:
|
|
// play Name — trigger an SFX on pulse 1 (one-shot)
|
|
// start_music Name — play a background music track on pulse 2
|
|
// stop_music — silence the music channel
|
|
//
|
|
// SFX and music tracks are compiled into PRG ROM data tables and
|
|
// walked one byte per NMI by the builtin audio driver. You can
|
|
// either use the builtin names (coin, jump, theme, ...) or declare
|
|
// your own via `sfx Name { ... }` / `music Name { ... }` blocks.
|
|
//
|
|
// Builtin SFX names:
|
|
// coin, pickup, collect — high ascending blip
|
|
// jump, hop — descending arc
|
|
// hit, damage, explode — low blast
|
|
// click, select, confirm — sharp beep
|
|
// cancel, back, error — low longer tone
|
|
// shoot, laser, fire — very high pulse
|
|
// step, footstep — low thud
|
|
//
|
|
// Builtin music names:
|
|
// title, theme, main — major arpeggio (looping)
|
|
// battle, boss — driving pulse (looping)
|
|
// win, victory, fanfare — ascending burst (one-shot)
|
|
// gameover, lose, fail — descending dirge (looping)
|
|
//
|
|
// Build: cargo run -- build examples/audio_demo.ne
|
|
// Output: examples/audio_demo.nes
|
|
|
|
game "Audio Demo" {
|
|
mapper: NROM
|
|
}
|
|
|
|
// ── User-declared sound effects ──
|
|
//
|
|
// An `sfx` block is a frame-accurate envelope for pulse 1. `pitch`
|
|
// 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
|
|
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
|
|
envelope: [15, 12, 8, 4, 1]
|
|
}
|
|
|
|
// ── User-declared music tracks ──
|
|
//
|
|
// 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 — 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: [
|
|
C4, E4, G4, C5, G4, E4
|
|
]
|
|
}
|
|
|
|
// ── Game state ──
|
|
|
|
var px: u8 = 128
|
|
var py: u8 = 120
|
|
var timer: u8 = 0
|
|
var music_on: bool = false
|
|
|
|
on frame {
|
|
// Move the smiley so you can see the game is running.
|
|
if button.right { px += 1 }
|
|
if button.left { px -= 1 }
|
|
if button.down { py += 1 }
|
|
if button.up { py -= 1 }
|
|
|
|
// Input-triggered SFX.
|
|
if button.a { play LongCoin }
|
|
if button.b { play Zap }
|
|
|
|
// Start/stop the user-declared theme.
|
|
if button.start { start_music Theme }
|
|
if button.select { stop_music }
|
|
|
|
// Auto-play a builtin coin SFX every 60 frames so the e2e
|
|
// harness (which runs headless without simulated input) can
|
|
// capture a non-silent audio hash. Also toggles the music
|
|
// every 120 frames to exercise start/stop paths.
|
|
timer += 1
|
|
if timer == 30 {
|
|
play coin
|
|
}
|
|
if timer == 120 {
|
|
timer = 0
|
|
if music_on {
|
|
stop_music
|
|
music_on = false
|
|
} else {
|
|
start_music Theme
|
|
music_on = true
|
|
}
|
|
}
|
|
|
|
draw Smiley at: (px, py)
|
|
}
|
|
|
|
start Main
|