1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/examples/sprite_0_split_demo.ne
Claude e0b268eea9
compiler: GNROM / debug port / sprite flicker / fade / sprite-0 split + docs
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.
2026-04-18 19:31:55 +00:00

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