mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 12:15:31 +02:00
286988a1e3
also fixes feet layering of gold with grass. Unifies gen1/gen2 grass layering into a single system
1080 lines
40 KiB
Lua
1080 lines
40 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 buffered input (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)
|
|
local SPEED_INITIAL = 64 -- 0.25 * 256
|
|
local SPEED_MAX = 512 -- 2.00 * 256
|
|
local SPEED_ACCEL = 2 -- (1/128) * 256
|
|
local SPEED_ROUGH_PENALTY = 64 -- 0.25 * 256
|
|
local SPEED_HARD_PENALTY = 128 -- 0.50 * 256
|
|
local SPEED_JUMP_THRESHOLD = 320 -- 1.25 * 256 (10/32)
|
|
|
|
-- 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 -- 24 sections ($18) of 128px = 3072px course
|
|
local BG_HEIGHT = 128 -- Rows the BG shows; HP window covers the rest (y=128..144)
|
|
|
|
-- Routine numbers (wSurfingMinigameRoutineNumber)
|
|
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
|
|
|
|
-- 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)
|
|
}
|
|
|
|
-- 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
|
|
}
|
|
|
|
-- 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)
|
|
local self = setmetatable({ game = game, onDone = onDone }, SurfingMinigame)
|
|
self.routine = ROUTINE_START_GAME
|
|
self.pikaState = PIKA_STATE_RIDING
|
|
self.t = 0
|
|
self.routineTimer = 0
|
|
self.distanceFixed = 0 -- 16.8 fixed-point (256 = 1 pixel)
|
|
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.save.surfingHighScore or 0
|
|
self.newRecord = false
|
|
self.currentPitch = 1.0
|
|
|
|
-- Hardware RNG simulation registers (hRandomAdd, hRandomSub, rDIV)
|
|
self.rDiv = 0
|
|
self.rAdd = 0x55
|
|
self.rSub = 0xaa
|
|
|
|
-- Wave height tracking & column ring buffer
|
|
self.waveFn = 0
|
|
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
|
|
|
|
-- 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 -- SpeedDividedBy32 (range 10..16)
|
|
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
|
|
|
|
-- 3-frame buffered D-Pad rotation & input accumulator
|
|
self.joyCounter = 0
|
|
self.inputAccum = 0 -- bit 0 = Right, bit 1 = Left
|
|
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.ohNoBanner = false
|
|
self.trickPopups = {} -- { text = "+150", x, y, timer }
|
|
self.waterSprays = {} -- { x, y, timer }
|
|
self.sprayTimer = 0
|
|
self.cloudOffsetFixed = 0
|
|
|
|
-- Results Tally Animation
|
|
self.tallyStep = 0
|
|
self.tallyTimer = 0
|
|
|
|
-- Load sheets safely
|
|
local function sheet(path)
|
|
if not (love and love.graphics and love.graphics.newImage) then return nil end
|
|
local ok, img = pcall(love.graphics.newImage, path)
|
|
return ok and img or nil
|
|
end
|
|
self.bg = sheet("assets/generated/minigame/surf_1a.png")
|
|
self.ob = sheet("assets/generated/minigame/surf_1b.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
|
|
|
|
Music.play(game.data, "Music_SurfingPikachu")
|
|
return self
|
|
end
|
|
|
|
-- Authentic Game Boy VBlank LFSR Random Number Generator
|
|
function SurfingMinigame:getGBRandom()
|
|
self.rDiv = (self.rDiv + 7 + (self.inputAccum or 0)) % 256
|
|
self.rAdd = (self.rAdd + self.rDiv) % 256
|
|
self.rSub = (self.rSub - self.rDiv + 256) % 256
|
|
return (self.rAdd + self.rSub) % 256
|
|
end
|
|
|
|
function SurfingMinigame:chooseSequence()
|
|
local distPx = math.floor(self.distanceFixed / 256)
|
|
if math.floor(distPx / 128) >= 0x16 then
|
|
self.waveFn = 0x6a
|
|
else
|
|
local r = self:getGBRandom()
|
|
if r ~= 0 then self.waveFn = SEQ_STARTS[((r - 1) % 8) + 1] end
|
|
end
|
|
return WAVE_PATTERNS[0x00], FLAT_WATER_Y, FLAT_WATER_Y
|
|
end
|
|
|
|
function SurfingMinigame:pushColumn()
|
|
local pat, hl, hr
|
|
if 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
|
|
self.colTail = self.colTail + 1
|
|
self.cols[self.colTail] = { pat = pat, hl = hl, hr = hr }
|
|
self.cols[self.colTail - 24] = nil
|
|
end
|
|
|
|
function SurfingMinigame:generateAhead()
|
|
local distPx = math.floor(self.distanceFixed / 256)
|
|
while self.colTail * 16 < distPx + 176 do self:pushColumn() end
|
|
end
|
|
|
|
-- Get water surface Y for a screen X coordinate (X=80 under Pikachu center)
|
|
function SurfingMinigame:seaY(x)
|
|
local distPx = math.floor(self.distanceFixed / 256)
|
|
local tile = math.floor((distPx + x) / 8)
|
|
local col = self.cols[math.floor(tile / 2)]
|
|
if not col then return FLAT_WATER_Y - 16 end
|
|
return (tile % 2 == 0 and col.hl or col.hr) - 16
|
|
end
|
|
|
|
-- Get the tile ID of the wave under Pikachu (sample 9-10 tiles into viewport)
|
|
function SurfingMinigame:getWaveTileUnderPika()
|
|
local distPx = math.floor(self.distanceFixed / 256)
|
|
local tile = math.floor((distPx + 80) / 8)
|
|
local col = self.cols[math.floor(tile / 2)]
|
|
if not col or not col.pat then return 0x01 end
|
|
local pat = col.pat
|
|
for i = 4, 8 do
|
|
local mt = pat[i]
|
|
if mt then
|
|
if mt == 0x02 or mt == 0x06 or mt == 0x0a or mt == 0x11 then
|
|
return 0x06 -- rising slope
|
|
elseif mt == 0x03 or mt == 0x07 or mt == 0x0b or mt == 0x0d then
|
|
return 0x07 -- falling slope
|
|
elseif mt == 0x08 or mt == 0x09 or mt == 0x0f or mt == 0x10 or mt == 0x14 or mt == 0x15 then
|
|
return 0x14 -- wave crest / face
|
|
end
|
|
end
|
|
end
|
|
return 0x01 -- flat open water
|
|
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
|
|
-- Flipped / upside-down frames (8..14) ALWAYS wipeout unconditionally!
|
|
if f >= 8 or f < 1 then
|
|
return "wipeout"
|
|
end
|
|
|
|
local tile = self:getWaveTileUnderPika()
|
|
|
|
if tile == 0x06 then -- risingSlope
|
|
if f == 6 then return "clean"
|
|
elseif f == 5 or f == 7 then return "rough"
|
|
elseif f == 4 then return "hard"
|
|
else return "wipeout" end -- 1, 2, 3
|
|
|
|
elseif tile == 0x07 then -- fallingSlope
|
|
if f == 2 then return "clean"
|
|
elseif f == 1 or f == 3 then return "rough"
|
|
elseif f == 4 then return "hard"
|
|
else return "wipeout" end -- 5, 6, 7
|
|
|
|
elseif tile == 0x14 or tile == 0x12 then -- waveCrest / waveFace
|
|
if f == 4 or f == 5 then return "clean"
|
|
elseif f == 3 or f == 6 then return "rough"
|
|
elseif f == 2 or f == 7 then return "hard"
|
|
else return "wipeout" end -- 1
|
|
|
|
else -- flat open water
|
|
if f == 4 then return "clean"
|
|
elseif f == 3 or f == 5 then return "rough"
|
|
elseif f == 2 or f == 6 then return "hard"
|
|
else return "wipeout" end -- 1, 7
|
|
end
|
|
end
|
|
|
|
function SurfingMinigame:updateRiding()
|
|
-- Automatic speed up (+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
|
|
|
|
-- Follow wave surface height
|
|
local targetY = self:seaY(80)
|
|
self.pikaY = math.floor(targetY)
|
|
self.pikaSubY = 0
|
|
|
|
-- Water spray every 4 frames
|
|
self.sprayTimer = self.sprayTimer + 1
|
|
if self.sprayTimer % 4 == 0 then
|
|
table.insert(self.waterSprays, { x = PIKA_X + 16, y = self.pikaY + 12, timer = 12 })
|
|
end
|
|
|
|
-- Board angle wobbling every 8 frames
|
|
self.boardAngleTimer = self.boardAngleTimer + 1
|
|
if self.boardAngleTimer % 8 == 0 then
|
|
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
|
|
|
|
-- Select frame based on slope
|
|
local tile = self:getWaveTileUnderPika()
|
|
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.frameSet = 4 + (self.boardAngleOffset - 1)
|
|
end
|
|
self.frameSet = math.max(1, math.min(14, self.frameSet))
|
|
|
|
-- Automatic jump off wave crest ($14) if speed >= 1.25 (SPEED_JUMP_THRESHOLD = 320)
|
|
local distPx = math.floor(self.distanceFixed / 256)
|
|
local subX = distPx % 8
|
|
if (subX >= 3 and subX <= 4) and tile == 0x14 and self.speedFixed >= SPEED_JUMP_THRESHOLD then
|
|
self.pikaState = PIKA_STATE_JUMPING
|
|
local spd = self.speedFixed / 256
|
|
self.jumpArcMagnitude = math.min(16, math.max(10, math.floor(spd * 8)))
|
|
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")
|
|
end
|
|
end
|
|
|
|
function SurfingMinigame:updateJumping()
|
|
-- Process accumulated input on the 3-frame buffer boundary
|
|
self.joyCounter = (self.joyCounter + 1) % 3
|
|
if self.joyCounter == 0 then
|
|
local rightHeld = bit.band(self.inputAccum, 1) ~= 0
|
|
local leftHeld = bit.band(self.inputAccum, 2) ~= 0
|
|
self.inputAccum = 0
|
|
|
|
if rightHeld then
|
|
self.rotCountLeft = 0
|
|
self.rotCountRight = self.rotCountRight + 1
|
|
if self.rotCountRight >= 11 then
|
|
self.rotCountRight = 0
|
|
self.radnessMeter = math.min(3, self.radnessMeter + 1)
|
|
self.trickFlags = bit.bor(self.trickFlags, 1)
|
|
Sound.play(self.game.data, "Tink")
|
|
end
|
|
-- Increment frame set (frontflip forward)
|
|
self.frameSet = (self.frameSet % 14) + 1
|
|
elseif leftHeld then
|
|
self.rotCountRight = 0
|
|
self.rotCountLeft = self.rotCountLeft + 1
|
|
if self.rotCountLeft >= 13 then
|
|
self.rotCountLeft = 0
|
|
self.radnessMeter = math.min(3, self.radnessMeter + 1)
|
|
self.trickFlags = bit.bor(self.trickFlags, 2)
|
|
Sound.play(self.game.data, "Tink")
|
|
end
|
|
-- Decrement frame set (backflip backward)
|
|
self.frameSet = self.frameSet - 1
|
|
if self.frameSet < 1 then self.frameSet = 14 end
|
|
end
|
|
end
|
|
|
|
-- Authentic Game Boy collision boundary & integer fixed-point jump physics
|
|
if not self.jumpDescending then
|
|
self.pikaSubY = (self.pikaSubY or 0) + (self.jumpArcMagnitude ^ 2) * 4
|
|
local intDelta = math.floor(self.pikaSubY / 256)
|
|
self.pikaSubY = self.pikaSubY % 256
|
|
self.pikaY = self.pikaY - intDelta
|
|
|
|
self.jumpArcMagnitude = self.jumpArcMagnitude - 0.5
|
|
if self.jumpArcMagnitude <= 0 then
|
|
self.jumpArcMagnitude = 0
|
|
self.jumpDescending = true
|
|
end
|
|
else
|
|
-- Hardware execution order: evaluate boundary before adding velocity
|
|
local waveY = math.floor(self:seaY(80))
|
|
if self.pikaY >= waveY then
|
|
self.pikaY = waveY
|
|
self.pikaSubY = 0
|
|
-- Evaluate landing angle vs wave slope
|
|
local result = self:evaluateLanding()
|
|
if result == "wipeout" then
|
|
self.pikaState = PIKA_STATE_CRASHED
|
|
self.crashTimer = 96
|
|
self.speedFixed = SPEED_INITIAL
|
|
self.frameSet = 4
|
|
Sound.play(self.game.data, "Faint_Fall")
|
|
else
|
|
if result == "rough" then
|
|
self.speedFixed = math.max(SPEED_INITIAL, self.speedFixed - SPEED_ROUGH_PENALTY)
|
|
elseif result == "hard" then
|
|
self.speedFixed = math.max(SPEED_INITIAL, self.speedFixed - SPEED_HARD_PENALTY)
|
|
end
|
|
self:calculateStuntPoints()
|
|
self.pikaState = PIKA_STATE_LANDING
|
|
self.routineTimer = 32
|
|
self.frameSet = 4
|
|
Sound.play(self.game.data, "Cut")
|
|
end
|
|
return
|
|
end
|
|
|
|
self.pikaSubY = (self.pikaSubY or 0) + (self.jumpArcMagnitude ^ 2) * 4
|
|
local intDelta = math.floor(self.pikaSubY / 256)
|
|
self.pikaSubY = self.pikaSubY % 256
|
|
self.pikaY = self.pikaY + intDelta
|
|
self.jumpArcMagnitude = self.jumpArcMagnitude + 0.5
|
|
|
|
if self.pikaY >= waveY then
|
|
self.pikaY = waveY
|
|
self.pikaSubY = 0
|
|
local result = self:evaluateLanding()
|
|
if result == "wipeout" then
|
|
self.pikaState = PIKA_STATE_CRASHED
|
|
self.crashTimer = 96
|
|
self.speedFixed = SPEED_INITIAL
|
|
self.frameSet = 4
|
|
Sound.play(self.game.data, "Faint_Fall")
|
|
else
|
|
if result == "rough" then
|
|
self.speedFixed = math.max(SPEED_INITIAL, self.speedFixed - SPEED_ROUGH_PENALTY)
|
|
elseif result == "hard" then
|
|
self.speedFixed = math.max(SPEED_INITIAL, self.speedFixed - SPEED_HARD_PENALTY)
|
|
end
|
|
self:calculateStuntPoints()
|
|
self.pikaState = PIKA_STATE_LANDING
|
|
self.routineTimer = 32
|
|
self.frameSet = 4
|
|
Sound.play(self.game.data, "Cut")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function SurfingMinigame:updateLanding()
|
|
self.routineTimer = self.routineTimer - 1
|
|
-- Sine wave splash offset
|
|
self.pikaYOffset = math.floor(math.sin((32 - self.routineTimer) / 32 * math.pi * 2) * 4)
|
|
if self.routineTimer % 4 == 0 then
|
|
table.insert(self.waterSprays, { x = PIKA_X + 16, y = self.pikaY + 12, timer = 12 })
|
|
end
|
|
if self.routineTimer <= 0 then
|
|
self.pikaYOffset = 0
|
|
self.pikaState = PIKA_STATE_RIDING
|
|
end
|
|
end
|
|
|
|
function SurfingMinigame:updateCrashed()
|
|
self.crashTimer = self.crashTimer - 1
|
|
if self.crashTimer <= 0 then
|
|
self.pikaState = PIKA_STATE_RIDING
|
|
self.frameSet = 4
|
|
end
|
|
end
|
|
|
|
function SurfingMinigame:update()
|
|
local input = self.game.input
|
|
self.t = self.t + 1
|
|
|
|
-- Accumulate physical button presses on every frame (eliminates polling blind spot)
|
|
if input:isDown("right") then self.inputAccum = bit.bor(self.inputAccum, 1) end
|
|
if input:isDown("left") then self.inputAccum = bit.bor(self.inputAccum, 2) 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
|
|
|
|
-- Routine state machine
|
|
if self.routine == ROUTINE_START_GAME then
|
|
if self.startBannerX > 80 then
|
|
self.startBannerX = math.max(80, self.startBannerX - 4)
|
|
else
|
|
self.routine = ROUTINE_RUN_GAME
|
|
end
|
|
elseif self.routine == ROUTINE_RUN_GAME then
|
|
-- Deduct 1 HP per frame (stamina countdown from 6000 BCD)
|
|
if self.hp > 0 then
|
|
self.hp = self.hp - 1
|
|
else
|
|
-- Game Over when HP hits 0
|
|
self.routine = ROUTINE_GAME_OVER
|
|
self.routineTimer = 128
|
|
self.speedFixed = 0
|
|
self.ohNoBanner = true
|
|
Sound.play(self.game.data, "Faint_Fall")
|
|
return
|
|
end
|
|
|
|
-- Scroll distance & generate wave columns (authentic 1:1 GB pace: distance += speedFixed)
|
|
self.distanceFixed = self.distanceFixed + self.speedFixed
|
|
self.cloudOffsetFixed = self.cloudOffsetFixed + math.floor(self.speedFixed * 0.25)
|
|
self:generateAhead()
|
|
|
|
-- Check if course goal reached (24 sections)
|
|
local distPx = math.floor(self.distanceFixed / 256)
|
|
if distPx >= (TOTAL_SECTIONS * 128) then
|
|
self.routine = ROUTINE_WAIT_RESULTS
|
|
self.routineTimer = 192
|
|
self.waveFn = 0x72
|
|
return
|
|
end
|
|
|
|
-- 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
|
|
-- 192 frames coasting past the goal line
|
|
self.distanceFixed = self.distanceFixed + (2 * 256)
|
|
self:generateAhead()
|
|
self.pikaY = math.floor(self:seaY(80))
|
|
self.routineTimer = self.routineTimer - 1
|
|
if self.routineTimer <= 0 then
|
|
self.routine = ROUTINE_SCROLL_RESULTS
|
|
self.routineTimer = 36
|
|
end
|
|
|
|
elseif self.routine == ROUTINE_SCROLL_RESULTS then
|
|
self.distanceFixed = self.distanceFixed + (1 * 256)
|
|
self:generateAhead()
|
|
self.pikaY = math.floor(self:seaY(80))
|
|
self.routineTimer = self.routineTimer - 1
|
|
if self.routineTimer <= 0 then
|
|
self.routine = ROUTINE_DRAW_RESULTS
|
|
self.routineTimer = 64
|
|
self.pikaState = PIKA_STATE_RESULTS
|
|
end
|
|
|
|
elseif self.routine == ROUTINE_DRAW_RESULTS then
|
|
self.routineTimer = self.routineTimer - 1
|
|
if self.routineTimer <= 0 then
|
|
self.routine = ROUTINE_WRITE_HP_LEFT
|
|
self.routineTimer = 32
|
|
end
|
|
|
|
elseif self.routine == ROUTINE_WRITE_HP_LEFT then
|
|
self.routineTimer = self.routineTimer - 1
|
|
if self.routineTimer <= 0 then
|
|
self.routine = ROUTINE_WRITE_RADNESS
|
|
self.routineTimer = 32
|
|
end
|
|
|
|
elseif self.routine == ROUTINE_WRITE_RADNESS then
|
|
self.routineTimer = self.routineTimer - 1
|
|
if self.routineTimer <= 0 then
|
|
self.routine = ROUTINE_WRITE_TOTAL
|
|
self.routineTimer = 32
|
|
end
|
|
|
|
elseif self.routine == ROUTINE_WRITE_TOTAL then
|
|
self.routineTimer = self.routineTimer - 1
|
|
if self.routineTimer <= 0 then
|
|
self.routine = ROUTINE_ADD_HP_TOTAL
|
|
self.tallyStep = 0
|
|
end
|
|
|
|
elseif self.routine == ROUTINE_ADD_HP_TOTAL then
|
|
-- Tally remaining HP into total score (99 pts/frame matching ld c, 99)
|
|
if 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.data, "Press_AB")
|
|
else
|
|
self.routine = ROUTINE_ADD_RAD_TOTAL
|
|
end
|
|
|
|
elseif self.routine == ROUTINE_ADD_RAD_TOTAL then
|
|
-- Tally Radness into total score (99 pts/frame matching ld c, 99)
|
|
if 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.data, "Press_AB")
|
|
else
|
|
self.routine = ROUTINE_WAIT_LAST
|
|
self.routineTimer = 64
|
|
-- High score check
|
|
self.newRecord = self.totalScore > (self.game.save.surfingHighScore or 0)
|
|
if self.newRecord then
|
|
self.game.save.surfingHighScore = self.totalScore
|
|
Sound.play(self.game.data, "Get_Item1")
|
|
Sound.playPikaCry(self.game.data, 34)
|
|
else
|
|
Sound.playPikaCry(self.game.data, 28)
|
|
end
|
|
end
|
|
|
|
elseif self.routine == ROUTINE_WAIT_LAST then
|
|
self.routineTimer = self.routineTimer - 1
|
|
if self.routineTimer <= 0 then
|
|
self.routine = ROUTINE_EXIT_ON_PRESS_A
|
|
end
|
|
|
|
elseif self.routine == ROUTINE_EXIT_ON_PRESS_A then
|
|
if input:wasPressed("a") or input:wasPressed("b") then
|
|
self.game.stack:pop()
|
|
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:wasPressed("a") or input:wasPressed("b")) then
|
|
self.game.stack:pop()
|
|
if self.onDone then self.onDone(0) end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Draw scrolling wave background
|
|
function SurfingMinigame:drawBackground()
|
|
local scx = math.floor(self.distanceFixed / 256)
|
|
local first = math.floor(scx / 16)
|
|
for c = first, first + 10 do
|
|
local col = self.cols[c]
|
|
if col then
|
|
local x = c * 16 - scx
|
|
for i = 1, 8 do
|
|
local mt = BG_METATILES[col.pat[i]]
|
|
if mt then
|
|
local y = (i - 1) * 16
|
|
love.graphics.draw(self.bg, self.tq[mt[1]], x, y)
|
|
love.graphics.draw(self.bg, self.tq[mt[2]], x + 8, y)
|
|
love.graphics.draw(self.bg, self.tq[mt[3]], x, y + 8)
|
|
love.graphics.draw(self.bg, self.tq[mt[4]], x + 8, y + 8)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Draw 3x3 Pikachu sprite (24x24 px centered at cx, cy)
|
|
function SurfingMinigame:draw3x3(baseTile, cx, cy, flipX, flipY)
|
|
for r = 0, 2 do
|
|
for c = 0, 2 do
|
|
local tileId = baseTile + r * 16 + c
|
|
local q = self.oq[tileId]
|
|
if q then
|
|
if not flipX and not flipY then
|
|
local dx = (cx - 12) + c * 8
|
|
local dy = (cy - 12) + r * 8
|
|
love.graphics.draw(self.ob, q, dx, dy)
|
|
else
|
|
local dx = (cx - 12) + (2 - c) * 8 + 8
|
|
local dy = (cy - 12) + (2 - r) * 8 + 8
|
|
love.graphics.draw(self.ob, q, dx, dy, 0, -1, -1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Draw HUD status bar
|
|
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)
|
|
|
|
-- Track progress line (tiles $15..$1c)
|
|
if self.bg and self.tq then
|
|
-- Top row (Y=128)
|
|
love.graphics.draw(self.bg, self.tq[0x15], 8, BG_HEIGHT)
|
|
love.graphics.draw(self.bg, self.tq[0x16], 16, BG_HEIGHT)
|
|
|
|
-- Bottom row (Y=136)
|
|
love.graphics.draw(self.bg, self.tq[0x17], 8, BG_HEIGHT + 8)
|
|
love.graphics.draw(self.bg, self.tq[0x18], 16, BG_HEIGHT + 8)
|
|
local trackTiles = { 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19 }
|
|
for i = 1, #trackTiles do
|
|
love.graphics.draw(self.bg, self.tq[trackTiles[i]], 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)
|
|
|
|
-- "HP:" label tiles on right (X=112, 120)
|
|
love.graphics.draw(self.bg, self.tq[0x20], 112, BG_HEIGHT + 8)
|
|
love.graphics.draw(self.bg, self.tq[0x21], 120, BG_HEIGHT + 8)
|
|
end
|
|
|
|
-- Mini-Pikachu progress marker (tile $fe)
|
|
local distPx = math.floor(self.distanceFixed / 256)
|
|
local progressRatio = math.min(1.0, math.max(0, distPx / (TOTAL_SECTIONS * 128)))
|
|
local markerX = 16 + math.floor(progressRatio * 80)
|
|
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 starting at X=128 (right after HP:)
|
|
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], 120 + i * 8, BG_HEIGHT + 8)
|
|
else
|
|
Font.draw(tostring(d), 120 + i * 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[tId] then
|
|
love.graphics.draw(self.bg, self.tq[tId], (c - 1) * 8, (r + 5) * 8)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Textbox frame on rows 1..9 (X=8..152, Y=8..72)
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.rectangle("fill", 8, 8, 144, 64)
|
|
love.graphics.setColor(0, 0, 0, 1)
|
|
love.graphics.rectangle("line", 8.5, 8.5, 143, 63)
|
|
|
|
-- Text lines
|
|
Font.draw(Strings("HP Left"), 16, 16)
|
|
if self.routine >= ROUTINE_WRITE_HP_LEFT then
|
|
Font.draw(string.format("%4d Pts", self.hp), 88, 16)
|
|
end
|
|
|
|
if self.routine >= ROUTINE_WRITE_RADNESS then
|
|
Font.draw(Strings("Radness"), 16, 32)
|
|
Font.draw(string.format("%4d Pts", self.radness), 88, 32)
|
|
end
|
|
|
|
if self.routine >= ROUTINE_WRITE_TOTAL then
|
|
Font.draw(Strings("Total"), 16, 48)
|
|
Font.draw(string.format("%4d Pts", self.totalScore), 88, 48)
|
|
end
|
|
|
|
if self.routine >= ROUTINE_WAIT_LAST then
|
|
if self.newRecord then
|
|
Font.draw(Strings("Hi-Score!!"), 48, 60)
|
|
end
|
|
end
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
end
|
|
|
|
function SurfingMinigame:draw()
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
|
|
|
if self.routine >= ROUTINE_DRAW_RESULTS and self.routine <= ROUTINE_EXIT_ON_PRESS_A then
|
|
self:drawResultsOutro()
|
|
return
|
|
end
|
|
|
|
-- Draw scrolling BG waves
|
|
self:drawBackground()
|
|
|
|
-- Parallax clouds in sky
|
|
local cloudOffsetPx = math.floor(self.cloudOffsetFixed / 256)
|
|
local c1x = (32 - cloudOffsetPx * 2) % 200 - 40
|
|
local c2x = (128 - cloudOffsetPx * 3) % 200 - 40
|
|
-- Wide cloud (5 tiles: $ec, $ed, $ed, $ee, $ef)
|
|
for i, tid in ipairs({ 0xec, 0xed, 0xed, 0xee, 0xef }) do
|
|
love.graphics.draw(self.ob, self.oq[tid], c1x + (i - 1) * 8, 12)
|
|
end
|
|
-- Narrow cloud (4 tiles: $ec, $ed, $ee, $ef)
|
|
for i, tid in ipairs({ 0xec, 0xed, 0xee, 0xef }) do
|
|
love.graphics.draw(self.ob, self.oq[tid], c2x + (i - 1) * 8, 20)
|
|
end
|
|
|
|
-- Draw water spray sprites
|
|
for _, s in ipairs(self.waterSprays) do
|
|
love.graphics.draw(self.ob, self.oq[0xa7], 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 + splash animation
|
|
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)
|
|
love.graphics.draw(self.ob, self.oq[0xa8], cx - 12, cy - 8)
|
|
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
|
|
if self.routine == ROUTINE_START_GAME 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
|
|
|
|
-- "Oh no.." banner on Game Over
|
|
if self.ohNoBanner 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
|
|
|
|
-- Draw HUD (Progress track, HP digits)
|
|
self:drawHUD()
|
|
end
|
|
|
|
function SurfingMinigame:sgbPalettes(game)
|
|
local P = require("src.render.PaletteFX")
|
|
local pal = (game and game.data and P.pal(game.data, "PIKACHUS_BEACH")) or PIKACHUS_BEACH_PAL
|
|
return { P.whole(pal) }
|
|
end
|
|
|
|
return SurfingMinigame
|