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