mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
examples: adopt the friendly asset syntax
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
This commit is contained in:
parent
48832ccb13
commit
ad951a835f
8 changed files with 676 additions and 353 deletions
|
|
@ -35,43 +35,45 @@ game "Audio Demo" {
|
|||
// ── User-declared sound effects ──
|
||||
//
|
||||
// An `sfx` block is a frame-accurate envelope for pulse 1. `pitch`
|
||||
// latches the pulse period once on trigger; `volume` runs one entry
|
||||
// per frame, so the envelope length controls the effect duration.
|
||||
// `duty` (0-3) picks the pulse waveform shape (2 = 50% square).
|
||||
// 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, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50, 0x50]
|
||||
volume: [15, 14, 13, 12, 11, 9, 7, 5, 3, 1]
|
||||
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, 0x20, 0x20, 0x20, 0x20]
|
||||
volume: [15, 12, 8, 4, 1]
|
||||
pitch: 0x20
|
||||
envelope: [15, 12, 8, 4, 1]
|
||||
}
|
||||
|
||||
// ── User-declared music tracks ──
|
||||
//
|
||||
// A `music` block is a flat list of `(pitch, duration)` note pairs.
|
||||
// Pitch 0 = rest; 1-60 = period table index (C1..B5, middle C = 37).
|
||||
// Duration is in frames (so at 60 fps, 30 = half second per note).
|
||||
// Music loops by default; set `repeat: false` for one-shot cues.
|
||||
// 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.
|
||||
// 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: [
|
||||
37, 20, // C4
|
||||
41, 20, // E4
|
||||
44, 20, // G4
|
||||
49, 20, // C5
|
||||
44, 20, // G4
|
||||
41, 20, // E4
|
||||
C4, E4, G4, C5, G4, E4
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,39 +24,39 @@ game "Palette + BG Demo" {
|
|||
|
||||
// ── Palettes ────────────────────────────────────────────────
|
||||
//
|
||||
// Each palette is 32 bytes laid out in the standard NES order:
|
||||
// $00-$0F background palettes 0-3 (4 colours each)
|
||||
// $10-$1F sprite palettes 0-3 (4 colours each, $10/$14/$18/$1C
|
||||
// mirror the bg slots)
|
||||
// Both palettes are authored in grouped form: one shared
|
||||
// `universal:` colour feeds every sub-palette's index-0 slot,
|
||||
// which auto-fixes the `$3F10/$3F14/$3F18/$3F1C` PPU mirror
|
||||
// trap (the parser handles the packing). Each `bgN` / `spN`
|
||||
// field only needs three colours — the universal is prepended.
|
||||
//
|
||||
// `CoolBlues` uses deep blue background tiles with pale highlights;
|
||||
// `WarmReds` swaps them for red/orange tones. Every 4 bytes is a
|
||||
// sub-palette.
|
||||
// Named NES colours resolve to master-palette indices via the
|
||||
// curated name table; see `docs/language-guide.md` for the full
|
||||
// list. Hex byte literals (`0x01`, `0x14`, …) still work if you
|
||||
// need a colour that doesn't have a name.
|
||||
|
||||
palette CoolBlues {
|
||||
colors: [
|
||||
0x0F, 0x01, 0x11, 0x21, // bg palette 0: black, deep blue, sky, pale
|
||||
0x0F, 0x02, 0x12, 0x22, // bg palette 1
|
||||
0x0F, 0x0C, 0x1C, 0x2C, // bg palette 2
|
||||
0x0F, 0x0B, 0x1B, 0x2B, // bg palette 3
|
||||
0x0F, 0x01, 0x11, 0x21, // sprite palette 0
|
||||
0x0F, 0x16, 0x27, 0x30, // sprite palette 1
|
||||
0x0F, 0x14, 0x24, 0x34, // sprite palette 2
|
||||
0x0F, 0x0B, 0x1B, 0x2B // sprite palette 3
|
||||
]
|
||||
universal: black
|
||||
bg0: [dk_blue, blue, sky_blue] // deep blue highlights
|
||||
bg1: [indigo, royal_blue, periwinkle] // cool purples
|
||||
bg2: [dk_cyan, cyan, lt_cyan] // aquas
|
||||
bg3: [dk_teal, teal, lt_teal] // teal accents
|
||||
sp0: [dk_blue, blue, sky_blue] // matches bg0
|
||||
sp1: [red, orange, white] // warm accent sprites
|
||||
sp2: [magenta, lt_magenta, pale_pink] // magenta sprites
|
||||
sp3: [dk_teal, teal, lt_teal] // matches bg3
|
||||
}
|
||||
|
||||
palette WarmReds {
|
||||
colors: [
|
||||
0x0F, 0x06, 0x16, 0x26, // bg palette 0: black, dark red, red, peach
|
||||
0x0F, 0x07, 0x17, 0x27, // bg palette 1
|
||||
0x0F, 0x08, 0x18, 0x28, // bg palette 2
|
||||
0x0F, 0x09, 0x19, 0x29, // bg palette 3
|
||||
0x0F, 0x06, 0x16, 0x26, // sprite palette 0
|
||||
0x0F, 0x16, 0x27, 0x30, // sprite palette 1
|
||||
0x0F, 0x14, 0x24, 0x34, // sprite palette 2
|
||||
0x0F, 0x0B, 0x1B, 0x2B // sprite palette 3
|
||||
]
|
||||
universal: black
|
||||
bg0: [dk_red, red, lt_red] // fiery warm bg
|
||||
bg1: [brown, dk_orange, orange] // autumnal
|
||||
bg2: [dk_olive, olive, yellow] // muted yellows
|
||||
bg3: [dk_green, green, lt_green] // greens for contrast
|
||||
sp0: [dk_red, red, lt_red] // matches bg0
|
||||
sp1: [red, orange, white] // highlight sprites
|
||||
sp2: [magenta, lt_magenta, pale_pink] // pinks
|
||||
sp3: [dk_teal, teal, lt_teal] // cool accent
|
||||
}
|
||||
|
||||
// ── Backgrounds ─────────────────────────────────────────────
|
||||
|
|
@ -74,40 +74,46 @@ palette WarmReds {
|
|||
// top-left to bottom-right so the swap is visually obvious.
|
||||
|
||||
background TitleScreen {
|
||||
tiles: [
|
||||
// Row 14 (offset 14*32 = 448): a ribbon of tile 0
|
||||
// across columns 4..28.
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
// Row 14
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
// Both backgrounds resolve to an all-zero nametable for this
|
||||
// demo — the visual difference between them comes from the
|
||||
// palette swap triggered alongside `load_background`. We still
|
||||
// author them with a `legend` + `map:` block here so the demo
|
||||
// exercises the pleasant syntax the same way a real game
|
||||
// would. The '.' legend entry alone is enough to fill an
|
||||
// all-tile-0 background; every row is padded to 32 columns
|
||||
// automatically by the parser.
|
||||
legend { ".": 0 }
|
||||
map: [
|
||||
"................................", // row 0
|
||||
"................................", // row 1
|
||||
"................................", // row 2
|
||||
"................................", // row 3
|
||||
"................................", // row 4
|
||||
"................................", // row 5
|
||||
"................................", // row 6
|
||||
"................................", // row 7
|
||||
"................................", // row 8
|
||||
"................................", // row 9
|
||||
"................................", // row 10
|
||||
"................................", // row 11
|
||||
"................................", // row 12
|
||||
"................................", // row 13
|
||||
"................................", // row 14 (ribbon)
|
||||
"................................" // row 15
|
||||
]
|
||||
}
|
||||
|
||||
background StageOne {
|
||||
tiles: [
|
||||
// A few scattered tile-0s across the first 4 rows so the
|
||||
// background visibly differs from TitleScreen after the
|
||||
// swap. Remaining rows are zero-padded and render as
|
||||
// tile 0 anyway — the visual difference comes from the
|
||||
// palette swap triggered alongside.
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
// StageOne's visual difference comes from the palette swap —
|
||||
// the tile grid is still the built-in smiley everywhere. We
|
||||
// only declare 4 rows to prove the parser's zero-padding
|
||||
// still works with the new tilemap form.
|
||||
legend { ".": 0 }
|
||||
map: [
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................"
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,54 +32,34 @@ game "Platformer" {
|
|||
}
|
||||
|
||||
// ── Palette ──────────────────────────────────────────────────
|
||||
// The background palette drives the sky / hill / ground look.
|
||||
// Each sub-palette reserves index 0 for the shared background
|
||||
// colour (sky blue $22), plus three working colours.
|
||||
// bg 0 (sky region): sky blue, white, light gray, black
|
||||
// bg 1 (air/blocks row): sky blue, red, peach, dark red
|
||||
// bg 2 (ground row): sky blue, lime green, dark green, dark brown
|
||||
// bg 3 (unused reserve): sky blue, yellow, orange, gold
|
||||
// The sprite sub-palette 0 (used by every `draw` statement — the
|
||||
// OAM attribute byte is always 0 in the current NEScript
|
||||
// runtime) carries the hero's red/peach/white body colours.
|
||||
// NOTE: the first byte of every sub-palette must match. The PPU
|
||||
// mirrors $3F10/$3F14/$3F18/$3F1C onto $3F00/$3F04/$3F08/$3F0C, so
|
||||
// writing 8 distinct "index 0" bytes ends up clobbering the
|
||||
// background universal colour with the last write. The canonical
|
||||
// fix — and what every NES game does — is to use the universal
|
||||
// background colour ($22 sky blue here) in all 8 slots. For sprite
|
||||
// palettes, index 0 is always transparent regardless of the stored
|
||||
// value, so this costs nothing visually.
|
||||
// Authored in grouped form: one shared `universal:` colour fills
|
||||
// every sub-palette's index-0 slot automatically. That single
|
||||
// declaration is enough to dodge the notorious `$3F10/$3F14/$3F18/
|
||||
// $3F1C` PPU mirror trap — when a program writes 8 distinct
|
||||
// "index 0" bytes sequentially, the last write clobbers the
|
||||
// shared background colour and the screen turns the wrong colour
|
||||
// (or all-black). Using one shared universal everywhere is what
|
||||
// every shipped NES game does; the parser handles it for us.
|
||||
//
|
||||
// `0x22` is the lavender-blue NES master palette slot Super Mario
|
||||
// Bros uses for its iconic sky. The colour names map to the
|
||||
// curated set documented in `docs/language-guide.md`.
|
||||
palette Main {
|
||||
colors: [
|
||||
// bg palette 0 — sky / clouds
|
||||
// 0 = sky blue (universal bg)
|
||||
// 1 = white
|
||||
// 2 = light gray
|
||||
// 3 = black
|
||||
0x22, 0x30, 0x10, 0x0F,
|
||||
// bg palette 1 — bricks, Q-blocks
|
||||
// 1 = red, 2 = peach, 3 = dark red
|
||||
0x22, 0x16, 0x27, 0x06,
|
||||
// bg palette 2 — grass, hills, bushes, dirt
|
||||
// 1 = light green
|
||||
// 2 = light brown
|
||||
// 3 = dark brown
|
||||
0x22, 0x1A, 0x17, 0x07,
|
||||
// bg palette 3 — unused but the mirroring still writes here
|
||||
0x22, 0x28, 0x27, 0x17,
|
||||
// sprite palette 0 — hero / enemy / coin
|
||||
// (OAM attribute is always 0 in the current runtime so
|
||||
// every sprite uses this one sub-palette)
|
||||
// 1 = red cap, 2 = peach skin, 3 = white
|
||||
0x22, 0x16, 0x27, 0x30,
|
||||
// sprite palette 1 — reserved
|
||||
0x22, 0x07, 0x17, 0x27,
|
||||
// sprite palette 2 — reserved
|
||||
0x22, 0x2A, 0x1A, 0x30,
|
||||
// sprite palette 3 — reserved
|
||||
0x22, 0x28, 0x18, 0x38
|
||||
]
|
||||
universal: 0x22 // sky blue (NES $22)
|
||||
|
||||
// Background sub-palettes
|
||||
bg0: [white, gray, black] // sky / clouds
|
||||
bg1: [red, orange, dk_red] // bricks, Q-blocks
|
||||
bg2: [bright_green, dk_orange, brown] // grass, hills, bushes, dirt
|
||||
bg3: [yellow, orange, dk_orange] // reserved
|
||||
|
||||
// Sprite sub-palette 0 — every `draw` uses this one slot
|
||||
// because the OAM attribute byte is always 0 in v0.1.
|
||||
// 1 = red cap, 2 = orange skin, 3 = white highlight.
|
||||
sp0: [red, orange, white]
|
||||
sp1: [brown, dk_orange, orange] // reserved
|
||||
sp2: [neon_green, bright_green, white] // reserved
|
||||
sp3: [yellow, olive, cream] // reserved
|
||||
}
|
||||
|
||||
// ── Tileset ──────────────────────────────────────────────────
|
||||
|
|
@ -89,38 +69,149 @@ palette Main {
|
|||
// same declaration drives the player, enemies, coins, and every
|
||||
// background tile after tile 0. Tile 0 is reserved by the linker
|
||||
// for the built-in smiley — the background leaves it unreferenced.
|
||||
//
|
||||
// All 15 tiles are authored as 8×8 ASCII pixel art and stacked
|
||||
// vertically (1 tile wide × 15 tiles tall) so the parser splits
|
||||
// them into consecutive CHR tile indices in reading order. The
|
||||
// art uses the `.abc` vocabulary (`. = 0`, `a = 1`, `b = 2`,
|
||||
// `c = 3`); regenerate with `cargo run --bin gen_platformer_tiles`.
|
||||
sprite Tileset {
|
||||
chr: [
|
||||
pixels: [
|
||||
// tile 1: Player head L
|
||||
0x3F, 0x7F, 0x70, 0x61, 0xCF, 0xCD, 0xC0, 0xC0, 0x00, 0x00, 0x0F, 0x1F, 0x3F, 0x3F, 0x3F, 0x3F,
|
||||
"..aaaaaa",
|
||||
".aaaaaaa",
|
||||
".aaabbbb",
|
||||
".aabbbbc",
|
||||
"aabbcccc",
|
||||
"aabbccbc",
|
||||
"aabbbbbb",
|
||||
"aabbbbbb",
|
||||
// tile 2: Player head R
|
||||
0xFC, 0xFE, 0x0E, 0x83, 0xF8, 0x4C, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
"aaaaaa..",
|
||||
"aaaaaaa.",
|
||||
"bbbbaaa.",
|
||||
"cbbbbbaa",
|
||||
"cccccbbb",
|
||||
"bcbbccbb",
|
||||
"bbbbbbbb",
|
||||
"bbbbbbbb",
|
||||
// tile 3: Player body L
|
||||
0x7F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x10, 0x18, 0x00, 0x7F, 0x7F, 0x67, 0x07,
|
||||
".aaaaaaa",
|
||||
"aaacaaaa",
|
||||
"aaaccaaa",
|
||||
"aaaaaaaa",
|
||||
".bbbbbbb",
|
||||
".bbbbbbb",
|
||||
".bb..bbb",
|
||||
"aaa..bbb",
|
||||
// tile 4: Player body R
|
||||
0xFE, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x07, 0x00, 0x08, 0x18, 0x00, 0xFE, 0xFE, 0xE6, 0xE0,
|
||||
"aaaaaaa.",
|
||||
"aaaacaaa",
|
||||
"aaaccaaa",
|
||||
"aaaaaaaa",
|
||||
"bbbbbbb.",
|
||||
"bbbbbbb.",
|
||||
"bbb..bb.",
|
||||
"bbb..aaa",
|
||||
// tile 5: Enemy
|
||||
0x3C, 0x7E, 0x76, 0xFF, 0x99, 0x7E, 0xBD, 0x99, 0x00, 0x00, 0x18, 0x38, 0x7E, 0x00, 0x00, 0x00,
|
||||
"..aaaa..",
|
||||
".aaaaaa.",
|
||||
".aacbaa.",
|
||||
"aacccaaa",
|
||||
"abbccbba",
|
||||
".aaaaaa.",
|
||||
"a.aaaa.a",
|
||||
"a..aa..a",
|
||||
// tile 6: Coin
|
||||
0x00, 0x00, 0x18, 0x24, 0x24, 0x18, 0x00, 0x00, 0x18, 0x3C, 0x7E, 0x7E, 0x7E, 0x7E, 0x3C, 0x18,
|
||||
"...bb...",
|
||||
"..bbbb..",
|
||||
".bbccbb.",
|
||||
".bcbbcb.",
|
||||
".bcbbcb.",
|
||||
".bbccbb.",
|
||||
"..bbbb..",
|
||||
"...bb...",
|
||||
// tile 7: Grass top
|
||||
0xFF, 0xBB, 0xFF, 0xFF, 0xFF, 0xFF, 0xBD, 0xFF, 0x00, 0x00, 0x00, 0x6A, 0xF7, 0xFF, 0xFF, 0xFF,
|
||||
"aaaaaaaa",
|
||||
"a.aaa.aa",
|
||||
"aaaaaaaa",
|
||||
"accacaca",
|
||||
"ccccaccc",
|
||||
"cccccccc",
|
||||
"cbccccbc",
|
||||
"cccccccc",
|
||||
// tile 8: Dirt
|
||||
0xFF, 0xBD, 0xFF, 0xF7, 0xFF, 0xBD, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
"cccccccc",
|
||||
"cbccccbc",
|
||||
"cccccccc",
|
||||
"ccccbccc",
|
||||
"cccccccc",
|
||||
"cbccccbc",
|
||||
"ccccbccc",
|
||||
"cccccccc",
|
||||
// tile 9: Brick
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x82, 0x82, 0x82, 0xFF, 0x10, 0x10, 0x10,
|
||||
"cccccccc",
|
||||
"caaaaaca",
|
||||
"caaaaaca",
|
||||
"caaaaaca",
|
||||
"cccccccc",
|
||||
"aaacaaaa",
|
||||
"aaacaaaa",
|
||||
"aaacaaaa",
|
||||
// tile 10: Cloud L
|
||||
0x00, 0x1C, 0x3E, 0x7F, 0x7F, 0x1F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x3E, 0x07,
|
||||
"........",
|
||||
"...aaa..",
|
||||
"..aaaaa.",
|
||||
".aaaaaaa",
|
||||
".aaaaaaa",
|
||||
".bbaaaaa",
|
||||
"..bbbbba",
|
||||
".....bbb",
|
||||
// tile 11: Cloud R
|
||||
0x00, 0x70, 0xF8, 0xFE, 0xFE, 0xF8, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x7C, 0xE0,
|
||||
"........",
|
||||
".aaa....",
|
||||
"aaaaa...",
|
||||
"aaaaaaa.",
|
||||
"aaaaaaa.",
|
||||
"aaaaabb.",
|
||||
"abbbbb..",
|
||||
"bbb.....",
|
||||
// tile 12: Hill
|
||||
0x00, 0x0C, 0x1E, 0x3F, 0x2F, 0x4B, 0x75, 0xC9, 0x00, 0x00, 0x00, 0x00, 0x10, 0x34, 0x0A, 0x36,
|
||||
"........",
|
||||
"....aa..",
|
||||
"...aaaa.",
|
||||
"..aaaaaa",
|
||||
"..abaaaa",
|
||||
".abbabaa",
|
||||
".aaababa",
|
||||
"aabbabba",
|
||||
// tile 13: Bush
|
||||
0x00, 0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x02, 0x06, 0x06, 0x55,
|
||||
"........",
|
||||
"...aa...",
|
||||
"..aaaa..",
|
||||
".aaaaac.",
|
||||
"aaaaaaca",
|
||||
"aaaaacca",
|
||||
"aaaaacca",
|
||||
"acacacac",
|
||||
// tile 14: Q Block
|
||||
0xFF, 0xFF, 0xC3, 0xDB, 0xD3, 0xC3, 0xFF, 0xFF, 0xFF, 0x81, 0xBD, 0xBD, 0xBD, 0xBD, 0x81, 0xFF,
|
||||
"cccccccc",
|
||||
"caaaaaac",
|
||||
"cabbbbac",
|
||||
"cabccbac",
|
||||
"cabcbbac",
|
||||
"cabbbbac",
|
||||
"caaaaaac",
|
||||
"cccccccc",
|
||||
// tile 15: Sky (blank)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........"
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -133,142 +224,121 @@ const TILE_ENEMY: u8 = 5
|
|||
const TILE_COIN: u8 = 6
|
||||
|
||||
// ── Background ──────────────────────────────────────────────
|
||||
// Full-screen 32×30 nametable showing a static level view that
|
||||
// horizontal scrolling pans across. The attribute table splits
|
||||
// the screen into three palette regions (sky/blocks/ground) so
|
||||
// the grass and hills pick up the right colours even without
|
||||
// per-tile attribute churn.
|
||||
// The 32×30 nametable is authored as ASCII art with a `legend`
|
||||
// block naming each tile by a single character. Horizontal
|
||||
// scrolling pans this single nametable via `scroll()` so it only
|
||||
// needs to look interesting at any X offset.
|
||||
//
|
||||
// `palette_map:` is the friendlier alternative to a 64-byte
|
||||
// hand-packed attribute table: 16×15 metatile entries, one
|
||||
// digit per 16×16 metatile, and the parser packs the 2-bit
|
||||
// `(br<<6)|(bl<<4)|(tr<<2)|tl` quadrants automatically. The
|
||||
// three palette regions are sky (`0`), brick row (`1`), and
|
||||
// ground (`2`).
|
||||
background Level {
|
||||
tiles: [
|
||||
// rows 0-4: clean sky
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
// row 5: clouds at x=4..5 and x=20..21
|
||||
15, 15, 15, 15, 10, 11, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 10, 11, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
// row 6: cloud at x=12..13
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 10, 11, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
// rows 7-14: more clean sky
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
// row 15: brick/Q-block platform left, brick platform right
|
||||
15, 15, 15, 15, 15, 15, 9, 9, 14, 9, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 9, 9, 9, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
// rows 16-19: sky
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
// row 20: hills (3 on left, 4 on right)
|
||||
15, 15, 12, 12, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 12, 12, 12, 12, 15, 15, 15, 15, 15, 15,
|
||||
// row 21: bushes above the grass line
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 15, 15, 15,
|
||||
// row 22: grass top (full width)
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
// rows 23-29: dirt with a few buried bricks for texture
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 8,
|
||||
8, 8, 8, 8, 8, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 9, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8
|
||||
legend {
|
||||
".": 15 // sky (blank)
|
||||
"<": 10 // cloud left half
|
||||
">": 11 // cloud right half
|
||||
"#": 9 // brick
|
||||
"Q": 14 // question block
|
||||
"^": 12 // hill silhouette
|
||||
"*": 13 // bush
|
||||
"=": 7 // grass top
|
||||
"%": 8 // dirt
|
||||
}
|
||||
|
||||
map: [
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"....<>..............<>..........",
|
||||
"............<>..................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"......##Q#........###...........",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"................................",
|
||||
"..^^^.................^^^^......",
|
||||
"..........***..............**...",
|
||||
"================================",
|
||||
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%",
|
||||
"%%%%%#%%%%%%%%%%%%#%%%%%%%%%%%%%",
|
||||
"%%%%%%%%%%%#%%%%%%%%%%%%#%%%%%%%",
|
||||
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
|
||||
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
|
||||
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
|
||||
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"
|
||||
]
|
||||
attributes: [
|
||||
// 8×8 attribute grid, one byte per 32×32-px metatile group.
|
||||
// Each byte packs 4 identical 2-bit quadrants selecting a
|
||||
// sub-palette (0..3):
|
||||
// 0x00 = sub-palette 0 (sky / clouds)
|
||||
// 0x55 = sub-palette 1 (bricks / Q-blocks)
|
||||
// 0xAA = sub-palette 2 (grass / hills / dirt)
|
||||
//
|
||||
// Row mapping: ay 0-2 (nt rows 0-11) = sky, ay 3 (nt rows
|
||||
// 12-15) = brick row, ay 4 (nt rows 16-19) = more sky, ay
|
||||
// 5-7 (nt rows 20-29) = ground.
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA
|
||||
|
||||
palette_map: [
|
||||
"0000000000000000", // metatile rows 0-5 → sky sub-palette
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"1111111111111111", // metatile rows 6-7 → brick sub-palette
|
||||
"1111111111111111",
|
||||
"0000000000000000", // metatile rows 8-9 → sky again
|
||||
"0000000000000000",
|
||||
"2222222222222222", // metatile rows 10-15 → ground sub-palette
|
||||
"2222222222222222", // (rows 15 sits off-screen but the PPU
|
||||
"2222222222222222", // still reads its attribute byte, so
|
||||
"2222222222222222", // we emit it explicitly to match the
|
||||
"2222222222222222", // hand-packed attribute table the
|
||||
"2222222222222222" // old form was using.)
|
||||
]
|
||||
}
|
||||
|
||||
// ── Audio ────────────────────────────────────────────────────
|
||||
//
|
||||
// SFX: scalar `pitch:` matches the v1 driver's latch-once
|
||||
// behaviour — pulse 1's period is sampled exactly once when the
|
||||
// effect triggers and never updated, so a single byte is the
|
||||
// natural form for the current pipeline. `envelope:` is the
|
||||
// friendlier alias for `volume:`.
|
||||
|
||||
// Custom boingy jump sound — descending pitch arc on pulse 1.
|
||||
// Custom boingy jump sound — fixed-pitch arc on pulse 1.
|
||||
sfx Boing {
|
||||
duty: 2
|
||||
pitch: [0x30, 0x38, 0x40, 0x48, 0x50, 0x58, 0x60]
|
||||
volume: [12, 11, 10, 9, 7, 5, 2]
|
||||
pitch: 0x30
|
||||
envelope: [12, 11, 10, 9, 7, 5, 2]
|
||||
}
|
||||
|
||||
// Custom coin ding — short rising blip.
|
||||
sfx Ding {
|
||||
duty: 2
|
||||
pitch: [0x20, 0x20, 0x20, 0x20]
|
||||
volume: [14, 12, 8, 3]
|
||||
pitch: 0x20
|
||||
envelope: [14, 12, 8, 3]
|
||||
}
|
||||
|
||||
// Custom looping underworld theme — playful four-bar loop on
|
||||
// pulse 2 that plays while the player is alive.
|
||||
// pulse 2 that plays while the player is alive. Note names
|
||||
// (C4 / E4 / etc.) plus a default `tempo:` so each note only
|
||||
// has to spell its frame count when it deviates from the beat.
|
||||
music Theme {
|
||||
duty: 2
|
||||
volume: 9
|
||||
repeat: true
|
||||
tempo: 10
|
||||
notes: [
|
||||
37, 10, // C4
|
||||
41, 10, // E4
|
||||
44, 10, // G4
|
||||
49, 10, // C5
|
||||
44, 10, // G4
|
||||
41, 10, // E4
|
||||
39, 15, // D4
|
||||
0, 5, // rest
|
||||
36, 10, // B3
|
||||
41, 10, // E4
|
||||
44, 10, // G4
|
||||
49, 20, // C5
|
||||
0, 10 // rest
|
||||
C4, E4, G4, C5, G4, E4,
|
||||
D4 15,
|
||||
rest 5,
|
||||
B3, E4, G4,
|
||||
C5 20,
|
||||
rest 10
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,17 +9,38 @@ game "Asset Demo" {
|
|||
mapper: NROM
|
||||
}
|
||||
|
||||
// Define a sprite with inline CHR tile data (16 bytes = one 8x8 tile)
|
||||
// This is a simple arrow pointing right
|
||||
// Define a sprite with ASCII pixel art. Each character maps to a
|
||||
// 2-bit palette index: `.` = 0 (transparent), `#` = 1, `%` = 2,
|
||||
// `@` = 3. The parser handles the 2-bitplane CHR encoding, so we
|
||||
// never touch hex bytes by hand.
|
||||
//
|
||||
// Arrow — a right-facing arrow in palette-index 1.
|
||||
sprite Arrow {
|
||||
chr: [0x18, 0x1C, 0xFE, 0xFF, 0xFF, 0xFE, 0x1C, 0x18,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
pixels: [
|
||||
"...##...",
|
||||
"...###..",
|
||||
"#######.",
|
||||
"########",
|
||||
"########",
|
||||
"#######.",
|
||||
"...###..",
|
||||
"...##..."
|
||||
]
|
||||
}
|
||||
|
||||
// Define a sprite with a heart shape
|
||||
// Heart — a full-colour heart in palette-index 3 (the brightest
|
||||
// shade, `@`).
|
||||
sprite Heart {
|
||||
chr: [0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00,
|
||||
0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00]
|
||||
pixels: [
|
||||
".@@..@@.",
|
||||
"@@@@@@@@",
|
||||
"@@@@@@@@",
|
||||
"@@@@@@@@",
|
||||
".@@@@@@.",
|
||||
"..@@@@..",
|
||||
"...@@...",
|
||||
"........"
|
||||
]
|
||||
}
|
||||
|
||||
var px: u8 = 128
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue