mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
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
216 lines
8.1 KiB
Text
216 lines
8.1 KiB
Text
// 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
|
||
}
|