mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
Another batch from the cc65/nesdoug gap catalogue. All six items
gated on marker labels (or default-false attributes) so existing
programs produce byte-identical ROMs — every pre-existing .nes
file round-trips unchanged.
**Language / runtime additions:**
- `mapper: GNROM` (iNES 66). Combines AxROM's 32 KB PRG pages with
CNROM's 8 KB CHR banks in a single `$8000` register. Linker
pads single-page ROMs to 32 KB to match mapper-66 expectations.
- `game { debug_port: fceux | mesen | 0xXXXX }`. `debug.log`,
`debug.assert`, and the `__debug_halt` sentinel now target a
user-selected address. `fceux` (default, $4800) and `mesen`
($4018) are named aliases; custom hex addresses are accepted
for unusual debuggers.
- `game { sprite_flicker: true }`. IR lowerer injects an
`IrOp::CycleSprites` at the top of every `on frame` handler,
which flips on the rotating-OAM NMI variant with no per-site
boilerplate. Default false so existing ROMs keep their layout.
- `fade_out(step_frames)` / `fade_in(step_frames)` builtins.
Blocking helpers that walk brightness 4 → 0 or 0 → 4 with
`step_frames` frames between each step. Runtime splices
`__fade_out`, `__fade_in`, and a callable `__wait_frame_rt`
helper when the builtin is used. Zero-guard on step_frames
prevents a pathological 256-frame spin when the caller
accidentally passes 0.
- `sprite_0_split(scroll_x, scroll_y)` intrinsic. Emits a
two-phase busy-wait on `$2002` bit 6 (wait-for-clear,
wait-for-set) then writes the new scroll values to `$2005`.
Works on any mapper — unlike `on_scanline(N)` which requires
MMC3. Enables HUD-over-playfield scrolling on NROM/UxROM/MMC1.
**Docs:**
- New paragraph in the language guide explaining the no-recursion
design choice and the explicit-stack workaround pattern.
- `future-work.md` updated to mark the shipped items out of the
catalogue; remaining items reshuffled in the priority ranking.
- README + examples/README updated with the new mapper and
builtins.
**Tests:**
- 12 new integration tests covering: GNROM header emission,
debug-port targeting (fceux/mesen/custom), unknown-alias
rejection, sprite_flicker on/off/bad-value, fade_out JSR + marker
coupling, fade omitted-when-unused, fade-in-expression rejected,
sprite_0_split byte-level busy-wait verification, sprite_0_split
arity enforcement, sprite_0_split omitted-when-unused, and an
extended void-intrinsic-in-expression-position test covering the
three new void builtins.
- `nes2_mapper_high_nibble_in_byte_8_is_zero_for_small_mappers`
extended to include GNROM.
- Four new examples with committed .nes ROMs + pixel/audio
goldens: `gnrom_simple`, `auto_sprite_flicker`, `fade_demo`,
`sprite_0_split_demo`.
All 752 tests pass. Clippy clean. 44/44 emulator goldens match.
73 lines
2.5 KiB
Text
73 lines
2.5 KiB
Text
// Sprite-0 split demo — `sprite_0_split(x, y)` busy-waits for the
|
|
// PPU's sprite-0 hit flag (`$2002` bit 6) and then writes the
|
|
// requested scroll values to `$2005`. This produces a mid-frame
|
|
// scroll change without needing MMC3's scanline IRQ, so the split
|
|
// works on NROM / UxROM / MMC1 — any mapper.
|
|
//
|
|
// For sprite-0 hit to actually fire we need:
|
|
// 1. A sprite in OAM slot 0 (the first `draw` in on_frame)
|
|
// 2. An opaque sprite pixel overlapping an opaque background
|
|
// pixel at some visible scanline
|
|
// 3. Rendering enabled (automatic when we have a bg + palette)
|
|
//
|
|
// This demo draws the smiley as sprite 0 at (16, 24) so its bottom
|
|
// row falls on scanline 31, where it overlaps the background's
|
|
// smiley tiles. The split then resets the scroll so the second
|
|
// half of the frame scrolls independently from the first.
|
|
|
|
game "Sprite 0 Split Demo" {
|
|
mapper: NROM
|
|
mirroring: horizontal
|
|
}
|
|
|
|
palette Colors {
|
|
universal: black
|
|
bg0: [dk_blue, blue, sky_blue]
|
|
bg1: [dk_red, red, lt_red]
|
|
bg2: [dk_green, green, lt_green]
|
|
bg3: [black, lt_gray, white]
|
|
sp0: [dk_blue, blue, sky_blue]
|
|
sp1: [red, orange, white]
|
|
sp2: [dk_teal, teal, lt_teal]
|
|
sp3: [dk_olive, olive, yellow]
|
|
}
|
|
|
|
background Tiled {
|
|
// Every cell is tile 0 (the built-in smiley) so sprite 0's
|
|
// opaque pixels overlap opaque background pixels at any
|
|
// position on the screen — guaranteeing sprite-0 hit fires
|
|
// on every frame regardless of sprite position.
|
|
legend { ".": 0 }
|
|
map: [
|
|
"................................",
|
|
"................................",
|
|
"................................",
|
|
"................................"
|
|
]
|
|
}
|
|
|
|
var top_scroll: u8 = 0
|
|
|
|
on frame {
|
|
// Top half scrolls to the left each frame (wraps at 256).
|
|
top_scroll += 1
|
|
|
|
// Sprite 0 at row 24 → its bottom edge is at scanline 31,
|
|
// which is inside the top-half scroll region. The hit fires
|
|
// around scanline 31 of the current frame.
|
|
draw Smiley at: (16, 24)
|
|
|
|
// After sprite-0 fires we reset scroll_x to 0 and scroll_y
|
|
// to 0, so the bottom half of the screen stays put while
|
|
// the top half drifts. The effect is a "scrolling status
|
|
// bar" pattern — classic technique for HUD-over-playfield.
|
|
sprite_0_split(0, 0)
|
|
|
|
// Set the top-half scroll AFTER sprite_0_split so the NEXT
|
|
// frame's top half gets the drifting value. Writes to $2005
|
|
// between NMI and sprite 0 affect the whole frame up to the
|
|
// split point.
|
|
scroll(top_scroll, 0)
|
|
}
|
|
|
|
start Main
|