CLOSES #644, CLOSES #703, CLOSES #726, CLOSES #737, CLOSES #750, CLOSES #752, CLOSES #764, CLOSES #765, CLOSES #768, CLOSES #773, CLOSES #774, CLOSES #775, CLOSES #777, CLOSES #780, CLOSES #782

This commit is contained in:
bryanthaboi
2026-08-04 09:25:28 -04:00
parent 6cfc85ca83
commit f2d9b74490
52 changed files with 3099 additions and 252 deletions
+21 -6
View File
@@ -9,8 +9,9 @@
-- CRED_TEXT_MON text appears at once, hold 110, mon wipe
-- CRED_TEXT_FADE fade in, hold 120, next screen replaces the text
-- CRED_TEXT text appears at once, hold 140
-- The mon wipe is DisplayCreditsMon: the middle band scrolls left 8px per
-- frame for 27 frames (ScrollCreditsMonLeft x7 then x20) while the next
-- The mon wipe is DisplayCreditsMon: three CreditsCopyTileMapToVRAM copies
-- (9 frames of Delay3, text still up), then the middle band scrolls left 8px
-- per frame for 27 frames (ScrollCreditsMonLeft x7 then x20) while the next
-- CreditsMons entry crosses right-to-left as a black silhouette
-- (BGP %11111100), leaving the band blank; BGP is left at %11000000, which
-- is why every post-wipe screen is a FADE variant. CRED_COPYRIGHT
@@ -52,6 +53,14 @@ local HOLD_FADE = 120
local HOLD_TEXT = 140
local WIPE_FRAMES = 27 -- ScrollCreditsMonLeft: 7 + 20 calls, 8px/frame
-- DisplayCreditsMon runs three CreditsCopyTileMapToVRAM calls (vBGMap0+$c,
-- vBGMap0, vBGMap1) before the first scroll, and each one ends in `jp Delay3`
-- (home/palettes.asm), so the credits text sits still for 9 more frames on
-- every mon screen. Dropping them ran the 15 mon screens 135 frames short and
-- brought THE END up 2.2s early against a credits theme whose length is fixed
-- by the ROM program (Music_Credits is 5880 frames and does not loop), which
-- is what made the song look like it overran the roll (#703).
local MON_PREP_FRAMES = 9
-- LoadCopyrightTiles (engine/movie/title.asm CopyrightTextString): tile
-- sequences into the extracted title/copyright.png strip (tiles $60-$72:
@@ -204,12 +213,17 @@ function Credits:update(dt)
self.timer = self.screen.mon and HOLD_FADE_MON or HOLD_FADE
elseif self.phase == "hold" then
if self.screen.mon then
self.phase = "wipe"
self.timer = WIPE_FRAMES
self.monImg, self.monTint = self:monSprite(self.screen.mon)
-- the text stays up through DisplayCreditsMon's VRAM copies; the
-- silhouette only starts moving once ScrollCreditsMonLeft does
self.phase = "mon_prep"
self.timer = MON_PREP_FRAMES
else
self:nextScreen()
end
elseif self.phase == "mon_prep" then
self.phase = "wipe"
self.timer = WIPE_FRAMES
self.monImg, self.monTint = self:monSprite(self.screen.mon)
elseif self.phase == "wipe" then
self.monImg = nil
self:nextScreen()
@@ -313,7 +327,8 @@ function Credits:draw()
love.graphics.rectangle("fill", 0, 0, 160, 32)
love.graphics.rectangle("fill", 0, 112, 160, 32)
love.graphics.setColor(1, 1, 1, 1)
if self.phase == "fade" or self.phase == "hold" then
if self.phase == "fade" or self.phase == "hold"
or self.phase == "mon_prep" then
self:drawPage(self.screen, 0, self.shade)
elseif self.phase == "wipe" then
-- ScrollCreditsMonLeft: the middle band scrolls left 8px/frame while
+36 -8
View File
@@ -155,7 +155,11 @@ local function obpIcon(path)
return love.graphics.newImage(id)
end
local function drawIcon(game, mon, x, y, selected, counter)
-- `forceAlt` picks the second animation frame outright, for callers with no
-- selection cursor of their own: Trade_AnimCircledMon
-- (engine/movie/trade.asm) cycles the party sprite's two frames the whole
-- time the mon rides the link cable (#750).
function PartyMenu.drawIcon(game, mon, x, y, selected, counter, forceAlt)
local icons = game.data.icons
if not icons then return end
local def = game.data.pokemon[mon.species]
@@ -202,7 +206,7 @@ local function drawIcon(game, mon, x, y, selected, counter)
end
local img = iconImages[key]
if not img then return end
local alt = false
local alt = forceAlt or false
if selected then
local px = math.floor(mon.hp * 48 / math.max(1, mon.stats.hp))
local speed = px >= 27 and 5 or px >= 10 and 16 or 32
@@ -237,13 +241,24 @@ local function drawIcon(game, mon, x, y, selected, counter)
-- whatever size the file is (unchanged path)
love.graphics.draw(img, x, y)
end
return true
end
function PartyMenu.new(game, opts)
opts = opts or {}
local self = setmetatable({}, PartyMenu)
self.game = game
self.index = 1
-- PartyMenuInit (home/pokemon.asm) seeds the cursor from
-- wPartyAndBillsPCSavedMenuItem rather than from zero, and
-- HandlePartyMenuInput writes wCurrentMenuItem back into it on every
-- input, so the party cursor survives closing and reopening the menu.
-- Only a battle clears it -- InitBattleVariables and end_of_battle.asm
-- both zero the byte, which BattleState mirrors. The clamp covers a
-- party that shrank (deposit / release) while the saved index was
-- pointing past the end. #768
local count = #(opts.party or (game.save and game.save.party) or {})
self.index = math.min(math.max(1, game.partyMenuSavedIndex or 1),
math.max(1, count))
self.onSwitch = opts.onSwitch
self.onCancel = opts.onCancel
self.pickOnly = opts.pickOnly
@@ -518,8 +533,10 @@ function PartyMenu:update(dt)
if input:wasPressed("up") then
self.index = self.index > 1 and self.index - 1 or math.max(1, #party)
self.game.partyMenuSavedIndex = self.index -- HandlePartyMenuInput #768
elseif input:wasPressed("down") then
self.index = self.index < #party and self.index + 1 or 1
self.game.partyMenuSavedIndex = self.index -- HandlePartyMenuInput #768
elseif input:wasPressed("b") then
self.game.stack:pop()
if self.onCancel then self.onCancel() end
@@ -567,10 +584,17 @@ function PartyMenu:update(dt)
{ label = Strings("STATS"), action = "stats" },
{ label = Strings("CANCEL"), action = "cancel" } }
else
-- STATS/SWITCH plus this mon's field moves (start_sub_menus.asm
-- builds the same dynamic list)
items = { { label = Strings("STATS"), action = "stats" },
{ label = Strings("SWITCH"), action = "switch" } }
-- This mon's field moves FIRST, then STATS/SWITCH
-- (start_sub_menus.asm builds the same dynamic list). The order is
-- load bearing: DisplayFieldMoveMonMenu (engine/menus/text_box.asm)
-- grows the box upward one row per field move and prints the field
-- move names ABOVE PokemonMenuEntries ("STATS/SWITCH/CANCEL"), and
-- StartMenu_Pokemon .choseOutOfBattleMove indexes wFieldMoves with
-- menu items 0..n-1 while STATS/SWITCH sit at the bottom of the
-- list. GetMonFieldMoves walks wPartyMon1Moves in slot order, so
-- the field moves keep the mon's move-list order -- which the loop
-- below already does. #768
items = {}
-- Field moves (HMs/TMs) are usable out of battle even when the mon
-- is fainted -- Gen 1 does not require HP for Cut/Fly/Surf/etc.
-- Battle still excludes this list via `not self.battle`. Softboiled
@@ -615,6 +639,10 @@ function PartyMenu:update(dt)
end
end
end
-- PokemonMenuEntries always closes the list, under the field moves
-- (text_box.asm .donePrintingNames). #768
items[#items + 1] = { label = Strings("STATS"), action = "stats" }
items[#items + 1] = { label = Strings("SWITCH"), action = "switch" }
end
local ctx = { battle = self.battle, overworld = ow }
local hooked = Runtime.call("ui.party.submenu", sameItems,
@@ -685,7 +713,7 @@ function PartyMenu:draw()
local def = self.game.data.pokemon[mon.species]
local y = PartyMenu.entryY(i)
love.graphics.setColor(1, 1, 1, 1)
drawIcon(self.game, mon, 8, y, i == self.index, self.blink or 0)
PartyMenu.drawIcon(self.game, mon, 8, y, i == self.index, self.blink or 0)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(mon.nickname or def.name, 24, y)
-- level at column 13 (<LV> tile + digits, PrintLevel) AND the
+15 -5
View File
@@ -66,14 +66,24 @@ function StartMenu.new(game)
panel .. Strings("\fWould you like to\nSAVE the game?"), nil, {
choice = function(yes)
if not yes then return end
-- "Now saving..." beat before the write (save.asm
-- NowSavingString), then GameSavedText + SFX_SAVE
-- SaveMenu .save (engine/menus/save.asm:164-181): "Now saving..."
-- is a bare PlaceString held by DelayFrames 120, then GameSavedText,
-- which ends in `done` and so never reaches TX_PROMPT_BUTTON.
-- Neither page takes a button press (#765); the second waits on
-- SFX_SAVE (PlaySoundWaitForCurrent + WaitForSoundToFinish) and then
-- DelayFrames 30. The write itself is invisible either side of the
-- "Now saving..." hold, so it stays on that box's onDone.
game.stack:push(TextBox.new(game, Strings("Now saving..."), function()
game:writeSave()
require("src.core.Sound").play(game.data, "Save")
game.stack:push(TextBox.new(game,
Strings("%s saved\nthe game!", game.save.player.name or "RED")))
end))
Strings("%s saved\nthe game!", game.save.player.name or "RED"),
nil, { auto = {
sound = function()
return require("src.core.Sound").play(game.data, "Save")
end,
delay = 30,
} }))
end, { auto = { delay = 120 } }))
end,
}))
end })
+281 -102
View File
@@ -3,14 +3,21 @@
-- spin in the air and land flat for points; a crooked landing wipes out
-- and ends the run. The scene is built from the real ROM sheets
-- (gfx/surfing_pikachu.asm, ripped at import to
-- assets/generated/minigame/surf_1a/1b.png): the scalloped water tiles,
-- the beach with the palm and the doll hut, the "HP:" score strip with
-- the sheet digits, the cloud, and the OAM Pikachu poses -- the air
-- tricks quantize to the sheet's rotation frames like the original's
-- sprite anims, instead of free-rotating one pose. The original drew
-- the big wave with per-scanline scroll tricks (wLYOverrides); here the
-- crest profile is a curve filled with the sheet's foam/shade tiles.
-- Score model keeps the original's shape (ride ticks + airtime + full
-- assets/generated/minigame/surf_1a/1b.png).
--
-- #726: the background is the original's own metatile scroller, not a
-- procedural stand-in. SurfingPikachu1Graphics1 is copied to vChars2
-- with LCDC's BG char base unset, so BG tile id N is simply tile N of
-- surf_1a (5 tiles per row). SurfingMinigame_ScrollAndGenerateBGMap
-- walks a jumptable of wave states, each of which hands back one
-- 8-metatile column (2x2 tiles each, so 16px wide by the 128px the BG
-- shows above the HP window) plus the two Pikachu ride heights for that
-- column. Porting those tables verbatim is what makes the water read as
-- water: the earlier stand-in tiled the wave-face tiles ($02/$07) over
-- the whole sea and drew the swell as a LOVE ellipse, which is the
-- "messed up graphics" in the report.
--
-- Score model keeps the port's shape (ride ticks + airtime + full
-- rotations); high score persists in save.surfingHighScore for the
-- beach-house printer.
@@ -23,10 +30,13 @@ local SurfingMinigame = {}
SurfingMinigame.__index = SurfingMinigame
SurfingMinigame.isOpaque = true
local PIKA_X = 44 -- fixed screen x while riding
local RUN_DISTANCE = 3200 -- scroll px from paddle-out to the beach
-- SURFING_MINIGAME_CENTER_X/FLAT_WATER_Y (surfing_pikachu.asm:1-2) are OAM
-- coordinates; screen x/y are those minus OAM_X_OFS/OAM_Y_OFS.
local FLAT_WATER_Y = 116
local PIKA_X = 68 -- fixed screen x while riding (center 80, 24px pose)
local RUN_DISTANCE = 3072 -- 24 sections of 8 metatile columns
local GRAVITY = 0.14
local HORIZON = 24 -- sea starts under the sky strip
local BG_HEIGHT = 128 -- rows the BG shows; the HP window covers the rest
-- surf_1b quads: {x, y, w, h} in sheet pixels (pose pitch is 24x24)
local B = {
@@ -50,18 +60,158 @@ local POSES = {
[315] = { 0, 0, 24, 24 }, -- tail down
}
-- surf_1a quads (BG tiles)
local A = {
scallop = { 16, 0, 8, 8 }, -- open-water pattern, row A
scallop2 = { 16, 8, 8, 8 }, -- row B variant
shade = { 8, 16, 8, 8 }, -- gray dither, wave belly
lip = { 24, 0, 8, 8 }, -- foam curl for the crest edge
palm = { 8, 32, 8, 8 }, -- palm fronds
beach = { 24, 32, 16, 8 }, -- black shore silhouette
hut = { 8, 40, 16, 8 }, -- the Pikachu doll hut on the sand
hp = { 20, 40, 20, 8 }, -- "HP:" score label
-- surf_1a is 5 tiles wide, so BG tile id N lives at (N%5*8, N/5*8). The
-- only quad the scene needs by hand is the window's "HP:" label, which
-- straddles a tile boundary in the sheet.
local HP_LABEL = { 20, 40, 20, 8 }
-- SurfingMinigame_BGMetatileTable (surfing_pikachu.asm): 2x2 tiles each,
-- stored top-left, top-right, bottom-left, bottom-right.
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 }, -- unused, identical to 11
[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 },
}
-- SurfingMinigameWavePattern00..1C plus SurfingMinigameBeachPattern: one
-- column of 8 metatiles, top to bottom.
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 },
}
-- RunSurfingMinigameRoutine's .WaveFunctions jumptable, flattened:
-- { pattern, left ride height, right ride height, what to do next }.
-- next: 0 = advance one state, 1 = reset to the chooser, 2 = stay put.
-- State 0 is SurfingMinigame_ChooseNextWaveSequence and is handled in
-- code because it rolls Random and forces the Big Kahuna near the goal.
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 },
-- 6a..71: the forced "Big Kahuna" finale; 71 holds flat water (its
-- loader just rets, so the state never advances on its own).
[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 },
-- 72..7b: the run-out to the beach, entered by hand at the goal
-- (SurfingMinigame_WaitToShowResults writes $72).
[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 },
}
-- SurfingMinigame_WaveSequenceStarts
local SEQ_STARTS = { 0x01, 0x0e, 0x1a, 0x29, 0x32, 0x40, 0x4d, 0x5c }
-- the #726 table-integrity check in tests/drivers reads these; nothing
-- else should
SurfingMinigame.BG_METATILES = BG_METATILES
SurfingMinigame.WAVE_PATTERNS = WAVE_PATTERNS
SurfingMinigame.WAVE_STEPS = WAVE_STEPS
-- SGB-style zones: one sea palette over the frame plus a yellow
-- OBJ-flavored palette tracking Pikachu's tiles (rectangular attribute
-- blocks are all the SGB could do, bleed and all)
@@ -91,6 +241,17 @@ function SurfingMinigame.new(game, onDone)
self.resultShown = 0
self.banner = nil -- {quad, frames}: GOOD!/YEAH-/Oh no..
-- SurfingPikachuMinigame_LoadGFXAndLayout prefills the BG with flat
-- water and only starts generating $a0 pixels (ten metatile columns)
-- ahead of the viewport, so the run opens on calm sea.
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
local function sheet(path)
local ok, img = pcall(love.graphics.newImage, path)
return ok and img or nil
@@ -98,8 +259,12 @@ function SurfingMinigame.new(game, onDone)
self.bg = sheet("assets/generated/minigame/surf_1a.png")
self.ob = sheet("assets/generated/minigame/surf_1b.png")
if self.bg then
self.aq = {}
for k, spec in pairs(A) do self.aq[k] = newQuad(spec, self.bg) end
self.tq = {}
for n = 0, 64 do
self.tq[n] = love.graphics.newQuad((n % 5) * 8, math.floor(n / 5) * 8,
8, 8, self.bg:getDimensions())
end
self.hpq = newQuad(HP_LABEL, self.bg)
end
if self.ob then
self.bq = {}
@@ -122,11 +287,53 @@ function SurfingMinigame.new(game, onDone)
return self
end
-- crest height at screen x for the current scroll (two sines so the
-- wave rolls instead of looping visibly)
-- SurfingMinigame_ChooseNextWaveSequence: past section $16 the finale is
-- forced, otherwise a nonzero Random picks one of eight sequence starts.
-- Either way this column itself is flat water.
function SurfingMinigame:chooseSequence()
if math.floor(self.distance / 128) >= 0x16 then
self.waveFn = 0x6a
else
local r = math.random(0, 255)
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
-- one 16px metatile column, appended on the right as the sea scrolls
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 -- columns behind the viewport
end
-- keep the generated columns covering the viewport plus the lookahead
function SurfingMinigame:generateAhead()
while self.colTail * 16 < self.distance + 176 do self:pushColumn() end
end
-- Pikachu's screen y for a screen x, from the per-tile-column ride
-- heights the wave states hand back (SurfingMinigame_SetPikachuHeight
-- samples the same array either side of the scroll's low bit).
function SurfingMinigame:seaY(x)
local s = self.distance + x
return 92 - 14 * math.sin(s / 26) - 6 * math.sin(s / 9.5)
local tile = math.floor((self.distance + 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
function SurfingMinigame:finishRun()
@@ -146,6 +353,13 @@ function SurfingMinigame:update()
if self.banner.frames <= 0 then self.banner = nil end
end
if self.phase == "results" then
-- the sea keeps sliding under the card for a beat so the beach
-- run-out (states $72..$7b) actually crosses the screen, like
-- SurfingMinigame_WaitToShowResults scrolling to the sand
if self.resultShown < 150 then
self.distance = self.distance + 1.2
self:generateAhead()
end
self.resultShown = self.resultShown + 1
if self.resultShown > 30
and (input:wasPressed("a") or input:wasPressed("b")) then
@@ -162,9 +376,13 @@ function SurfingMinigame:update()
-- the wave scrolls by the current speed; the beach ends the run
self.distance = self.distance + 0.8 + self.speed * 0.35
self:generateAhead()
if self.distance >= RUN_DISTANCE then
-- rode it all the way in: distance bonus like the original's goal
self.score = self.score + 500
-- SurfingMinigame_WaitToShowResults hands the generator state $72 so
-- the sand runs out under the coast-in
self.waveFn = 0x72
self:finishRun()
return
end
@@ -218,16 +436,11 @@ function SurfingMinigame:update()
end
end
-- draw one 8x8 sheet tile quad at x, y
function SurfingMinigame:tile(q, x, y)
love.graphics.draw(self.bg, self.aq[q], x, y)
end
function SurfingMinigame:sgbPalettes()
local P = require("src.render.PaletteFX")
local zones = { P.whole(SEA_PAL) }
if self.phase ~= "wipeout" and self.phase ~= "results" then
local tx = math.floor((PIKA_X - 12) / 8)
local tx = math.floor(PIKA_X / 8)
local ty = math.floor(math.max(0, self.pikaScreenY or 60) / 8)
zones[#zones + 1] = P.zone(PIKA_PAL, tx, ty, tx + 3, ty + 3)
end
@@ -242,87 +455,52 @@ function SurfingMinigame:drawScore(x, y, n)
end
end
-- the BG map: metatile columns from the wave generator, scrolled by
-- distance (SurfingMinigame_ScrollAndGenerateBGMap)
function SurfingMinigame:drawBackground()
local scx = math.floor(self.distance)
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
function SurfingMinigame:draw()
local haveSheets = self.bg and self.ob
-- sky
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144)
if not haveSheets then
-- cache predates the surf sheets: plain shapes keep it playable
love.graphics.setColor(0, 0, 0, 1)
Font.draw(Strings("SCORE %d", self.score), 4, 4)
love.graphics.rectangle("fill", PIKA_X - 8,
self:seaY(PIKA_X) - 16 - self.y, 16, 16)
love.graphics.rectangle("fill", PIKA_X, self:seaY(PIKA_X) - self.y, 16, 16)
love.graphics.setColor(1, 1, 1, 1)
return
end
self:drawBackground()
-- cloud in the sky strip
love.graphics.draw(self.ob, self.bq.cloud, 112, 8)
-- open water: the scalloped pattern tiles the whole sea, phase-locked
-- to the scroll so the surface slides
local shift = math.floor(self.distance) % 8
for ty = HORIZON, 136, 8 do
local alt = (ty / 8) % 2 == 0
for tx = -8, 160, 8 do
self:tile(alt and "scallop" or "scallop2", tx - shift, ty)
end
end
-- the wave face: a white patch hugging the ride line (the original
-- carved it with per-scanline scroll; the ellipse stands in), with a
-- few scallops floating inside and the foam lip along its upper edge
local faceY = self:seaY(56) + 10
love.graphics.setColor(1, 1, 1, 1)
love.graphics.ellipse("fill", 56, faceY, 46, 30)
love.graphics.ellipse("fill", 100, faceY + 16, 40, 22)
for _, spot in ipairs({ { 30, 8 }, { 70, 16 }, { 48, 22 } }) do
self:tile("scallop", 56 - 46 + spot[1] - shift, faceY - 24 + spot[2])
end
local pikaY = self:seaY(PIKA_X) - 20 - self.y
for a = 205, 335, 18 do
local r = math.rad(a)
local lx = 56 + math.cos(r) * 44 - 4
local ly = faceY + math.sin(r) * 28 - 4
-- foam that would land inside Pikachu's SGB zone comes out orange;
-- leave that patch to the spray ellipse instead
if math.abs(lx - PIKA_X) > 28 or math.abs(ly - (pikaY + 12)) > 26 then
self:tile("lip", lx, ly)
end
end
self:tile("shade", 92 - shift, faceY + 20)
self:tile("shade", 116 - shift, faceY + 24)
-- beach slides through at the start and again before the goal
local beachX
if self.distance < 160 then
beachX = -self.distance
elseif self.distance > RUN_DISTANCE - 200 then
beachX = 160 - (self.distance - (RUN_DISTANCE - 200))
end
if beachX then
for tx = 0, 32, 8 do
self:tile("beach", beachX + tx, 128)
self:tile("beach", beachX + tx, 136)
end
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle("fill", beachX + 9, 118, 2, 10)
love.graphics.setColor(1, 1, 1, 1)
self:tile("palm", beachX + 6, 112)
self:tile("hut", beachX + 20, 118)
end
-- Pikachu. The white spray patch under him doubles as the yellow SGB
-- zone's backdrop: shade 0 maps to white in both palettes, so the
-- attribute-block bleed never shows on the water pattern.
love.graphics.setColor(1, 1, 1, 1)
local py = self:seaY(PIKA_X) - 20 - self.y
-- Pikachu rides at the height his tile column reports
local py = self:seaY(PIKA_X) - self.y
self.pikaScreenY = py -- the yellow SGB zone tracks this
love.graphics.ellipse("fill", PIKA_X, py + 12, 25, 21)
if self.phase == "wipeout" then
love.graphics.draw(self.ob, self.bq.splash, PIKA_X - 16,
self:seaY(PIKA_X) - 16)
love.graphics.draw(self.ob, self.bq.splash, PIKA_X - 4, py)
else
local quad
if self.phase == "ride" and self.speed <= 2
@@ -332,7 +510,7 @@ function SurfingMinigame:draw()
local bucket = math.floor(((self.rot % 360) + 22.5) / 45) % 8 * 45
quad = self.bq.poses[bucket] or self.bq.poses[0]
end
love.graphics.draw(self.ob, quad, PIKA_X - 12, py)
love.graphics.draw(self.ob, quad, PIKA_X, py)
end
-- banner beats: GOOD! / YEAH- / Oh no..
@@ -340,11 +518,12 @@ function SurfingMinigame:draw()
love.graphics.draw(self.ob, self.bq[self.banner.quad], 60, 40)
end
-- score strip, bottom right: HP: + sheet digits
-- the HP window sits under the BG rows ($7e into hWY puts it at y 126;
-- tile-aligned here): "HP:" plus the sheet digits over plain white
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 100, 134, 60, 10)
love.graphics.draw(self.bg, self.aq.hp, 102, 135)
self:drawScore(126, 135, self.score)
love.graphics.rectangle("fill", 0, BG_HEIGHT, 160, 144 - BG_HEIGHT)
love.graphics.draw(self.bg, self.hpq, 8, BG_HEIGHT + 4)
self:drawScore(32, BG_HEIGHT + 4, self.score)
if self.phase == "results" then
love.graphics.setColor(1, 1, 1, 1)
+35 -20
View File
@@ -10,8 +10,16 @@ local TradeAnim = {}
TradeAnim.__index = TradeAnim
TradeAnim.isOpaque = true
-- Trade_LoadMonSprite runs SET_PAL_POKEMON_WHOLE_SCREEN for the mon it puts
-- on screen; every other step of the sequence runs SET_PAL_GENERIC, which is
-- PAL_MEWMON (data/sgb/sgb_packets.asm PalPacket_Generic). #750
function TradeAnim:sgbPalettes(game)
return require("src.render.PaletteFX").wholeNamed(game.data, "MEWMON")
local P = require("src.render.PaletteFX")
local mon = (self.phase == "show_player" and self.sent)
or (self.phase == "show_enemy" and self.received)
local colors = mon and P.monPal(game.data, mon.species)
if colors then return { P.whole(colors) } end
return P.wholeNamed(game.data, "MEWMON")
end
local DEFAULT_ART = {
@@ -350,17 +358,17 @@ function TradeAnim:drawMonInfo(mon, ot, otId, boxTy)
love.graphics.setColor(1, 1, 1, 1)
end
function TradeAnim:drawIconInBubble(sprite, x, y)
local spr = sprite
if spr then
local sw, sh = spr:getDimensions()
local s = 16 / math.max(sw, sh)
love.graphics.draw(spr, x, y, 0, s, s)
else
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle("fill", x + 4, y + 4, 8, 8)
love.graphics.setColor(1, 1, 1, 1)
end
-- Trade_WriteCircledMonOAM: the mon crosses the cable as its party-menu
-- sprite (wMonPartySpriteSpecies -> WriteMonPartySpriteOAMBySpecies), not as
-- its battle pic, and Trade_AnimCircledMon flips both it and the ring to
-- their second frame every step. The ring is four OAM blocks --
-- Trade_CircleOAMBlocks .OAMBlock0-3 at (8,8) (24,8) (8,24) (24,24) with the
-- X/Y flips -- so the 16x32 bubble sheet holds one quadrant per frame and the
-- circle it makes is 32x32 around the 16x16 icon. The icon rides OAM
-- block 0 and the circle blocks 1-4 (Trade_WriteCircleOAMBlock counts a up
-- from 1), and the lower OAM index wins overlap on DMG, so the icon draws
-- on top of the circle's filled interior. #750
function TradeAnim:drawIconInBubble(mon, x, y)
if self.img.bubble then
if not self.bubbleQuad then
local iw, ih = self.img.bubble:getDimensions()
@@ -370,7 +378,19 @@ function TradeAnim:drawIconInBubble(sprite, x, y)
or self.bubbleQuad
end
local q = self.cableFlash and self.bubbleQuadAlt or self.bubbleQuad
love.graphics.draw(self.img.bubble, q, x - 8, y - 8)
local left, top = x - 8, y - 8
local right, bottom = left + 32, top + 32
love.graphics.draw(self.img.bubble, q, left, top)
love.graphics.draw(self.img.bubble, q, right, top, 0, -1, 1)
love.graphics.draw(self.img.bubble, q, left, bottom, 0, 1, -1)
love.graphics.draw(self.img.bubble, q, right, bottom, 0, -1, -1)
end
local drawn = mon and require("src.ui.PartyMenu").drawIcon(
self.game, mon, x, y, false, 0, self.cableFlash)
if not drawn then
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle("fill", x + 4, y + 4, 8, 8)
love.graphics.setColor(1, 1, 1, 1)
end
end
@@ -471,13 +491,8 @@ function TradeAnim:draw()
love.graphics.translate(160, 0)
self:drawRightGB()
love.graphics.pop()
local sprite
if p == "transfer_lr" then
sprite = self.sentSprite
else
sprite = self.recvSprite
end
self:drawIconInBubble(sprite, self.monX, self.monY)
local mon = p == "transfer_lr" and self.sent or self.received
self:drawIconInBubble(mon, self.monX, self.monY)
if self.cableFlash then
love.graphics.setColor(1, 1, 1, 0.15)
love.graphics.rectangle("fill", 0, 32, 160, 8)