mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-26 15:31:15 +02:00
1785 lines
65 KiB
Lua
1785 lines
65 KiB
Lua
-- Surfing Pikachu minigame (engine/minigame/surfing_pikachu.asm): the
|
||
-- Summer Beach House wave run. Pikachu accelerates automatically down
|
||
-- the wave face, launches off the crest, spins in the air with buffered
|
||
-- D-pad controls, and lands to match the wave slope for points.
|
||
--
|
||
-- 1:1 Love2D port of the authentic Pokémon Yellow disassembly:
|
||
-- - Exact 13-stage routine state machine (Start, Run, Coast, Outro, Tally, Hi-Score, Game Over)
|
||
-- - Rigid 16.8 fixed-point integer physics (no float drift; speed += 2 per frame, 512 max)
|
||
-- - Input accumulator eliminating the 3-frame polling blind spot
|
||
-- - GB hardware VBlank execution order (collision boundary checked before position update)
|
||
-- - Deterministic Game Boy DIV/Random LFSR wave sequence generator
|
||
-- - 14-angle rotation model with 3-frame joy duty cycle (Right = frontflip, Left = backflip)
|
||
-- - Stunt scoring: +50 (single), +150 (double same), +350 (triple same), +180 (mixed), +500 (triple mixed)
|
||
-- - Tile interaction landing matrix (Clean, Rough -64, Hard -128, Crash/Wipeout)
|
||
-- - Non-fatal crash recovery: Pikachu wipes out for 96 frames, resets speed to 64 (0.25), and continues
|
||
-- - HP stamina timer counting down from 6000 BCD (60.00s)
|
||
-- - HUD progress track with mini-Pikachu marker advancing across 24 sections
|
||
-- - Animated "START" and "Oh no.." banners, floating trick score popups, and water sprays
|
||
-- - Dynamic 5-tier music tempo tracking Pikachu's speed
|
||
-- - Full beach outro results scene with step-by-step tally animation and high-score fanfare
|
||
|
||
local Font = require("src.render.Font")
|
||
local Strings = require("src.core.Strings")
|
||
local Music = require("src.core.Music")
|
||
local Sound = require("src.core.Sound")
|
||
local bit = require("bit")
|
||
|
||
local SurfingMinigame = {}
|
||
SurfingMinigame.__index = function(t, k)
|
||
if k == "phase" then
|
||
local r = rawget(t, "routine")
|
||
if r and r >= 4 then
|
||
return "results"
|
||
elseif rawget(t, "pikaState") == 1 then
|
||
return "air"
|
||
elseif rawget(t, "pikaState") == 3 then
|
||
return "wipeout"
|
||
else
|
||
return "ride"
|
||
end
|
||
elseif k == "speed" then
|
||
return (rawget(t, "speedFixed") or 64) / 256
|
||
elseif k == "distance" then
|
||
return (rawget(t, "distanceFixed") or 0) / 256
|
||
elseif k == "score" then
|
||
local tot = rawget(t, "totalScore") or 0
|
||
if tot > 0 then return tot end
|
||
return (rawget(t, "radness") or 0) + (rawget(t, "hp") or 0)
|
||
end
|
||
return SurfingMinigame[k]
|
||
end
|
||
SurfingMinigame.isOpaque = true
|
||
|
||
-- Fixed-point physics constants (256 = 1.0 px/frame)
|
||
-- Pret metric audit (surfing_pikachu.asm):
|
||
-- Speed: init 0.25, max 2.0 (high byte cp $2), +1/128/frame, jump min GetSpeedDividedBy32 >= $a
|
||
-- Penalties: rough -0.25, hard -0.5, wipeout reset 0.25 (underflow guards at 0.25 / 0.5)
|
||
-- HP: $6000 BCD, -1/frame; course: 24 sections (distance byte cp $18); Big Kahuna at section $16
|
||
-- BG scroll: 1.5 px/frame; coast: 9.0 px/frame for 192 frames; crash $60 frames; landing splash $20/4
|
||
-- Flips: left cp $b, right cp $d; radness meter caps at $3; trick flags bits 0=left 1=right
|
||
-- Joypad: SurfingPikachu_GetJoypad_3FrameBuffer reloads hFrameCounter with $2 (1 sample + 2 blank frames)
|
||
-- Music tempo index: high byte of ((speed & $3ff) << 1) → tiers at speed 128/256/384/512
|
||
local SPEED_INITIAL = 64 -- 0.25 * 256
|
||
local SPEED_MAX = 512 -- 2.00 * 256 (SpeedUpPikachu: high byte cp $2)
|
||
local SPEED_ACCEL = 2 -- 0.0078125 = 1/128 px/frame in 8.8 fixed
|
||
local SPEED_ROUGH_PENALTY = 64 -- 0.25 * 256
|
||
local SPEED_HARD_PENALTY = 128 -- 0.50 * 256
|
||
local PIKA_SPRITE_OFFSET = 16 -- sprite anchor above wSurfingMinigamePikachuObjectHeight
|
||
local SPEED_JUMP_MIN_DIV32 = 10 -- TryStartJump: GetSpeedDividedBy32 cp $a (speed >= 1.25)
|
||
local FLIP_LEFT_FRAMES = 11 -- DPadAction .dLeft cp $b
|
||
local FLIP_RIGHT_FRAMES = 13 -- DPadAction .dRight cp $d
|
||
local RADNESS_METER_MAX = 3 -- IncreaseRadnessMeter cap
|
||
local JOY_FRAME_RELOAD = 2 -- GetJoypad_3FrameBuffer: ld a, $2
|
||
local CRASH_FRAMES = 96 -- UpdateCrashedPikachu initial timer $60
|
||
local COAST_FRAMES = 192 -- WaitToShowResults routine delay
|
||
local GAME_OVER_DELAY = 128 -- Game over before accepting A ($80)
|
||
local LANDING_SPLASH_FRAMES = 8 -- FIELD_C += 4 until cp $20
|
||
local RESULTS_FRAMESET_INIT = 0x0f -- InitResultsPikachu: frameset ID written to ANIM_OBJ_FRAME_SET
|
||
local RESULTS_BOB_CYCLE = 64 -- UpdateResultsPikachu FIELD_C & $3f
|
||
local RESULTS_BOB_START = 32 -- sine bob only when FIELD_C >= $20
|
||
local RESULTS_BOB_AMP = 2 -- SurfingPikachu_Sine scale ($10 table; ~2px on screen)
|
||
local RESULTS_PIKA_BASES = { 0xa0, 0xa3 } -- .ResultsPikachu OAM frame pair
|
||
|
||
-- Original constants (surfing_pikachu.asm)
|
||
local FLAT_WATER_Y = 116 -- OAM Y 0x74 = screen Y 100 (PIKA_Y = FLAT_WATER_Y - 16)
|
||
local PIKA_X = 68 -- Fixed screen X while riding (center 80, 24px pose)
|
||
local TOTAL_SECTIONS = 24 -- wSurfingMinigameDistance byte 0 reaches $18 (24)
|
||
local SECTION_ACC_MAX = 65536 -- 16-bit distance acc overflow (256px per section in 8.8 fixed)
|
||
local SECTION_PX = 256 -- pixels advanced per section (65536 >> 8)
|
||
local BG_HEIGHT = 128 -- Rows the BG shows; HP window covers the rest (y=128..144)
|
||
local COAST_SPEED_FIXED = 9 * 256 -- SurfingMinigame_CoastAfterGoal: 9.0 px/frame
|
||
local BG_SCROLL_STEP = 384 -- ScrollAndGenerateBGMap: 1.5 px/frame (8.8 fixed)
|
||
local RDIV_PER_FRAME = 17 -- rDIV @ 16384 Hz ≈ 273 ticks per 59.7275 Hz frame (mod 256)
|
||
local COAST_X_OFFSET = 160 -- $a0 ahead of viewport (CoastAfterGoal)
|
||
local OUTRO_SCROLL_X_OFFSET = 224 -- TILEMAP_WIDTH_PX - 32 (ScrollToResultsScreen)
|
||
local OUTRO_SCROLL_START = 144 -- hSCX $90 at results transition
|
||
local OUTRO_SCROLL_STEP = 4 -- hSCX -= 4 per frame (36 frames total)
|
||
local FINISH_DISTANCE_FIXED = TOTAL_SECTIONS * SECTION_PX * 256
|
||
local MAP_COLS = 16 -- vBGMap0 metatile columns (256px / 16)
|
||
-- SurfingPikachuMinigame_InitStaticSpriteLayout cloud OAM X coords (9 sprites)
|
||
local CLOUD_SPRITE_X_INIT = { 32, 40, 48, 56, 64, 128, 136, 144, 152 }
|
||
|
||
local function scrollPx(self)
|
||
return math.floor((self.scrollFixed or 0) / 256)
|
||
end
|
||
|
||
-- Routine numbers (wSurfingMinigameRoutineNumber)
|
||
local ROUTINE_TITLE = -1
|
||
local ROUTINE_START_GAME = 0
|
||
local ROUTINE_RUN_GAME = 1
|
||
local ROUTINE_WAIT_RESULTS = 2
|
||
local ROUTINE_SCROLL_RESULTS = 3
|
||
local ROUTINE_DRAW_RESULTS = 4
|
||
local ROUTINE_WRITE_HP_LEFT = 5
|
||
local ROUTINE_WRITE_RADNESS = 6
|
||
local ROUTINE_WRITE_TOTAL = 7
|
||
local ROUTINE_ADD_HP_TOTAL = 8
|
||
local ROUTINE_ADD_RAD_TOTAL = 9
|
||
local ROUTINE_WAIT_LAST = 10
|
||
local ROUTINE_EXIT_ON_PRESS_A = 11
|
||
local ROUTINE_GAME_OVER = 12
|
||
|
||
local function isOutroScroll(self)
|
||
return self.routine == ROUTINE_WAIT_RESULTS or self.routine == ROUTINE_SCROLL_RESULTS
|
||
end
|
||
|
||
local function displayScx(self)
|
||
return self.hScx or 0
|
||
end
|
||
|
||
-- SurfingMinigame_UpdateMusicTempo: index = high byte of ((speed & $3ff) << 1)
|
||
local function pretTempoTier(speedFixed)
|
||
local lo = bit.band(speedFixed, 0xFF)
|
||
local hi = bit.band(math.floor(speedFixed / 256), 3)
|
||
local shifted = bit.band((lo + hi * 256) * 2, 0xFFFF)
|
||
return math.min(4, math.floor(shifted / 256)) + 1
|
||
end
|
||
|
||
-- SurfingMinigame_GetSpeedDividedBy32 (speed * 8, high byte)
|
||
local function getSpeedDividedBy32(speedFixed)
|
||
return math.floor((speedFixed * 8) / 256)
|
||
end
|
||
|
||
local function pikaSpriteYFromObjectHeight(objectHeightPx)
|
||
return math.floor(objectHeightPx or FLAT_WATER_Y) - PIKA_SPRITE_OFFSET
|
||
end
|
||
|
||
-- SurfingMinigame_ReduceSpeedBy64 / ReduceSpeedBy128
|
||
local function reduceSpeedBy64(speedFixed)
|
||
if speedFixed >= 256 then
|
||
return speedFixed - SPEED_ROUGH_PENALTY
|
||
elseif speedFixed >= SPEED_INITIAL then
|
||
return speedFixed - SPEED_ROUGH_PENALTY
|
||
end
|
||
return 0
|
||
end
|
||
|
||
local function reduceSpeedBy128(speedFixed)
|
||
if speedFixed >= 256 then
|
||
return speedFixed - SPEED_HARD_PENALTY
|
||
elseif speedFixed >= SPEED_HARD_PENALTY then
|
||
return speedFixed - SPEED_HARD_PENALTY
|
||
end
|
||
return 0
|
||
end
|
||
|
||
local function jumpArcCombined(self)
|
||
return (self.jumpArcMagnitude or 0) * 256 + (self.jumpArcFraction or 0)
|
||
end
|
||
|
||
local function setJumpArcCombined(self, value)
|
||
if value < 0 then value = 0 end
|
||
self.jumpArcMagnitude = math.floor(value / 256)
|
||
self.jumpArcFraction = value % 256
|
||
end
|
||
|
||
local function applyJumpArcDelta(self, delta)
|
||
setJumpArcCombined(self, jumpArcCombined(self) + delta)
|
||
end
|
||
|
||
local function applyJumpVerticalDelta(self, sign)
|
||
local a = self.jumpArcMagnitude or 0
|
||
if a == 0 and (self.jumpArcFraction or 0) == 0 then return end
|
||
self.pikaSubY = (self.pikaSubY or 0) + (a * a) * 4
|
||
local intDelta = math.floor(self.pikaSubY / 256)
|
||
self.pikaSubY = self.pikaSubY % 256
|
||
if sign < 0 then
|
||
self.pikaY = self.pikaY - intDelta
|
||
else
|
||
self.pikaY = self.pikaY + intDelta
|
||
end
|
||
end
|
||
|
||
-- pret GenerateBGMap write column: ((hSCX + XOffset) & $f0) / 16
|
||
local function mapColWrite(self)
|
||
local xOffset = COAST_X_OFFSET
|
||
if self.routine == ROUTINE_SCROLL_RESULTS then
|
||
xOffset = OUTRO_SCROLL_X_OFFSET
|
||
end
|
||
local sum = (displayScx(self) + xOffset) % 256
|
||
return math.floor(bit.band(sum, 0xF0) / 16) % MAP_COLS
|
||
end
|
||
|
||
local function mapColScreen(scx, screenCol)
|
||
return (math.floor(scx / 16) + screenCol) % MAP_COLS
|
||
end
|
||
|
||
local function mapColAtX(scx, x)
|
||
return math.floor((scx + x) / 16) % MAP_COLS
|
||
end
|
||
|
||
-- Pikachu states (wSurfingMinigamePikachuState)
|
||
local PIKA_STATE_RIDING = 0
|
||
local PIKA_STATE_JUMPING = 1
|
||
local PIKA_STATE_LANDING = 2
|
||
local PIKA_STATE_CRASHED = 3
|
||
local PIKA_STATE_GAME_END = 4
|
||
local PIKA_STATE_INIT_RESULTS = 5
|
||
local PIKA_STATE_RESULTS = 6
|
||
|
||
-- Metatile lookup (2x2 tiles each)
|
||
local BG_METATILES = {
|
||
[0x00] = { 0x00, 0x00, 0x00, 0x00 }, -- sky block (blank)
|
||
[0x01] = { 0x0b, 0x0b, 0x0b, 0x0b }, -- open water
|
||
[0x02] = { 0x0b, 0x02, 0x02, 0x06 },
|
||
[0x03] = { 0x03, 0x0b, 0x07, 0x03 },
|
||
[0x04] = { 0x06, 0x06, 0x06, 0x06 },
|
||
[0x05] = { 0x07, 0x07, 0x07, 0x07 },
|
||
[0x06] = { 0x06, 0x04, 0x04, 0x08 },
|
||
[0x07] = { 0x05, 0x07, 0x08, 0x05 },
|
||
[0x08] = { 0x0b, 0x0b, 0x11, 0x12 },
|
||
[0x09] = { 0x0b, 0x0b, 0x13, 0x03 },
|
||
[0x0a] = { 0x14, 0x12, 0x04, 0x08 },
|
||
[0x0b] = { 0x13, 0x07, 0x08, 0x05 },
|
||
[0x0c] = { 0x06, 0x14, 0x06, 0x14 },
|
||
[0x0d] = { 0x13, 0x07, 0x13, 0x07 },
|
||
[0x0e] = { 0x08, 0x08, 0x08, 0x08 }, -- solid blue
|
||
[0x0f] = { 0x14, 0x12, 0x14, 0x12 },
|
||
[0x10] = { 0x0b, 0x11, 0x02, 0x14 },
|
||
[0x11] = { 0x06, 0x14, 0x06, 0x14 },
|
||
[0x12] = { 0x0c, 0x0c, 0x0d, 0x0d }, -- beach top block
|
||
[0x13] = { 0x0d, 0x0d, 0x0d, 0x0d }, -- beach sand block
|
||
[0x14] = { 0x0e, 0x0f, 0x10, 0x0b }, -- beach shore block
|
||
[0x15] = { 0x12, 0x13, 0x12, 0x13 },
|
||
}
|
||
|
||
-- Wave pattern slices (8 metatiles each)
|
||
local WAVE_PATTERNS = {
|
||
[0x00] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01 },
|
||
[0x01] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x04, 0x06 },
|
||
[0x02] = { 0x00, 0x00, 0x00, 0x01, 0x02, 0x04, 0x06, 0x0e },
|
||
[0x03] = { 0x00, 0x00, 0x00, 0x10, 0x11, 0x06, 0x0e, 0x0e },
|
||
[0x04] = { 0x00, 0x00, 0x00, 0x15, 0x15, 0x0e, 0x0e, 0x0e },
|
||
[0x05] = { 0x00, 0x00, 0x00, 0x03, 0x05, 0x07, 0x0e, 0x0e },
|
||
[0x06] = { 0x00, 0x00, 0x00, 0x01, 0x03, 0x05, 0x07, 0x0e },
|
||
[0x07] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x05, 0x07 },
|
||
[0x08] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x04, 0x06 },
|
||
[0x09] = { 0x00, 0x00, 0x00, 0x01, 0x02, 0x04, 0x06, 0x0e },
|
||
[0x0a] = { 0x00, 0x00, 0x00, 0x08, 0x0f, 0x0a, 0x0e, 0x0e },
|
||
[0x0b] = { 0x00, 0x00, 0x00, 0x09, 0x0d, 0x0b, 0x0e, 0x0e },
|
||
[0x0c] = { 0x00, 0x00, 0x00, 0x01, 0x03, 0x05, 0x07, 0x0e },
|
||
[0x0d] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x05, 0x07 },
|
||
[0x0e] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x04, 0x06 },
|
||
[0x0f] = { 0x00, 0x00, 0x00, 0x01, 0x10, 0x11, 0x06, 0x0e },
|
||
[0x10] = { 0x00, 0x00, 0x00, 0x01, 0x15, 0x15, 0x0e, 0x0e },
|
||
[0x11] = { 0x00, 0x00, 0x00, 0x01, 0x03, 0x05, 0x07, 0x0e },
|
||
[0x12] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x05, 0x07 },
|
||
[0x13] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x04, 0x06 },
|
||
[0x14] = { 0x00, 0x00, 0x00, 0x01, 0x08, 0x0f, 0x0a, 0x0e },
|
||
[0x15] = { 0x00, 0x00, 0x00, 0x01, 0x09, 0x0d, 0x0b, 0x0e },
|
||
[0x16] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x05, 0x07 },
|
||
[0x17] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x10, 0x11, 0x06 },
|
||
[0x18] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x15, 0x15, 0x0e },
|
||
[0x19] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x05, 0x07 },
|
||
[0x1a] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x08, 0x0f, 0x0a },
|
||
[0x1b] = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x09, 0x0d, 0x0b },
|
||
[0x1c] = { 0x00, 0x00, 0x00, 0x14, 0x14, 0x14, 0x14, 0x14 },
|
||
beach = { 0x00, 0x00, 0x00, 0x12, 0x13, 0x13, 0x13, 0x13 },
|
||
}
|
||
|
||
local ADV, RESET, STAY = 0, 1, 2
|
||
local WAVE_STEPS = {
|
||
[0x01] = { 0x13, 116, 108, ADV }, [0x02] = { 0x14, 100, 92, ADV },
|
||
[0x03] = { 0x15, 92, 92, ADV }, [0x04] = { 0x16, 100, 108, ADV },
|
||
[0x05] = { 0x00, 116, 116, ADV }, [0x06] = { 0x17, 116, 108, ADV },
|
||
[0x07] = { 0x18, 100, 100, ADV }, [0x08] = { 0x19, 100, 108, ADV },
|
||
[0x09] = { 0x00, 116, 116, ADV }, [0x0a] = { 0x00, 116, 116, ADV },
|
||
[0x0b] = { 0x00, 116, 116, ADV }, [0x0c] = { 0x00, 116, 116, ADV },
|
||
[0x0d] = { 0x00, 116, 116, RESET },
|
||
[0x0e] = { 0x08, 116, 108, ADV }, [0x0f] = { 0x09, 100, 92, ADV },
|
||
[0x10] = { 0x0a, 84, 76, ADV }, [0x11] = { 0x0b, 76, 76, ADV },
|
||
[0x12] = { 0x0c, 84, 92, ADV }, [0x13] = { 0x0d, 100, 108, ADV },
|
||
[0x14] = { 0x00, 116, 116, ADV }, [0x15] = { 0x00, 116, 116, ADV },
|
||
[0x16] = { 0x00, 116, 116, ADV }, [0x17] = { 0x00, 116, 116, ADV },
|
||
[0x18] = { 0x00, 116, 116, ADV }, [0x19] = { 0x00, 116, 116, RESET },
|
||
[0x1a] = { 0x0e, 116, 108, ADV }, [0x1b] = { 0x0f, 100, 92, ADV },
|
||
[0x1c] = { 0x10, 84, 84, ADV }, [0x1d] = { 0x11, 84, 92, ADV },
|
||
[0x1e] = { 0x12, 100, 108, ADV }, [0x1f] = { 0x0e, 116, 108, ADV },
|
||
[0x20] = { 0x0f, 100, 92, ADV }, [0x21] = { 0x10, 84, 84, ADV },
|
||
[0x22] = { 0x11, 84, 92, ADV }, [0x23] = { 0x12, 100, 108, ADV },
|
||
[0x24] = { 0x00, 116, 116, ADV }, [0x25] = { 0x00, 116, 116, ADV },
|
||
[0x26] = { 0x00, 116, 116, ADV }, [0x27] = { 0x00, 116, 116, ADV },
|
||
[0x28] = { 0x00, 116, 116, RESET },
|
||
[0x29] = { 0x13, 116, 108, ADV }, [0x2a] = { 0x14, 100, 92, ADV },
|
||
[0x2b] = { 0x15, 92, 92, ADV }, [0x2c] = { 0x16, 100, 108, ADV },
|
||
[0x2d] = { 0x00, 116, 116, ADV }, [0x2e] = { 0x00, 116, 116, ADV },
|
||
[0x2f] = { 0x00, 116, 116, ADV }, [0x30] = { 0x00, 116, 116, ADV },
|
||
[0x31] = { 0x00, 116, 116, RESET },
|
||
[0x32] = { 0x17, 116, 108, ADV }, [0x33] = { 0x18, 100, 100, ADV },
|
||
[0x34] = { 0x19, 100, 108, ADV }, [0x35] = { 0x17, 116, 108, ADV },
|
||
[0x36] = { 0x18, 100, 100, ADV }, [0x37] = { 0x19, 100, 108, ADV },
|
||
[0x38] = { 0x17, 116, 108, ADV }, [0x39] = { 0x18, 100, 100, ADV },
|
||
[0x3a] = { 0x19, 100, 108, ADV }, [0x3b] = { 0x00, 116, 116, ADV },
|
||
[0x3c] = { 0x00, 116, 116, ADV }, [0x3d] = { 0x00, 116, 116, ADV },
|
||
[0x3e] = { 0x00, 116, 116, ADV }, [0x3f] = { 0x00, 116, 116, RESET },
|
||
[0x40] = { 0x1a, 116, 108, ADV }, [0x41] = { 0x1b, 108, 108, ADV },
|
||
[0x42] = { 0x0e, 116, 108, ADV }, [0x43] = { 0x0f, 100, 92, ADV },
|
||
[0x44] = { 0x10, 84, 84, ADV }, [0x45] = { 0x11, 84, 92, ADV },
|
||
[0x46] = { 0x12, 100, 108, ADV }, [0x47] = { 0x1a, 116, 108, ADV },
|
||
[0x48] = { 0x1b, 108, 108, ADV }, [0x49] = { 0x00, 116, 116, ADV },
|
||
[0x4a] = { 0x00, 116, 116, ADV }, [0x4b] = { 0x00, 116, 116, ADV },
|
||
[0x4c] = { 0x00, 116, 116, RESET },
|
||
[0x4d] = { 0x08, 116, 108, ADV }, [0x4e] = { 0x09, 100, 92, ADV },
|
||
[0x4f] = { 0x0a, 84, 76, ADV }, [0x50] = { 0x0b, 76, 76, ADV },
|
||
[0x51] = { 0x0c, 84, 92, ADV }, [0x52] = { 0x0d, 100, 108, ADV },
|
||
[0x53] = { 0x00, 116, 116, ADV }, [0x54] = { 0x1a, 116, 108, ADV },
|
||
[0x55] = { 0x1b, 108, 108, ADV }, [0x56] = { 0x1a, 116, 108, ADV },
|
||
[0x57] = { 0x1b, 108, 108, ADV }, [0x58] = { 0x00, 116, 116, ADV },
|
||
[0x59] = { 0x00, 116, 116, ADV }, [0x5a] = { 0x00, 116, 116, ADV },
|
||
[0x5b] = { 0x00, 116, 116, RESET },
|
||
[0x5c] = { 0x0e, 116, 108, ADV }, [0x5d] = { 0x0f, 100, 92, ADV },
|
||
[0x5e] = { 0x10, 84, 84, ADV }, [0x5f] = { 0x11, 84, 92, ADV },
|
||
[0x60] = { 0x12, 100, 108, ADV }, [0x61] = { 0x13, 116, 108, ADV },
|
||
[0x62] = { 0x14, 100, 92, ADV }, [0x63] = { 0x15, 92, 92, ADV },
|
||
[0x64] = { 0x16, 100, 108, ADV }, [0x65] = { 0x00, 116, 116, ADV },
|
||
[0x66] = { 0x00, 116, 116, ADV }, [0x67] = { 0x00, 116, 116, ADV },
|
||
[0x68] = { 0x00, 116, 116, ADV }, [0x69] = { 0x00, 116, 116, RESET },
|
||
[0x6a] = { 0x01, 116, 108, ADV }, [0x6b] = { 0x02, 100, 92, ADV },
|
||
[0x6c] = { 0x03, 84, 76, ADV }, [0x6d] = { 0x04, 68, 68, ADV },
|
||
[0x6e] = { 0x05, 68, 76, ADV }, [0x6f] = { 0x06, 84, 92, ADV },
|
||
[0x70] = { 0x07, 100, 108, ADV }, [0x71] = { 0x00, 116, 116, STAY },
|
||
[0x72] = { 0x00, 116, 116, ADV }, [0x73] = { 0x1c, 116, 116, ADV },
|
||
[0x74] = { "beach", 116, 116, ADV }, [0x75] = { "beach", 116, 116, ADV },
|
||
[0x76] = { "beach", 116, 116, ADV }, [0x77] = { "beach", 116, 116, ADV },
|
||
[0x78] = { "beach", 116, 116, ADV }, [0x79] = { "beach", 116, 116, ADV },
|
||
[0x7a] = { "beach", 116, 116, ADV }, [0x7b] = { "beach", 116, 116, RESET },
|
||
}
|
||
local SEQ_STARTS = {
|
||
0x01, 0x0e, 0x1a, 0x29, 0x32, 0x40, 0x4d, 0x5c
|
||
}
|
||
|
||
SurfingMinigame.BG_METATILES = BG_METATILES
|
||
SurfingMinigame.WAVE_PATTERNS = WAVE_PATTERNS
|
||
SurfingMinigame.WAVE_STEPS = WAVE_STEPS
|
||
|
||
-- Pikachu base frames: 7 visual angles x 2 animation toggle frames each
|
||
local ANGLE_BASES = {
|
||
[1] = { 0x00, 0x36 }, -- Angle 00 (nose up steep / backflip apex)
|
||
[2] = { 0x03, 0x39 }, -- Angle 01 (nose up moderate)
|
||
[3] = { 0x06, 0x3c }, -- Angle 02 (nose up slight)
|
||
[4] = { 0x09, 0x60 }, -- Angle 03 (flat horizontal ride)
|
||
[5] = { 0x0c, 0x63 }, -- Angle 04 (nose down slight)
|
||
[6] = { 0x30, 0x66 }, -- Angle 05 (nose down moderate)
|
||
[7] = { 0x33, 0x69 }, -- Angle 06 (nose down steep / frontflip apex)
|
||
}
|
||
|
||
-- OAM Definitions from surfing_pikachu_oam.asm
|
||
-- .WaterSpray (3 tiles, relative to PIKA_X = 68, y = self.pikaY)
|
||
local OAM_WATER_SPRAY = {
|
||
{ dy = -4, dx = 11, tile = 0xa7, xflip = false },
|
||
{ dy = 4, dx = 3, tile = 0xb6, xflip = false },
|
||
{ dy = 4, dx = 11, tile = 0xb7, xflip = false },
|
||
}
|
||
|
||
-- .SmallSplash (6 tiles, relative to cx = 80, cy = self.pikaY + 4)
|
||
local OAM_SMALL_SPLASH = {
|
||
{ dy = -4, dx = -16, tile = 0xa7, xflip = true },
|
||
{ dy = -4, dx = 8, tile = 0xa7, xflip = false },
|
||
{ dy = 4, dx = -16, tile = 0xb7, xflip = true },
|
||
{ dy = 4, dx = -8, tile = 0xb6, xflip = true },
|
||
{ dy = 4, dx = 0, tile = 0xb6, xflip = false },
|
||
{ dy = 4, dx = 8, tile = 0xb7, xflip = false },
|
||
}
|
||
|
||
-- .LargeSplash (12 tiles, relative to cx = 80, cy = self.pikaY + 4)
|
||
local OAM_LARGE_SPLASH = {
|
||
{ dy = -12, dx = -16, tile = 0xa8, xflip = false },
|
||
{ dy = -12, dx = -8, tile = 0xa9, xflip = false },
|
||
{ dy = -12, dx = 0, tile = 0xa9, xflip = true },
|
||
{ dy = -12, dx = 8, tile = 0xa8, xflip = true },
|
||
{ dy = -4, dx = -16, tile = 0xb8, xflip = false },
|
||
{ dy = -4, dx = -8, tile = 0xb9, xflip = false },
|
||
{ dy = -4, dx = 0, tile = 0xb9, xflip = true },
|
||
{ dy = -4, dx = 8, tile = 0xb8, xflip = true },
|
||
{ dy = 4, dx = -16, tile = 0xc8, xflip = false },
|
||
{ dy = 4, dx = -8, tile = 0xc9, xflip = false },
|
||
{ dy = 4, dx = 0, tile = 0xc9, xflip = true },
|
||
{ dy = 4, dx = 8, tile = 0xc8, xflip = true },
|
||
}
|
||
|
||
-- Intro title Pikachu (surfing_pikachu_oam.asm .IntroPikachu, frames $20-$23).
|
||
-- Each animation frame adds four to the VRAM tile base ($80, $84, $88, $8c);
|
||
-- relative tile ids use the usual 16-wide OBJ row stride ($10 per row).
|
||
local INTRO_PIKA_FRAME_BASE = { 0x80, 0x84, 0x88, 0x8c }
|
||
local INTRO_PIKA_OAM = {
|
||
{ dy = -12, dx = -16, tile = 0x03, xflip = true },
|
||
{ dy = -12, dx = -8, tile = 0x02, xflip = true },
|
||
{ dy = -12, dx = 0, tile = 0x01, xflip = true },
|
||
{ dy = -12, dx = 8, tile = 0x00, xflip = true },
|
||
{ dy = -4, dx = -16, tile = 0x13, xflip = true },
|
||
{ dy = -4, dx = -8, tile = 0x12, xflip = true },
|
||
{ dy = -4, dx = 0, tile = 0x11, xflip = true },
|
||
{ dy = -4, dx = 8, tile = 0x10, xflip = true },
|
||
{ dy = 4, dx = -16, tile = 0x23, xflip = true },
|
||
{ dy = 4, dx = -8, tile = 0x22, xflip = true },
|
||
{ dy = 4, dx = 0, tile = 0x21, xflip = true },
|
||
{ dy = 4, dx = 8, tile = 0x20, xflip = true },
|
||
}
|
||
|
||
-- Beach outro tilemap (gfx/surfing_pikachu/beach_outro.tilemap, 20x10)
|
||
local BEACH_OUTRO = {
|
||
{ 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0e, 0x0f, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
{ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x10, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
{ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0e, 0x0f, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
{ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x10, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
{ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0e, 0x0f, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
{ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x10, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
{ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0e, 0x0f, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
{ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x10, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
{ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0e, 0x0f, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
{ 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x10, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b },
|
||
}
|
||
|
||
-- Authentic SGB / GBC Pikachus Beach Palette (Shade 0=White, Shade 1=Pikachu Yellow, Shade 2=Sea Blue, Shade 3=Black)
|
||
local PIKACHUS_BEACH_PAL = {
|
||
{ 255, 255, 255 }, -- 0: White foam/highlights
|
||
{ 255, 224, 0 }, -- 1: Vibrant Pikachu Yellow
|
||
{ 88, 168, 248 }, -- 2: Ocean Blue Sea Water
|
||
{ 25, 25, 25 }, -- 3: Black Outlines
|
||
}
|
||
-- Title intro uses BG palette slot 1 (PalPacket_PikachusBeachTitle /
|
||
-- UnknownPacket_72751) so the logo's shade-2 pixels tint yellow, not sea blue.
|
||
local PIKACHUS_BEACH_TITLE_PAL = {
|
||
{ 255, 255, 255 },
|
||
{ 132, 132, 132 },
|
||
{ 255, 206, 74 },
|
||
{ 25, 25, 25 },
|
||
}
|
||
|
||
-- 5 Tempo tiers (117, 109, 101, 93, 85 from surfing_pikachu.asm)
|
||
local TEMPO_TIERS = { 1.0, 117 / 109, 117 / 101, 117 / 93, 117 / 85 }
|
||
|
||
function SurfingMinigame.new(game, onDone, skipTitle)
|
||
local self = setmetatable({ game = game, onDone = onDone }, SurfingMinigame)
|
||
self.routine = skipTitle and ROUTINE_RUN_GAME or ROUTINE_TITLE
|
||
self.pikaState = PIKA_STATE_RIDING
|
||
self.t = 0
|
||
self.routineTimer = 0
|
||
self.distanceFixed = 0 -- 16.8 fixed-point course progress (256 = 1 pixel)
|
||
self.distanceSection = 0 -- wSurfingMinigameDistance byte 0 (pret big-endian)
|
||
self.distanceAcc = 0 -- wSurfingMinigameDistance bytes 1-2 (16-bit, big-endian)
|
||
self.scrollFixed = 0 -- BG scroll (hSCX); diverges from distance during outro coast
|
||
self.speedFixed = SPEED_INITIAL -- 64 = 0.25 px/frame
|
||
self.hp = 6000 -- starts at 6000 (60.00 seconds)
|
||
self.radness = 0 -- accumulated trick stunt points
|
||
self.totalScore = 0 -- tallied total score
|
||
self.hiScore = (game and game.save and game.save.surfingHighScore) or 0
|
||
self.newRecord = false
|
||
self.currentPitch = 1.0
|
||
self.isMinigame = true
|
||
self.isFixedSpeed = true
|
||
|
||
-- Hardware RNG simulation (pret never resets hRandomAdd/hRandomSub on minigame entry)
|
||
self.rDiv = 0
|
||
self.rAdd = 0x55
|
||
self.rSub = 0xaa
|
||
|
||
-- Wave height tracking: pret vBGMap0 (32 metatile columns, circular)
|
||
self.waveFn = 0
|
||
self.bgMap = {}
|
||
for c = 0, MAP_COLS - 1 do
|
||
self.bgMap[c] = { pat = WAVE_PATTERNS[0x00], hl = FLAT_WATER_Y, hr = FLAT_WATER_Y }
|
||
end
|
||
-- Linear cols mirror kept for unit tests only
|
||
self.cols = {}
|
||
for c = 0, 10 do
|
||
self.cols[c] = { pat = WAVE_PATTERNS[0x00], hl = FLAT_WATER_Y, hr = FLAT_WATER_Y }
|
||
end
|
||
self.colTail = 10
|
||
|
||
-- wSurfingMinigameWaveHeight (20 entries, shifted each GenerateBGMap)
|
||
self.waveHeight = {}
|
||
for i = 1, 20 do
|
||
self.waveHeight[i] = FLAT_WATER_Y
|
||
end
|
||
self.bgMapReadTile = 0x0b -- wSurfingMinigameBGMapReadBuffer (updated each ReadBGMapBuffer)
|
||
self.waveRandomValue = 0 -- wSurfingMinigameWaveRandomValue (Random each RunGame frame)
|
||
|
||
-- Jumping, Arc Physics & Air Rotation
|
||
self.pikaY = FLAT_WATER_Y - 16 -- integer screen Y of Pikachu (sea waterline)
|
||
self.pikaSubY = 0 -- 8.8 subpixel carry (0..255)
|
||
self.pikaYOffset = 0 -- sine offset (splash/bobbing)
|
||
self.jumpArcMagnitude = 0 -- wSurfingMinigameJumpArcMagnitude (GetSpeedDividedBy32, >= 10)
|
||
self.jumpArcFraction = 0 -- wSurfingMinigameJumpArcFraction (8.8 sub-byte)
|
||
self.jumpDescending = false
|
||
self.frameSet = 4 -- Starts at Frame 4 (flat horizontal ride)
|
||
self.boardAngleOffset = 0 -- wobbling 0..2
|
||
self.boardAngleDecreasing = false
|
||
self.boardAngleTimer = 0
|
||
self.crashTimer = 0
|
||
self.landingTimer = 0
|
||
|
||
-- Outro coast / results card (SurfingMinigame_WaitToShowResults .. WaitLast)
|
||
self.hScx = 0 -- hSCX register (8-bit, wraps)
|
||
self.scxFrac = 0 -- wSurfingMinigameSCX fractional byte
|
||
self.scxHi = 0 -- wSurfingMinigameSCXHi (zero-init WRAM; band 0 until hSCX >= $10)
|
||
self.scxLast = 0 -- wSurfingMinigameSCX2 (last hSCX seen by GenerateBGMap)
|
||
self.showResultsCard = false
|
||
self.outroLines = { hp = false, rad = false, total = false, hiScore = false }
|
||
|
||
-- SurfingPikachu_GetJoypad_3FrameBuffer (hJoy5 + hFrameCounter countdown)
|
||
self.joyFrameCounter = 0
|
||
self.joy5Left = false
|
||
self.joy5Right = false
|
||
self.rotCountLeft = 0
|
||
self.rotCountRight = 0
|
||
self.radnessMeter = 0 -- consecutive flips (capped at 3)
|
||
self.trickFlags = 0 -- bit 0 = right flip (front), bit 1 = left flip (back)
|
||
|
||
-- Sprites & Popups
|
||
self.startBannerX = 224 -- slides from 224 to 80 (center)
|
||
self.introPikaX = 80 -- intro Pikachu walks off-screen before the run
|
||
self.ohNoBanner = false
|
||
self.trickPopups = {} -- { text = "+150", x, y, timer }
|
||
self.waterSprays = {} -- { x, y, timer }
|
||
self.sprayTimer = 0
|
||
self.cloudScrollFrac = 0 -- wSurfingMinigameCloudScrollFraction
|
||
self.cloudSpriteX = {}
|
||
for i, x in ipairs(CLOUD_SPRITE_X_INIT) do
|
||
self.cloudSpriteX[i] = x
|
||
end
|
||
|
||
-- Results Tally Animation
|
||
self.tallyStep = 0
|
||
self.tallyTimer = 0
|
||
|
||
-- Load sheets safely across all GameVersion prefix paths
|
||
local function sheet(path)
|
||
if not (love and love.graphics and love.graphics.newImage) then return nil end
|
||
local GameVersion = require("src.core.GameVersion")
|
||
local prefix = (GameVersion and GameVersion.cachePrefix and GameVersion.cachePrefix(game and game.version)) or "yellow/"
|
||
local filename = path:match("([^/]+)$") or path
|
||
local paths = {
|
||
prefix .. path,
|
||
path,
|
||
"yellow/assets/generated/minigame/" .. filename,
|
||
"assets/generated/minigame/" .. filename,
|
||
}
|
||
for _, p in ipairs(paths) do
|
||
local ok, img = pcall(love.graphics.newImage, p)
|
||
if ok and img then return img end
|
||
end
|
||
return nil
|
||
end
|
||
self.bg = sheet("assets/generated/minigame/surf_1a.png")
|
||
self.ob = sheet("assets/generated/minigame/surf_1b.png")
|
||
self.intro = sheet("assets/generated/minigame/surf_1c.png")
|
||
self.titleBg = sheet("assets/generated/minigame/title_bg.png")
|
||
|
||
if self.bg then
|
||
self.tq = {}
|
||
local bgW, bgH = self.bg:getDimensions()
|
||
for n = 0, 64 do
|
||
self.tq[n] = love.graphics.newQuad((n % 5) * 8, math.floor(n / 5) * 8, 8, 8, bgW, bgH)
|
||
end
|
||
end
|
||
|
||
if self.ob then
|
||
self.oq = {}
|
||
local obW, obH = self.ob:getDimensions()
|
||
for n = 0, 255 do
|
||
self.oq[n] = love.graphics.newQuad((n % 16) * 8, math.floor(n / 16) * 8, 8, 8, obW, obH)
|
||
end
|
||
end
|
||
|
||
if self.intro then
|
||
self.iq = {}
|
||
local iW, iH = self.intro:getDimensions() -- 96x96 tile sheet (12x12 tiles)
|
||
for n = 0, 143 do
|
||
self.iq[n] = love.graphics.newQuad((n % 12) * 8, math.floor(n / 12) * 8, 8, 8, iW, iH)
|
||
end
|
||
-- Title Banner Logo ("PIKACHU'S BEACH") (96x32 px at Y=32..64)
|
||
self.introLogoQuad = love.graphics.newQuad(0, 32, 96, 32, iW, iH)
|
||
-- Instruction Text ("Use Control Pad to Surf") (96x32 px at Y=64..96)
|
||
self.introTextQuad = love.graphics.newQuad(0, 64, 96, 32, iW, iH)
|
||
end
|
||
|
||
-- GPU Shader & Canvas for authentic HBlank wave distortion
|
||
if love and love.graphics and love.graphics.newShader then
|
||
local shaderCode = [[
|
||
extern float u_time;
|
||
extern float u_water_line;
|
||
|
||
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) {
|
||
vec2 uv = texture_coords;
|
||
if (uv.y >= u_water_line) {
|
||
float pixel_y = uv.y * 144.0;
|
||
float wave_px = sin((pixel_y * 0.39269908) + (u_time * 6.0)) * (1.5 / 160.0);
|
||
uv.x = fract(uv.x + wave_px);
|
||
}
|
||
return Texel(texture, uv) * color;
|
||
}
|
||
]]
|
||
local ok, shader = pcall(love.graphics.newShader, shaderCode)
|
||
if ok then self.waveShader = shader end
|
||
end
|
||
|
||
if love and love.graphics and love.graphics.newCanvas then
|
||
local ok, canvas = pcall(love.graphics.newCanvas, 160, 144)
|
||
if ok then self.bgCanvas = canvas end
|
||
end
|
||
|
||
Music.play(game and game.data, "Music_SurfingPikachu")
|
||
return self
|
||
end
|
||
|
||
-- Transition cleanly from Title Screen to active run, flushing input bleed-through
|
||
function SurfingMinigame:startFromTitle()
|
||
self.routine = ROUTINE_START_GAME
|
||
self.joyFrameCounter = 0
|
||
self.joy5Left = false
|
||
self.joy5Right = false
|
||
self.rotCountLeft = 0
|
||
self.rotCountRight = 0
|
||
-- Title screen elapsed frames churn rDIV/LFSR like overworld play before entry.
|
||
for _ = 1, self.t + math.floor(self.introPikaX / 2) do
|
||
self:getGBRandom()
|
||
end
|
||
self:resetTempo()
|
||
if self.game and self.game.input and self.game.input.clearPressed then
|
||
self.game.input:clearPressed()
|
||
end
|
||
end
|
||
|
||
-- Authentic Game Boy LFSR Random Number Generator (engine/math/random.asm Random_)
|
||
function SurfingMinigame:getGBRandom()
|
||
local div1 = self.rDiv
|
||
self.rDiv = (self.rDiv + 1) % 256
|
||
local div2 = self.rDiv
|
||
self.rAdd = (self.rAdd + div1) % 256
|
||
self.rSub = (self.rSub - div2 + 256) % 256
|
||
return self.rAdd
|
||
end
|
||
|
||
function SurfingMinigame:chooseSequence()
|
||
local section = self.distanceSection or 0
|
||
if section == 0x16 then
|
||
self.waveFn = 0x6a
|
||
elseif section < 0x16 then
|
||
-- Random at selection time (when waveFn==0 column generates), not frame-start
|
||
-- waveRandomValue. Fixed LFSR init otherwise phase-locks to two small sequences.
|
||
local r = self:getGBRandom()
|
||
if r ~= 0 then
|
||
self.waveFn = SEQ_STARTS[bit.band(r - 1, 0x07) + 1]
|
||
end
|
||
end
|
||
return WAVE_PATTERNS[0x00], FLAT_WATER_Y, FLAT_WATER_Y
|
||
end
|
||
|
||
-- SurfingMinigame_UpdatePikachuDistance (3-byte big-endian distance + section counter)
|
||
function SurfingMinigame:updatePikachuDistance()
|
||
local acc = (self.distanceAcc or 0) + self.speedFixed
|
||
while acc >= SECTION_ACC_MAX do
|
||
acc = acc - SECTION_ACC_MAX
|
||
self.distanceSection = (self.distanceSection or 0) + 1
|
||
end
|
||
self.distanceAcc = acc
|
||
self.distanceFixed = self.distanceSection * SECTION_ACC_MAX + acc
|
||
end
|
||
|
||
function SurfingMinigame:moveClouds()
|
||
-- SurfingMinigame_MoveClouds: add speed high byte to each cloud OAM X (8-bit wrap)
|
||
local sum = (self.cloudScrollFrac or 0) + (self.speedFixed or 0)
|
||
self.cloudScrollFrac = sum % 256
|
||
local delta = math.floor(sum / 256)
|
||
if delta == 0 then return end
|
||
for i = 1, #CLOUD_SPRITE_X_INIT do
|
||
self.cloudSpriteX[i] = (self.cloudSpriteX[i] + delta) % 256
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:pushColumn()
|
||
local pat, hl, hr
|
||
|
||
if self.routine == ROUTINE_WAIT_RESULTS then
|
||
-- CoastAfterGoal with wSurfingMinigameWaveRandomValue = 0 → flat slice
|
||
pat, hl, hr = WAVE_PATTERNS[0x00], FLAT_WATER_Y, FLAT_WATER_Y
|
||
elseif self.waveFn >= 0x74 then
|
||
pat, hl, hr = WAVE_PATTERNS["beach"], FLAT_WATER_Y, FLAT_WATER_Y
|
||
self.waveFn = 0x74
|
||
elseif self.waveFn == 0 then
|
||
pat, hl, hr = self:chooseSequence()
|
||
else
|
||
local step = WAVE_STEPS[self.waveFn]
|
||
if not step then
|
||
self.waveFn = 0
|
||
pat, hl, hr = WAVE_PATTERNS[0x00], FLAT_WATER_Y, FLAT_WATER_Y
|
||
else
|
||
pat, hl, hr = WAVE_PATTERNS[step[1]], step[2], step[3]
|
||
if step[4] == ADV then self.waveFn = self.waveFn + 1
|
||
elseif step[4] == RESET then self.waveFn = 0 end
|
||
end
|
||
end
|
||
|
||
local col = { pat = pat, hl = hl, hr = hr }
|
||
self.bgMap[mapColWrite(self)] = col
|
||
|
||
-- Shift wSurfingMinigameWaveHeight left, append new column heights
|
||
for i = 1, 18 do
|
||
self.waveHeight[i] = self.waveHeight[i + 2]
|
||
end
|
||
self.waveHeight[19] = hl
|
||
self.waveHeight[20] = hr
|
||
|
||
if not isOutroScroll(self) then
|
||
self.colTail = self.colTail + 1
|
||
self.cols[self.colTail] = col
|
||
self.cols[self.colTail - 24] = nil
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:advanceScx(deltaFixed)
|
||
local sum = (self.hScx or 0) * 256 + (self.scxFrac or 0) + deltaFixed
|
||
self.hScx = math.floor(sum / 256) % 256
|
||
self.scxFrac = sum % 256
|
||
end
|
||
|
||
-- SurfingMinigame_GenerateBGMap: new column only when hSCX changes and $f0 band changes
|
||
function SurfingMinigame:generateBgMapIfNeeded()
|
||
local h = self.hScx or 0
|
||
if h == self.scxLast then return end
|
||
self.scxLast = h
|
||
local band = bit.band(h, 0xF0)
|
||
if band == self.scxHi then return end
|
||
self.scxHi = band
|
||
self:pushColumn()
|
||
end
|
||
|
||
-- Legacy linear buffer kept for unit tests; pret uses generateBgMapIfNeeded only.
|
||
function SurfingMinigame:generateAhead()
|
||
self:generateBgMapIfNeeded()
|
||
end
|
||
|
||
local function columnAt(self, x)
|
||
local scx = displayScx(self)
|
||
return self.bgMap[mapColAtX(scx, x)]
|
||
end
|
||
|
||
-- SurfingMinigame_SetPikachuHeight: wave height with slope from prior BGMapReadBuffer
|
||
function SurfingMinigame:pikaObjectHeight(tileForSlope)
|
||
local scx = displayScx(self)
|
||
local idx = (bit.band(scx, 8) ~= 0) and 9 or 8
|
||
local h = self.waveHeight[idx]
|
||
local tile = tileForSlope or self.bgMapReadTile or 0x0b
|
||
if tile == 0x06 or tile == 0x14 then
|
||
return h - bit.band(scx, 7)
|
||
elseif tile == 0x07 then
|
||
return h + bit.band(scx, 7)
|
||
end
|
||
return h
|
||
end
|
||
|
||
local function pikaWaterY(self)
|
||
return self:pikaObjectHeight(self.bgMapReadTile)
|
||
end
|
||
|
||
-- Get water surface Y for a screen X coordinate (X=80 under Pikachu center)
|
||
function SurfingMinigame:seaY(x)
|
||
if x == 80 then
|
||
return pikaWaterY(self)
|
||
end
|
||
local col = columnAt(self, x)
|
||
if not col then return FLAT_WATER_Y end
|
||
local scx = displayScx(self)
|
||
local tile = math.floor((scx + x) / 8)
|
||
return tile % 2 == 0 and col.hl or col.hr
|
||
end
|
||
|
||
-- Chr tile at Pikachu's object height (SurfingMinigame_ReadBGMapBuffer).
|
||
function SurfingMinigame:sampleBgTileAt(scx, objectHeightPx)
|
||
local tile_col = math.floor((scx + 72) / 8)
|
||
local tile_row = math.floor(objectHeightPx / 8)
|
||
|
||
local col = self.bgMap[mapColAtX(scx, 72)]
|
||
if not col or not col.pat then return 0x0b end
|
||
|
||
local i = math.floor(tile_row / 2) + 1
|
||
if i < 1 or i > 8 then return 0x0b end
|
||
local mt = BG_METATILES[col.pat[i]]
|
||
if not mt then return 0x0b end
|
||
|
||
local sub_x = (tile_col % 2 == 0) and 0 or 1
|
||
local sub_y = (tile_row % 2 == 0) and 0 or 1
|
||
return mt[1 + sub_x + sub_y * 2] or 0x0b
|
||
end
|
||
|
||
function SurfingMinigame:getWaveTileUnderPika()
|
||
return self.bgMapReadTile or 0x0b
|
||
end
|
||
|
||
function SurfingMinigame:spawnTrickPopup(text)
|
||
table.insert(self.trickPopups, {
|
||
text = text,
|
||
x = PIKA_X,
|
||
y = self.pikaY - 14,
|
||
timer = 32,
|
||
})
|
||
end
|
||
|
||
function SurfingMinigame:calculateStuntPoints()
|
||
if self.radnessMeter <= 0 then return end
|
||
local pts = 0
|
||
local popup = "+50"
|
||
if self.trickFlags == 3 then
|
||
-- Mixed front and back flips
|
||
if self.radnessMeter >= 3 then
|
||
pts = 500
|
||
popup = "+500"
|
||
else
|
||
pts = 180
|
||
popup = "+180"
|
||
end
|
||
else
|
||
-- Same direction flips
|
||
if self.radnessMeter == 1 then
|
||
pts = 50
|
||
popup = "+50"
|
||
elseif self.radnessMeter == 2 then
|
||
pts = 150
|
||
popup = "+150"
|
||
else
|
||
pts = 350
|
||
popup = "+350"
|
||
end
|
||
end
|
||
self.radness = self.radness + pts
|
||
self:spawnTrickPopup(popup)
|
||
end
|
||
|
||
-- Slope/tile interaction matrix upon landing (SurfingMinigame_TileInteraction)
|
||
function SurfingMinigame:evaluateLanding()
|
||
local f = self.frameSet
|
||
if f >= 8 or f < 1 then
|
||
return "wipeout"
|
||
end
|
||
|
||
local tile = self:getWaveTileUnderPika()
|
||
|
||
if tile == 0x06 then -- rising slope
|
||
if f <= 3 then return "wipeout"
|
||
elseif f == 4 then return "hard"
|
||
elseif f == 5 or f == 7 then return "rough"
|
||
elseif f == 6 then return "clean"
|
||
else return "wipeout" end
|
||
|
||
elseif tile == 0x07 then -- falling slope
|
||
if f == 1 then return "rough"
|
||
elseif f == 2 then return "clean"
|
||
elseif f == 3 then return "rough"
|
||
elseif f == 4 then return "hard"
|
||
else return "wipeout" end
|
||
|
||
elseif tile == 0x12 or tile == 0x14 then -- wave face / crest
|
||
if f == 1 then return "wipeout"
|
||
elseif f == 2 or f == 7 then return "hard"
|
||
elseif f == 3 or f == 6 then return "rough"
|
||
elseif f == 4 or f == 5 then return "clean"
|
||
else return "wipeout" end
|
||
|
||
else -- flat open water (every other metatile id)
|
||
if f == 1 or f == 7 then return "wipeout"
|
||
elseif f == 2 or f == 6 then return "hard"
|
||
elseif f == 3 or f == 5 then return "rough"
|
||
elseif f == 4 then return "clean"
|
||
else return "wipeout" end
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:updateTempo()
|
||
local tier = pretTempoTier(self.speedFixed or SPEED_INITIAL)
|
||
local targetPitch = TEMPO_TIERS[tier] or 1.0
|
||
if self.currentPitch ~= targetPitch then
|
||
self.currentPitch = targetPitch
|
||
if Music and Music.setPitch then
|
||
Music.setPitch(self.currentPitch)
|
||
end
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:resetTempo()
|
||
self.currentPitch = 1.0
|
||
if Music and Music.setPitch then
|
||
Music.setPitch(1.0)
|
||
end
|
||
end
|
||
|
||
-- Pret RunDelayTimer: count down routineTimer; return true when it hits zero.
|
||
local function outroDelayExpired(self)
|
||
if self.routineTimer > 0 then
|
||
self.routineTimer = self.routineTimer - 1
|
||
return false
|
||
end
|
||
return true
|
||
end
|
||
|
||
function SurfingMinigame:beginResultsCard()
|
||
self.showResultsCard = true
|
||
self.outroLines = { hp = false, rad = false, total = false, hiScore = false }
|
||
self:initResultsPikachu()
|
||
self.speedFixed = 0
|
||
-- DrawResultsScreen clears cloud OAM (sprites 5–13); hide parallax clouds on the beach card
|
||
self.cloudSpriteX = nil
|
||
end
|
||
|
||
-- SurfingMinigame_InitResultsPikachu: flat results pose at shore Y, reset bob counter
|
||
function SurfingMinigame:initResultsPikachu()
|
||
self.pikaState = PIKA_STATE_INIT_RESULTS
|
||
self.frameSet = 4 -- flat ride (visible pose; pret writes frameset $0f to anim struct)
|
||
self.pikaY = FLAT_WATER_Y - PIKA_SPRITE_OFFSET
|
||
self.pikaSubY = 0
|
||
self.pikaYOffset = 0
|
||
self.resultsBobTimer = 0
|
||
end
|
||
|
||
-- SurfingMinigame_UpdateResultsPikachu (hi-score path only sets PIKA_STATE_RESULTS in pret)
|
||
function SurfingMinigame:updateResultsPikachu()
|
||
self.resultsBobTimer = (self.resultsBobTimer or 0) + 2
|
||
local phase = bit.band(self.resultsBobTimer, RESULTS_BOB_CYCLE - 1)
|
||
if phase >= RESULTS_BOB_START then
|
||
self.pikaYOffset = math.floor(
|
||
math.sin((phase - RESULTS_BOB_START) / (RESULTS_BOB_CYCLE / 2) * math.pi * 2) * RESULTS_BOB_AMP
|
||
)
|
||
else
|
||
self.pikaYOffset = 0
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:drawResultsPikachu()
|
||
if not (self.ob and self.oq) then return end
|
||
local cx, cy = 80, self.pikaY + (self.pikaYOffset or 0)
|
||
self.pikaScreenY = cy
|
||
local toggle = math.floor(self.t / 8) % 2 + 1
|
||
local base = RESULTS_PIKA_BASES[toggle]
|
||
if not self.oq[base] then
|
||
base = ANGLE_BASES[4][toggle]
|
||
end
|
||
self:draw3x3(base, cx, cy, false, false)
|
||
end
|
||
|
||
function SurfingMinigame:updateRiding()
|
||
local tile = self.bgMapReadTile or 0x0b
|
||
local subX = bit.band(displayScx(self), 7)
|
||
|
||
-- SurfingMinigame_TryStartJump (before SpeedUpPikachu; uses pre-accel speed)
|
||
if (subX >= 3 and subX <= 4) and tile == 0x14 and getSpeedDividedBy32(self.speedFixed) >= SPEED_JUMP_MIN_DIV32 then
|
||
self.pikaState = PIKA_STATE_JUMPING
|
||
self.jumpArcMagnitude = getSpeedDividedBy32(self.speedFixed)
|
||
self.jumpArcFraction = 0
|
||
self.pikaSubY = 0
|
||
self.jumpDescending = false
|
||
self.radnessMeter = 0
|
||
self.trickFlags = 0
|
||
self.rotCountLeft = 0
|
||
self.rotCountRight = 0
|
||
Sound.play(self.game.data, "Ledge_Jump")
|
||
-- SurfingMinigame_UpdateSurfingFrame still runs on .startedJump
|
||
if subX >= 3 and subX <= 4 then
|
||
if tile == 0x06 or tile == 0x14 then
|
||
self.frameSet = 6 + (self.boardAngleOffset - 1)
|
||
elseif tile == 0x07 then
|
||
self.frameSet = 2 + (self.boardAngleOffset - 1)
|
||
end
|
||
self.frameSet = math.max(1, math.min(14, self.frameSet))
|
||
end
|
||
return
|
||
end
|
||
|
||
-- SurfingMinigame_UpdateSurfingFrame (only updates frame when subX is 3..4)
|
||
if subX >= 3 and subX <= 4 then
|
||
if tile == 0x06 or tile == 0x14 then
|
||
self.frameSet = 6 + (self.boardAngleOffset - 1)
|
||
elseif tile == 0x07 then
|
||
self.frameSet = 2 + (self.boardAngleOffset - 1)
|
||
else
|
||
self:updateBoardAngle()
|
||
self.frameSet = 4
|
||
end
|
||
self.frameSet = math.max(1, math.min(14, self.frameSet))
|
||
end
|
||
|
||
-- SurfingMinigame_SpeedUpPikachu (+2/256 = +1/128 per frame up to max 512 = 2.0)
|
||
if self.speedFixed < SPEED_MAX then
|
||
self.speedFixed = math.min(SPEED_MAX, self.speedFixed + SPEED_ACCEL)
|
||
end
|
||
self:updateTempo()
|
||
|
||
-- Water spray every 4 frames
|
||
self.sprayTimer = self.sprayTimer + 1
|
||
if self.sprayTimer % 4 == 0 then
|
||
table.insert(self.waterSprays, { x = PIKA_X, y = self.pikaY, timer = 4 })
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:updateBoardAngle()
|
||
self.boardAngleTimer = (self.boardAngleTimer or 0) + 1
|
||
if self.boardAngleTimer % 8 ~= 0 then return end
|
||
if self.boardAngleDecreasing then
|
||
if self.boardAngleOffset > 0 then
|
||
self.boardAngleOffset = self.boardAngleOffset - 1
|
||
else
|
||
self.boardAngleDecreasing = false
|
||
end
|
||
else
|
||
if self.boardAngleOffset < 2 then
|
||
self.boardAngleOffset = self.boardAngleOffset + 1
|
||
else
|
||
self.boardAngleDecreasing = true
|
||
end
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:handleLanding()
|
||
local result = self:evaluateLanding()
|
||
if result == "wipeout" then
|
||
self.pikaState = PIKA_STATE_CRASHED
|
||
self.crashTimer = CRASH_FRAMES
|
||
self.speedFixed = SPEED_INITIAL
|
||
self.frameSet = 4
|
||
Sound.play(self.game.data, "Faint_Fall")
|
||
else
|
||
if result == "rough" then
|
||
self.speedFixed = reduceSpeedBy64(self.speedFixed)
|
||
elseif result == "hard" then
|
||
self.speedFixed = reduceSpeedBy128(self.speedFixed)
|
||
end
|
||
if self.routine == ROUTINE_RUN_GAME then
|
||
self:calculateStuntPoints()
|
||
end
|
||
self.pikaState = PIKA_STATE_LANDING
|
||
self.landingTimer = LANDING_SPLASH_FRAMES
|
||
self.frameSet = 4
|
||
Sound.play(self.game.data, "Cut")
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:updateJumping()
|
||
-- SurfingMinigame_DPadAction (hJoy5 sampled every 2 frames via GetJoypad)
|
||
if self.routine == ROUTINE_RUN_GAME then
|
||
if self.joy5Left then
|
||
self.rotCountRight = 0
|
||
self.rotCountLeft = self.rotCountLeft + 1
|
||
if self.rotCountLeft >= FLIP_LEFT_FRAMES then
|
||
self.rotCountLeft = 0
|
||
self.radnessMeter = math.min(RADNESS_METER_MAX, self.radnessMeter + 1)
|
||
self.trickFlags = bit.bor(self.trickFlags, 1)
|
||
Sound.play(self.game.data, "Tink")
|
||
end
|
||
if self.frameSet >= 14 then
|
||
self.frameSet = 1
|
||
else
|
||
self.frameSet = self.frameSet + 1
|
||
end
|
||
elseif self.joy5Right then
|
||
self.rotCountLeft = 0
|
||
self.rotCountRight = self.rotCountRight + 1
|
||
if self.rotCountRight >= FLIP_RIGHT_FRAMES then
|
||
self.rotCountRight = 0
|
||
self.radnessMeter = math.min(RADNESS_METER_MAX, self.radnessMeter + 1)
|
||
self.trickFlags = bit.bor(self.trickFlags, 2)
|
||
Sound.play(self.game.data, "Tink")
|
||
end
|
||
if self.frameSet <= 1 then
|
||
self.frameSet = 14
|
||
else
|
||
self.frameSet = self.frameSet - 1
|
||
end
|
||
end
|
||
end
|
||
|
||
-- SurfingMinigame_UpdatePikachuHeight (arc delta before velocity each phase)
|
||
if not self.jumpDescending then
|
||
if (self.jumpArcMagnitude or 0) == 0 and (self.jumpArcFraction or 0) == 0 then
|
||
self.jumpDescending = true
|
||
else
|
||
applyJumpArcDelta(self, -128) -- -0.5 px/frame in 8.8 fixed
|
||
applyJumpVerticalDelta(self, -1)
|
||
end
|
||
else
|
||
local waveY = pikaSpriteYFromObjectHeight(self.pikaObjectHeightPx)
|
||
if self.pikaY >= waveY then
|
||
self.pikaY = waveY
|
||
self.pikaSubY = 0
|
||
self:handleLanding()
|
||
return
|
||
end
|
||
|
||
applyJumpArcDelta(self, 128) -- +0.5 px/frame in 8.8 fixed
|
||
applyJumpVerticalDelta(self, 1)
|
||
|
||
if self.pikaY >= waveY then
|
||
self.pikaY = waveY
|
||
self.pikaSubY = 0
|
||
self:handleLanding()
|
||
end
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:updateLanding()
|
||
self.landingTimer = (self.landingTimer or 0) - 1
|
||
|
||
-- pikaY already updated by SetPikachuHeight in tick (pre-scroll object height).
|
||
|
||
-- Sine wave splash offset (FIELD_C 0..$20 by +4/frame)
|
||
self.pikaYOffset = math.floor(math.sin((32 - math.max(0, self.landingTimer)) / 32 * math.pi * 2) * 4)
|
||
if self.landingTimer % 4 == 0 then
|
||
table.insert(self.waterSprays, { x = PIKA_X, y = self.pikaY, timer = 4 })
|
||
end
|
||
|
||
if self.landingTimer <= 0 then
|
||
self.pikaYOffset = 0
|
||
self.pikaState = PIKA_STATE_RIDING
|
||
self.frameSet = 4
|
||
end
|
||
end
|
||
|
||
function SurfingMinigame:updateCrashed()
|
||
self.crashTimer = self.crashTimer - 1
|
||
self:resetTempo()
|
||
-- pikaY already updated by SetPikachuHeight in tick.
|
||
if self.crashTimer <= 0 then
|
||
self.pikaState = PIKA_STATE_RIDING
|
||
self.frameSet = 4
|
||
end
|
||
end
|
||
|
||
-- Single Game Boy hardware VBlank cycle tick (59.7275 Hz)
|
||
function SurfingMinigame:tick()
|
||
local input = self.game and self.game.input
|
||
self.t = self.t + 1
|
||
self.rDiv = (self.rDiv + RDIV_PER_FRAME) % 256
|
||
|
||
-- Title Screen State (auto-advances when intro Pikachu walks off-screen)
|
||
if self.routine == ROUTINE_TITLE then
|
||
if self.t % 2 == 0 then
|
||
self.introPikaX = self.introPikaX + 1
|
||
end
|
||
if self.introPikaX >= 192 then
|
||
self:startFromTitle()
|
||
end
|
||
return
|
||
end
|
||
|
||
-- SurfingPikachu_GetJoypad_3FrameBuffer: hJoy5 = hJoyHeld when hFrameCounter==0, then reload $2;
|
||
-- VBlank decrements counter → 1 sample frame + 2 blank frames (3-frame duty cycle).
|
||
if self.routine == ROUTINE_RUN_GAME then
|
||
if (self.joyFrameCounter or 0) == 0 then
|
||
self.joy5Left = input and input:isDown("left")
|
||
self.joy5Right = input and input:isDown("right")
|
||
self.joyFrameCounter = JOY_FRAME_RELOAD
|
||
else
|
||
self.joy5Left = false
|
||
self.joy5Right = false
|
||
end
|
||
else
|
||
self.joy5Left = false
|
||
self.joy5Right = false
|
||
self.joyFrameCounter = 0
|
||
end
|
||
|
||
-- Update trick popups
|
||
for i = #self.trickPopups, 1, -1 do
|
||
local p = self.trickPopups[i]
|
||
p.y = p.y - 0.5
|
||
p.timer = p.timer - 1
|
||
if p.timer <= 0 then table.remove(self.trickPopups, i) end
|
||
end
|
||
|
||
-- Update water sprays
|
||
for i = #self.waterSprays, 1, -1 do
|
||
local s = self.waterSprays[i]
|
||
s.timer = s.timer - 1
|
||
if s.timer <= 0 then table.remove(self.waterSprays, i) end
|
||
end
|
||
|
||
-- Slide START banner during RunGame (pret animates it while gameplay runs)
|
||
if self.routine == ROUTINE_RUN_GAME and self.startBannerX > 80 then
|
||
self.startBannerX = math.max(80, self.startBannerX - 4)
|
||
end
|
||
|
||
-- Routine state machine
|
||
if self.routine == ROUTINE_START_GAME then
|
||
-- SurfingMinigame_StartGame: spawn banner, inc routine; RunGame begins next frame
|
||
self.routine = ROUTINE_RUN_GAME
|
||
return
|
||
elseif self.routine == ROUTINE_RUN_GAME then
|
||
-- SurfingMinigame_RunGame: distance check first (cp $18)
|
||
if (self.distanceSection or 0) >= TOTAL_SECTIONS then
|
||
self.distanceSection = TOTAL_SECTIONS
|
||
self.routine = ROUTINE_WAIT_RESULTS
|
||
self.routineTimer = COAST_FRAMES
|
||
self.scxHi = bit.band(self.hScx or 0, 0xF0)
|
||
self.scxLast = self.hScx or 0
|
||
self.waveFn = 0
|
||
self:resetTempo()
|
||
return
|
||
end
|
||
|
||
-- HP dead check before frame logic (pret or [hl] on wSurfingMinigamePikachuHP)
|
||
if (self.hp or 0) <= 0 then
|
||
self.routine = ROUTINE_GAME_OVER
|
||
self.routineTimer = GAME_OVER_DELAY
|
||
self.speedFixed = 0
|
||
self.ohNoBanner = true
|
||
Sound.play(self.game and self.game.data, "Faint_Fall")
|
||
return
|
||
end
|
||
|
||
self.waveRandomValue = self:getGBRandom()
|
||
|
||
-- Pret RunGame order: SetPikachuHeight, ReadBGMapBuffer, Scroll, Distance, Deduct1HP
|
||
local objectHeight = self:pikaObjectHeight(self.bgMapReadTile)
|
||
self.pikaObjectHeightPx = objectHeight
|
||
if self.pikaState ~= PIKA_STATE_JUMPING then
|
||
self.pikaY = pikaSpriteYFromObjectHeight(objectHeight)
|
||
self.pikaSubY = 0
|
||
end
|
||
local preScrollScx = displayScx(self)
|
||
self.bgMapReadTile = self:sampleBgTileAt(preScrollScx, objectHeight)
|
||
|
||
self:advanceScx(BG_SCROLL_STEP)
|
||
self:generateBgMapIfNeeded()
|
||
|
||
self:updatePikachuDistance()
|
||
self.scrollFixed = self.distanceFixed
|
||
|
||
-- SurfingMinigame_Deduct1HP (after UpdatePikachuDistance)
|
||
self.hp = self.hp - 1
|
||
|
||
-- Update Pikachu by state
|
||
if self.pikaState == PIKA_STATE_RIDING then
|
||
self:updateRiding()
|
||
elseif self.pikaState == PIKA_STATE_JUMPING then
|
||
self:updateJumping()
|
||
elseif self.pikaState == PIKA_STATE_LANDING then
|
||
self:updateLanding()
|
||
elseif self.pikaState == PIKA_STATE_CRASHED then
|
||
self:updateCrashed()
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_WAIT_RESULTS then
|
||
if self.routineTimer > 0 then
|
||
-- RunDelayTimer then CoastAfterGoal (192 coast frames, not 193)
|
||
self.routineTimer = self.routineTimer - 1
|
||
self:advanceScx(COAST_SPEED_FIXED)
|
||
self:generateBgMapIfNeeded()
|
||
self:resetTempo()
|
||
end
|
||
|
||
if self.pikaState == PIKA_STATE_JUMPING then
|
||
self:updateJumping()
|
||
elseif self.pikaState == PIKA_STATE_LANDING then
|
||
self:updateLanding()
|
||
elseif self.pikaState == PIKA_STATE_CRASHED then
|
||
self:updateCrashed()
|
||
else
|
||
local targetY = self:seaY(80)
|
||
self.pikaY = math.floor(targetY) - 16
|
||
self.pikaSubY = 0
|
||
self.frameSet = 4
|
||
end
|
||
|
||
if self.routineTimer <= 0 then
|
||
-- pret .doneDelay: enter results scroll immediately (no wait for landing/crash)
|
||
self.routine = ROUTINE_SCROLL_RESULTS
|
||
self.hScx = OUTRO_SCROLL_START
|
||
self.scxFrac = 0
|
||
self.scxHi = 0
|
||
self.scxLast = 0
|
||
self.waveFn = 0x72
|
||
self.pikaState = PIKA_STATE_GAME_END
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_SCROLL_RESULTS then
|
||
if (self.hScx or 0) <= 0 then
|
||
self.routine = ROUTINE_DRAW_RESULTS
|
||
self.pikaState = PIKA_STATE_INIT_RESULTS
|
||
else
|
||
self.hScx = self.hScx - OUTRO_SCROLL_STEP
|
||
self:generateBgMapIfNeeded()
|
||
local targetY = self:seaY(80)
|
||
self.pikaY = math.floor(targetY) - 16
|
||
self.pikaSubY = 0
|
||
self.frameSet = 4
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_DRAW_RESULTS then
|
||
-- DrawResultsScreenAndWait: one-shot static beach tilemap + textbox frame
|
||
self:beginResultsCard()
|
||
self.routineTimer = 32
|
||
self.routine = ROUTINE_WRITE_HP_LEFT
|
||
|
||
elseif self.routine == ROUTINE_WRITE_HP_LEFT then
|
||
if outroDelayExpired(self) then
|
||
self.outroLines.hp = true
|
||
self.routineTimer = 64
|
||
self.routine = ROUTINE_WRITE_RADNESS
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_WRITE_RADNESS then
|
||
if outroDelayExpired(self) then
|
||
self.outroLines.rad = true
|
||
self.routineTimer = 64
|
||
self.routine = ROUTINE_WRITE_TOTAL
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_WRITE_TOTAL then
|
||
if outroDelayExpired(self) then
|
||
self.outroLines.total = true
|
||
self.routineTimer = 64
|
||
self.routine = ROUTINE_ADD_HP_TOTAL
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_ADD_HP_TOTAL then
|
||
if not outroDelayExpired(self) then
|
||
-- waiting before tally starts
|
||
elseif self.hp > 0 then
|
||
local step = math.min(self.hp, 99)
|
||
self.hp = self.hp - step
|
||
self.totalScore = self.totalScore + step
|
||
Sound.play(self.game and self.game.data, "Press_AB")
|
||
else
|
||
self.routineTimer = 64
|
||
self.routine = ROUTINE_ADD_RAD_TOTAL
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_ADD_RAD_TOTAL then
|
||
if not outroDelayExpired(self) then
|
||
-- waiting before tally starts
|
||
elseif self.radness > 0 then
|
||
local step = math.min(self.radness, 99)
|
||
self.radness = self.radness - step
|
||
self.totalScore = self.totalScore + step
|
||
Sound.play(self.game and self.game.data, "Press_AB")
|
||
else
|
||
self.newRecord = self.totalScore > ((self.game and self.game.save and self.game.save.surfingHighScore) or 0)
|
||
if self.newRecord then
|
||
if self.game and self.game.save then self.game.save.surfingHighScore = self.totalScore end
|
||
self.outroLines.hiScore = true
|
||
self.pikaState = PIKA_STATE_RESULTS
|
||
Sound.play(self.game and self.game.data, "Get_Item1")
|
||
Sound.playPikaCry(self.game and self.game.data, 34)
|
||
else
|
||
Sound.playPikaCry(self.game and self.game.data, 28)
|
||
end
|
||
self.routineTimer = GAME_OVER_DELAY
|
||
self.routine = ROUTINE_WAIT_LAST
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_WAIT_LAST then
|
||
if outroDelayExpired(self) then
|
||
self.routine = ROUTINE_EXIT_ON_PRESS_A
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_EXIT_ON_PRESS_A then
|
||
if input and input:wasPressed("a") then
|
||
if self.game and self.game.stack then self.game.stack:pop() end
|
||
if self.onDone then self.onDone(self.totalScore) end
|
||
end
|
||
|
||
elseif self.routine == ROUTINE_GAME_OVER then
|
||
self.routineTimer = self.routineTimer - 1
|
||
if self.routineTimer <= 0 and input and input:wasPressed("a") then
|
||
if self.game and self.game.stack then self.game.stack:pop() end
|
||
if self.onDone then self.onDone(0) end
|
||
end
|
||
end
|
||
|
||
if self.showResultsCard then
|
||
if self.pikaState == PIKA_STATE_RESULTS then
|
||
self:updateResultsPikachu()
|
||
else
|
||
self.pikaYOffset = 0
|
||
end
|
||
end
|
||
|
||
-- Pret SurfingPikachuLoop calls MoveClouds every frame while gameplay is active
|
||
if self.routine == ROUTINE_RUN_GAME
|
||
or self.routine == ROUTINE_WAIT_RESULTS
|
||
or self.routine == ROUTINE_GAME_OVER then
|
||
self:moveClouds()
|
||
end
|
||
|
||
-- hFrameCounter decrements after game logic (VBlank)
|
||
if self.routine == ROUTINE_RUN_GAME and (self.joyFrameCounter or 0) > 0 then
|
||
self.joyFrameCounter = self.joyFrameCounter - 1
|
||
end
|
||
end
|
||
|
||
-- Decoupled timestep accumulator for modern multi-refresh-rate displays
|
||
function SurfingMinigame:update(dt)
|
||
if not dt then
|
||
self:tick()
|
||
return
|
||
end
|
||
|
||
-- Accumulate real-world time
|
||
self.tickAccumulator = (self.tickAccumulator or 0) + dt
|
||
|
||
-- Exact Game Boy Color framerate: 59.7275 Hz (approx 0.0167427 seconds per frame)
|
||
local gbTickRate = 1 / 59.7275
|
||
|
||
-- Process as many physical hardware frames as necessary
|
||
while self.tickAccumulator >= gbTickRate do
|
||
self:tick()
|
||
self.tickAccumulator = self.tickAccumulator - gbTickRate
|
||
end
|
||
end
|
||
|
||
-- Draw scrolling wave background with authentic GPU shader HBlank wave distortion
|
||
function SurfingMinigame:drawBackground()
|
||
local scx = displayScx(self)
|
||
|
||
local function renderTiles()
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
if not self.bg then return end
|
||
for i = 0, 10 do
|
||
local col = self.bgMap[mapColScreen(scx, i)]
|
||
local x = i * 16 - (scx % 16)
|
||
if col then
|
||
for row = 1, 8 do
|
||
local mt = BG_METATILES[col.pat[row]]
|
||
if mt then
|
||
local y = (row - 1) * 16
|
||
local function drawTile(tid, tx, ty)
|
||
if tid ~= 0x00 and self.tq[tid] then
|
||
love.graphics.draw(self.bg, self.tq[tid], tx, ty)
|
||
end
|
||
end
|
||
drawTile(mt[1], x, y)
|
||
drawTile(mt[2], x + 8, y)
|
||
drawTile(mt[3], x, y + 8)
|
||
drawTile(mt[4], x + 8, y + 8)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if self.bgCanvas and self.waveShader and not isOutroScroll(self) and not self.showResultsCard
|
||
and love.graphics.setCanvas and love.graphics.getCanvas then
|
||
-- 1. Capture the framework's active canvas
|
||
local prevCanvas = love.graphics.getCanvas()
|
||
|
||
love.graphics.setCanvas(self.bgCanvas)
|
||
-- 2. Clear with transparency (0 alpha) so the sky remains empty
|
||
love.graphics.clear(0, 0, 0, 0)
|
||
renderTiles()
|
||
|
||
-- 3. Restore the framework's canvas before drawing!
|
||
love.graphics.setCanvas(prevCanvas)
|
||
|
||
-- 4. Safely capture and restore the shader
|
||
local prevShader = love.graphics.getShader and love.graphics.getShader()
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.setShader(self.waveShader)
|
||
|
||
if self.waveShader.hasUniform and self.waveShader:hasUniform("u_time") then
|
||
self.waveShader:send("u_time", self.t / 60.0)
|
||
end
|
||
if self.waveShader.hasUniform and self.waveShader:hasUniform("u_water_line") then
|
||
self.waveShader:send("u_water_line", 48.0 / 144.0)
|
||
end
|
||
|
||
love.graphics.draw(self.bgCanvas, 0, 0)
|
||
love.graphics.setShader(prevShader)
|
||
else
|
||
renderTiles()
|
||
end
|
||
end
|
||
|
||
-- Draw intro title Pikachu (SurfingPikachu1Graphics3 via .IntroPikachu OAM)
|
||
function SurfingMinigame:drawIntroPikachu(cx, cy)
|
||
if not (self.intro and self.iq) then return end
|
||
local frame = math.floor(self.t / 7) % 4 + 1
|
||
local vramBase = INTRO_PIKA_FRAME_BASE[frame]
|
||
local sheetBase = vramBase - 0x80
|
||
for _, sp in ipairs(INTRO_PIKA_OAM) do
|
||
local tileId = sheetBase + sp.tile
|
||
local q = self.iq[tileId]
|
||
if q then
|
||
local x = cx + sp.dx + (sp.xflip and 8 or 0)
|
||
local y = cy + sp.dy
|
||
if sp.xflip then
|
||
love.graphics.draw(self.intro, q, x, y, 0, -1, 1)
|
||
else
|
||
love.graphics.draw(self.intro, q, x, y)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Draw 3x3 Pikachu sprite (24x24 px centered at cx, cy)
|
||
function SurfingMinigame:draw3x3(baseTile, cx, cy, flipX, flipY)
|
||
local sx = flipX and -1 or 1
|
||
local sy = flipY and -1 or 1
|
||
for r = 0, 2 do
|
||
for c = 0, 2 do
|
||
local tileId = baseTile + r * 16 + c
|
||
local q = self.oq[tileId]
|
||
if q then
|
||
local colIdx = flipX and (2 - c) or c
|
||
local rowIdx = flipY and (2 - r) or r
|
||
local dx = (cx - 12) + colIdx * 8 + (flipX and 8 or 0)
|
||
local dy = (cy - 12) + rowIdx * 8 + (flipY and 8 or 0)
|
||
love.graphics.draw(self.ob, q, dx, dy, 0, sx, sy)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Draw HUD status bar with inverted progress marker (moving right-to-left towards beach)
|
||
function SurfingMinigame:drawHUD()
|
||
-- White background in bottom 16 rows
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.rectangle("fill", 0, BG_HEIGHT, 160, 16)
|
||
|
||
-- Window tilemap (SurfingPikachuMinigame_DrawStaticTilemapLayout):
|
||
-- row 0 cols 1-2: $15,$16 island; row 1 cols 1-9: $17,$18,$19×7;
|
||
-- row 1 cols 12-13: $1b,$1c "HP:"; digits are OAM sprites at X=$80.
|
||
if self.bg and self.tq then
|
||
love.graphics.draw(self.bg, self.tq[0x15], 8, BG_HEIGHT)
|
||
love.graphics.draw(self.bg, self.tq[0x16], 16, BG_HEIGHT)
|
||
|
||
love.graphics.draw(self.bg, self.tq[0x17], 8, BG_HEIGHT + 8)
|
||
love.graphics.draw(self.bg, self.tq[0x18], 16, BG_HEIGHT + 8)
|
||
for i = 1, 7 do
|
||
love.graphics.draw(self.bg, self.tq[0x19], 16 + i * 8, BG_HEIGHT + 8)
|
||
end
|
||
love.graphics.draw(self.bg, self.tq[0x1b], 96, BG_HEIGHT + 8)
|
||
love.graphics.draw(self.bg, self.tq[0x1c], 104, BG_HEIGHT + 8)
|
||
end
|
||
|
||
-- Mini-Pikachu progress marker (tile $fe)
|
||
-- Game Boy: initial OAM X = $50 (80) = screen X 72, decrements by 2 per section.
|
||
local markerStartX = 72 -- OAM X $50 (80) minus 8-pixel OAM offset
|
||
local markerX = markerStartX - (self.distanceSection or 0) * 2
|
||
if self.ob and self.oq and self.oq[0xfe] then
|
||
love.graphics.draw(self.ob, self.oq[0xfe], markerX, BG_HEIGHT + 6)
|
||
end
|
||
|
||
-- 4 HP countdown digits (OAM X $80..$98)
|
||
local s = string.format("%04d", math.max(0, math.floor(self.hp)))
|
||
for i = 1, 4 do
|
||
local d = tonumber(s:sub(i, i)) or 0
|
||
if self.ob and self.oq and self.oq[0xd0 + d] then
|
||
love.graphics.draw(self.ob, self.oq[0xd0 + d], 128 + (i - 1) * 8, BG_HEIGHT + 8)
|
||
else
|
||
Font.draw(tostring(d), 128 + (i - 1) * 8, BG_HEIGHT + 8)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Draw beach outro results scene
|
||
function SurfingMinigame:drawResultsOutro()
|
||
-- Draw beach outro tilemap in rows 6..15
|
||
for r = 1, 10 do
|
||
for c = 1, 20 do
|
||
local tId = BEACH_OUTRO[r][c]
|
||
if tId and self.tq and self.tq[tId] then
|
||
love.graphics.draw(self.bg, self.tq[tId], (c - 1) * 8, (r + 5) * 8)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Fill interior with white
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.rectangle("fill", 16, 16, 128, 56)
|
||
|
||
-- Draw the textbox frame using original Game Boy border tiles (rows 1..9, Y=8..72)
|
||
local function drawBoxRow(rowIdx, leftTile, interiorTile, rightTile)
|
||
local y = rowIdx * 8
|
||
if self.tq[leftTile] then
|
||
love.graphics.draw(self.bg, self.tq[leftTile], 8, y)
|
||
end
|
||
if interiorTile and self.tq[interiorTile] then
|
||
for colIdx = 2, 17 do
|
||
love.graphics.draw(self.bg, self.tq[interiorTile], colIdx * 8, y)
|
||
end
|
||
end
|
||
if self.tq[rightTile] then
|
||
love.graphics.draw(self.bg, self.tq[rightTile], 144, y)
|
||
end
|
||
end
|
||
|
||
drawBoxRow(1, 0x3b, 0x40, 0x3c)
|
||
for r = 2, 8 do
|
||
drawBoxRow(r, 0x3f, nil, 0x3f)
|
||
end
|
||
drawBoxRow(9, 0x3d, 0x40, 0x3e)
|
||
|
||
-- Text lines appear as each Write* routine fires in pret
|
||
if self.outroLines.hp then
|
||
Font.draw(Strings("HP Left"), 16, 16)
|
||
Font.draw(string.format("%04d", self.hp), 80, 16)
|
||
Font.draw(Strings("Pts"), 120, 16)
|
||
end
|
||
|
||
if self.outroLines.rad then
|
||
Font.draw(Strings("Radness"), 16, 32)
|
||
Font.draw(string.format("%04d", self.radness), 80, 32)
|
||
Font.draw(Strings("Pts"), 120, 32)
|
||
end
|
||
|
||
if self.outroLines.total then
|
||
Font.draw(Strings("Total"), 16, 48)
|
||
Font.draw(string.format("%04d", self.totalScore), 80, 48)
|
||
Font.draw(Strings("Pts"), 120, 48)
|
||
end
|
||
|
||
if self.outroLines.hiScore then
|
||
Font.draw(Strings("Hi-Score!!"), 48, 64)
|
||
end
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
end
|
||
|
||
-- Draw minigame Title Screen ("Pikachu's Beach")
|
||
function SurfingMinigame:drawTitleScreen()
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||
|
||
-- 1. Draw authentic 160x144 composite title background from ROM
|
||
if self.titleBg then
|
||
love.graphics.draw(self.titleBg, 0, 0)
|
||
else
|
||
Font.draw("PIKACHU'S BEACH", 20, 32)
|
||
end
|
||
|
||
-- 2. Intro Pikachu paddling out (SurfingPikachu1Graphics3 / surf_1c.png)
|
||
self:drawIntroPikachu(self.introPikaX, FLAT_WATER_Y)
|
||
end
|
||
|
||
function SurfingMinigame:draw()
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||
|
||
if self.routine == ROUTINE_TITLE then
|
||
self:drawTitleScreen()
|
||
return
|
||
end
|
||
|
||
if self.showResultsCard then
|
||
self:drawResultsOutro()
|
||
self:drawResultsPikachu()
|
||
self:drawHUD()
|
||
return
|
||
end
|
||
|
||
-- Draw scrolling BG waves
|
||
self:drawBackground()
|
||
|
||
-- Parallax clouds (SurfingMinigame_MoveClouds: 9 OAM sprites, 8-bit X wrap)
|
||
if self.ob and self.oq and self.cloudSpriteX then
|
||
local wideTiles = { 0xec, 0xed, 0xed, 0xee, 0xef }
|
||
for i, tid in ipairs(wideTiles) do
|
||
local x = self.cloudSpriteX[i]
|
||
if x < 168 then
|
||
love.graphics.draw(self.ob, self.oq[tid], x, 12)
|
||
end
|
||
end
|
||
local narrowTiles = { 0xec, 0xed, 0xee, 0xef }
|
||
for i, tid in ipairs(narrowTiles) do
|
||
local x = self.cloudSpriteX[i + 5]
|
||
if x < 168 then
|
||
love.graphics.draw(self.ob, self.oq[tid], x, 20)
|
||
end
|
||
end
|
||
end
|
||
-- Helper function to draw OAM multi-sprite composite objects with palette and X-flipping
|
||
function self:drawOAMSprites(sprites, ox, oy)
|
||
if not (self.ob and self.oq) then return end
|
||
love.graphics.setColor(0.5, 0.5, 0.5, 1) -- OAM_PAL1 maps shade 1 to shade 2 (Sea Blue)
|
||
for _, sp in ipairs(sprites) do
|
||
local q = self.oq[sp.tile]
|
||
if q then
|
||
local x = ox + sp.dx
|
||
local y = oy + sp.dy
|
||
if sp.xflip then
|
||
love.graphics.draw(self.ob, q, x + 8, y, 0, -1, 1)
|
||
else
|
||
love.graphics.draw(self.ob, q, x, y)
|
||
end
|
||
end
|
||
end
|
||
love.graphics.setColor(1, 1, 1, 1)
|
||
end
|
||
|
||
-- Draw water spray sprites on trailing edge of surfboard
|
||
for _, s in ipairs(self.waterSprays) do
|
||
self:drawOAMSprites(OAM_WATER_SPRAY, s.x, s.y)
|
||
end
|
||
|
||
-- Draw Pikachu
|
||
local cx = 80
|
||
local cy = self.pikaY + (self.pikaYOffset or 0)
|
||
self.pikaScreenY = cy
|
||
|
||
if self.pikaState == PIKA_STATE_CRASHED then
|
||
-- Empty surfboard (3 tiles at bottom)
|
||
if self.ob and self.oq then
|
||
love.graphics.draw(self.ob, self.oq[0x98], cx - 12, cy + 4)
|
||
love.graphics.draw(self.ob, self.oq[0x99], cx - 4, cy + 4)
|
||
love.graphics.draw(self.ob, self.oq[0x9a], cx + 4, cy + 4)
|
||
end
|
||
-- Multi-tile splash animation (.SmallSplash then .LargeSplash)
|
||
local elapsed = 60 - (self.crashTimer or 0)
|
||
if elapsed < 16 then
|
||
self:drawOAMSprites(OAM_SMALL_SPLASH, cx, cy + 4)
|
||
else
|
||
self:drawOAMSprites(OAM_LARGE_SPLASH, cx, cy + 4)
|
||
end
|
||
else
|
||
local angleIdx = ((self.frameSet - 1) % 7) + 1
|
||
local isFlipped = self.frameSet > 7
|
||
local toggle = math.floor(self.t / 8) % 2 + 1
|
||
local base = ANGLE_BASES[angleIdx][toggle]
|
||
self:draw3x3(base, cx, cy, isFlipped, isFlipped)
|
||
end
|
||
|
||
-- Draw trick popups
|
||
for _, p in ipairs(self.trickPopups) do
|
||
Font.draw(p.text, p.x, p.y)
|
||
end
|
||
|
||
-- "START" banner (slides in during early RunGame frames)
|
||
if self.startBannerX > 80 then
|
||
if self.ob and self.oq then
|
||
for r = 0, 1 do
|
||
for c = 0, 5 do
|
||
local tid = 0xe0 + r * 16 + c
|
||
love.graphics.draw(self.ob, self.oq[tid], self.startBannerX - 24 + c * 8, 64 + r * 8)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- "Oh no.." banner on Game Over
|
||
if self.ohNoBanner then
|
||
if self.ob and self.oq then
|
||
for r = 0, 1 do
|
||
for c = 0, 5 do
|
||
local tid = 0xca + r * 16 + c
|
||
love.graphics.draw(self.ob, self.oq[tid], 80 - 24 + c * 8, 64 + r * 8)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Draw HUD (Progress track, HP digits)
|
||
self:drawHUD()
|
||
end
|
||
|
||
function SurfingMinigame:sgbPalettes(game)
|
||
local P = require("src.render.PaletteFX")
|
||
local beach = (game and game.data and P.pal(game.data, "PIKACHUS_BEACH")) or PIKACHUS_BEACH_PAL
|
||
if self.routine == ROUTINE_TITLE then
|
||
local title = (game and game.data and P.pal(game.data, "PIKACHUS_BEACH_TITLE"))
|
||
or PIKACHUS_BEACH_TITLE_PAL
|
||
-- SurfingMinigame_TitleTilemap at (4,0), 12x6; ATTR_BLK pal 1 over that rect.
|
||
return { P.whole(beach), P.zone(title, 4, 0, 15, 5) }
|
||
end
|
||
return { P.whole(beach) }
|
||
end
|
||
|
||
return SurfingMinigame
|