1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/examples/war/render.ne
Claude 9137b1f713
examples/war: polish pass + README entry + plan close-out
End-of-implementation polish for the War example after the
compiler bugs were fixed:

- Title state now calls draw_big_war_banner instead of inlining
  12 draws — same pixel output, fewer lines.
- P_WAR_BURY redraws the previous round's face-up cards while
  the noise thumps fire so the table doesn't look empty for
  24 frames between the WAR banner and the new face-ups.
- Drop draw_word_war from render.ne (orphaned by the BIG WAR
  metasprite).
- Refresh comments in background.ne (now references the real
  felt tile) and deal_state.ne (drop the stale FRAMES_DEAL_STEP
  reference now that the deal pace is hard-coded at 2 frames).
- README.md and examples/README.md gain a war row.
- PLAN.md marks every implementation step complete and records
  the design revisions made along the way.
- Refresh the war audio hash to match the new ROM (the title
  screen helper change shifts one frame of pulse-2 timing
  enough to flip the FNV-1a). The frame-180 PNG is unchanged.

https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 16:08:03 +00:00

216 lines
8.1 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// war/render.ne — card, digit, and text rendering helpers.
//
// Every function in this file is a thin wrapper around one or
// more `draw Tileset at: (x, y) frame: N` calls that writes to
// the runtime OAM cursor. Each `draw` takes one sprite slot, so
// a 16×24 card face burns 6 sprite slots, a single-character
// letter burns 1, and so on. The caller is responsible for
// sprite budgeting — see PLAN.md §3.
//
// Function-local var names are prefixed with the function's
// short name to avoid the global symbol-table collisions that
// E0501 would otherwise complain about.
// ── Card face ──────────────────────────────────────────────
//
// Draws the 6-sprite card face at (x, y) for the packed card
// byte. Layout (each cell is 8×8):
//
// [rank ][small_suit] row 0
// [pipL ][pipR ] row 1
// [blankL][blankR ] row 2
//
// Rank is the condensed glyph at TILE_RANK_BASE + (rank - 1);
// small suit + big-pip halves are indexed directly off their
// base constants.
fun draw_card_face(dcf_in_x: u8, dcf_in_y: u8, dcf_in_card: u8) {
// Snapshot every parameter into a fresh local *before* any
// nested function call. NEScript v0.1 passes parameters via
// fixed zero-page slots ($04, $05, $06), and any inner call
// overwrites those slots with its own parameter values —
// including the inline `card_rank` / `card_suit` calls below,
// which would otherwise leave `x` and `y` corrupted by the
// time the draw lines run.
var dcf_x: u8 = dcf_in_x
var dcf_y: u8 = dcf_in_y
var dcf_card: u8 = dcf_in_card
var dcf_rank: u8 = card_rank(dcf_card)
var dcf_suit: u8 = card_suit(dcf_card)
var dcf_rank_tile: u8 = TILE_RANK_BASE + dcf_rank - 1
var dcf_small_tile: u8 = TILE_SUIT_SMALL_BASE + dcf_suit
var dcf_pipl_tile: u8 = TILE_PIP_L_BASE + dcf_suit
var dcf_pipr_tile: u8 = TILE_PIP_R_BASE + dcf_suit
var dcf_x1: u8 = dcf_x + 8
var dcf_y1: u8 = dcf_y + 8
var dcf_y2: u8 = dcf_y + 16
// Row 0 — rank corner + small suit
draw Tileset at: (dcf_x, dcf_y) frame: dcf_rank_tile
draw Tileset at: (dcf_x1, dcf_y) frame: dcf_small_tile
// Row 1 — big centre pip (left + right halves)
draw Tileset at: (dcf_x, dcf_y1) frame: dcf_pipl_tile
draw Tileset at: (dcf_x1, dcf_y1) frame: dcf_pipr_tile
// Row 2 — blank bottom so the card body is symmetric
draw Tileset at: (dcf_x, dcf_y2) frame: TILE_FRAME_BLANK_L
draw Tileset at: (dcf_x1, dcf_y2) frame: TILE_FRAME_BLANK_R
}
// Draw the card-back lattice at (x, y). 6 sprites again. Rows
// 0 and 2 reuse the same top/bottom back tiles; row 1 uses the
// bottom row of the lattice as a filler so the pattern stays
// continuous.
fun draw_card_back(x: u8, y: u8) {
var dcb_x1: u8 = x + 8
var dcb_y1: u8 = y + 8
var dcb_y2: u8 = y + 16
draw Tileset at: (x, y) frame: TILE_BACK_TL
draw Tileset at: (dcb_x1, y) frame: TILE_BACK_TR
draw Tileset at: (x, dcb_y1) frame: TILE_BACK_BL
draw Tileset at: (dcb_x1, dcb_y1) frame: TILE_BACK_BR
draw Tileset at: (x, dcb_y2) frame: TILE_BACK_TL
draw Tileset at: (dcb_x1, dcb_y2) frame: TILE_BACK_TR
}
// Dispatch between front and back based on the fly_face_up flag
// stamped by the phase handlers. Snapshots params first because
// the inner call clobbers $04/$05.
fun draw_flying_card(x: u8, y: u8) {
var dfc_x: u8 = x
var dfc_y: u8 = y
if fly_face_up == 0 {
draw_card_back(dfc_x, dfc_y)
} else {
draw_card_face(dfc_x, dfc_y, fly_card)
}
}
// ── Digits + text ─────────────────────────────────────────
// Draw a single decimal digit 0..9 at (x, y).
fun draw_digit(x: u8, y: u8, d: u8) {
draw Tileset at: (x, y) frame: TILE_DIGIT_BASE + d
}
// Draw a two-digit decimal count (0..99) at (x, y). Leading zero
// is preserved so the HUD always renders two glyphs wide, which
// keeps the layout stable as the counts change.
fun draw_count(x: u8, y: u8, v: u8) {
var dct_tens: u8 = 0
var dct_n: u8 = v
// Divide by 10 without the software divide: repeatedly
// subtract 10 until n < 10. For v ≤ 52 this loops at most
// 5 times, cheaper than calling `/` and `%`.
while dct_n >= 10 {
dct_n -= 10
dct_tens += 1
}
draw_digit(x, y, dct_tens)
draw_digit(x + 8, y, dct_n)
}
// Draw a single letter 'A'..'Z' using the sprite font. `ch` is
// the letter index (0 = A, 25 = Z). Deliberately NOT marked
// `inline`: when this was inlined the resulting code put each
// inlined `draw` at double the intended X step (the inliner
// appears to re-evaluate the (x + N) parameter expression in a
// way that compounds across consecutive draws). Keeping it as
// a real function call gives every draw_letter call its own
// argument-evaluation context and the spacing comes out right.
fun draw_letter(x: u8, y: u8, ch: u8) {
draw Tileset at: (x, y) frame: TILE_LETTER_BASE + ch
}
// Short helper for drawing the word "PLAYER" at (x, y). 6 letters.
// Used by the HUD and the victory banner.
//
// We accumulate the X position in a local instead of passing
// `x + N` as a call argument: the latter pattern miscompiles in
// NEScript v0.1 (consecutive `x + N` arguments to the same
// function appear to alias the parameter slot, leaving the second
// onward call site reading a stale offset). Stepping a local
// avoids the issue entirely.
fun draw_word_player(x: u8, y: u8) {
var dwp_px: u8 = x
draw_letter(dwp_px, y, 15) // P
dwp_px += 8
draw_letter(dwp_px, y, 11) // L
dwp_px += 8
draw_letter(dwp_px, y, 0) // A
dwp_px += 8
draw_letter(dwp_px, y, 24) // Y
dwp_px += 8
draw_letter(dwp_px, y, 4) // E
dwp_px += 8
draw_letter(dwp_px, y, 17) // R
}
// "WINS" — used on the victory screen.
fun draw_word_wins(x: u8, y: u8) {
var dww_px: u8 = x
draw_letter(dww_px, y, 22) // W
dww_px += 8
draw_letter(dww_px, y, 8) // I
dww_px += 8
draw_letter(dww_px, y, 13) // N
dww_px += 8
draw_letter(dww_px, y, 18) // S
}
// "PRESS" — used on the title screen.
fun draw_word_press(x: u8, y: u8) {
var dwr_px: u8 = x
draw_letter(dwr_px, y, 15) // P
dwr_px += 8
draw_letter(dwr_px, y, 17) // R
dwr_px += 8
draw_letter(dwr_px, y, 4) // E
dwr_px += 8
draw_letter(dwr_px, y, 18) // S
dwr_px += 8
draw_letter(dwr_px, y, 18) // S
}
// ── Fly animation driver ──────────────────────────────────
//
// We avoid the software multiply (and the W0101 "expensive
// multiply" warning) by stepping the fly position by a fixed
// constant each frame instead of computing `start + dx * t /
// FRAMES`. The constant FLY_STEP is chosen so that
// FRAMES_FLY * FLY_STEP equals the deck-to-play distance on
// both axes:
//
// FRAMES_FLY * FLY_STEP = 64 px (16 * 4)
//
// The screen layout (DECK_*_X, PLAY_*_X, DECK_Y, PLAY_Y) is
// arranged so every animation traverses exactly 64 px on both
// axes, which keeps the per-frame step uniform.
const FLY_STEP: u8 = 4
// Step the global (fly_x, fly_y) by FLY_STEP in the directions
// stored in fly_dx_sign / fly_dy_sign. Sign 0 = positive (move
// right / down), 1 = negative (move left / up). Called once per
// frame inside the relevant fly-phase handler.
fun step_fly_pos() {
if fly_dx_sign == 0 {
fly_x += FLY_STEP
} else {
fly_x -= FLY_STEP
}
if fly_dy_sign == 0 {
fly_y += FLY_STEP
} else {
fly_y -= FLY_STEP
}
}
// Initialise the fly state for a card slide from (sx, sy) using
// the given direction signs. NEScript v0.1 only allocates four
// zero-page slots ($04-$07) for function parameters, so callers
// must stash `fly_card` and `fly_face_up` directly into the
// globals before invoking arm_fly — passing them as the 5th and
// 6th params would silently drop the values on the floor.
fun arm_fly(sx: u8, sy: u8, dxsign: u8, dysign: u8) {
fly_x = sx
fly_y = sy
fly_dx_sign = dxsign
fly_dy_sign = dysign
}