1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/examples/war/render.ne

217 lines
8.1 KiB
Text
Raw Normal View History

2026-04-15 15:22:20 +00:00
// 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
}