mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
yellow alpha
This commit is contained in:
@@ -158,15 +158,22 @@ local function useOn(game, battle, id, target, list, moveIndex, picker)
|
||||
local moveId = payload
|
||||
local mdef = game.data.moves[moveId]
|
||||
local function teach()
|
||||
-- PIKAHAPPY_USEDTMHM on a successful teach (item_effects.asm:2500)
|
||||
local function taught()
|
||||
require("src.world.PikachuFollower")
|
||||
.modifyHappiness(game.save, "USEDTMHM", target)
|
||||
end
|
||||
if #target.moves < 4 then
|
||||
table.insert(target.moves, { id = moveId, pp = mdef.pp })
|
||||
showMessages(game, { Strings("%s learned\n%s!", target.nickname or
|
||||
game.data.pokemon[target.species].name, mdef.name) })
|
||||
if result == "learn" then consume(game, id) end
|
||||
taught()
|
||||
else
|
||||
require("src.ui.Screens").push(game, "MoveLearnMenu", target, moveId,
|
||||
function(learned)
|
||||
if learned and result == "learn" then consume(game, id) end
|
||||
if learned then taught() end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
+45
-3
@@ -127,6 +127,9 @@ local function deposit(game)
|
||||
end
|
||||
table.remove(game.save.party, item.value)
|
||||
table.insert(active, mon)
|
||||
-- PIKAHAPPY_DEPOSITED (engine/pokemon/bills_pc.asm:247)
|
||||
require("src.world.PikachuFollower")
|
||||
.modifyHappiness(game.save, "DEPOSITED", mon)
|
||||
local name = monName(game, mon)
|
||||
game.stringBuffer = name
|
||||
game.boxNumString = tostring(game.save.currentBox)
|
||||
@@ -220,13 +223,43 @@ local function drawChrome(game)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
-- PrintPCBox (engine/printer/printer.asm): Yellow's box-list print job,
|
||||
-- box number plus each stored mon's name, level and dex number; the PNG
|
||||
-- under prints/ stands in for the printer paper.
|
||||
local function printBox(game)
|
||||
local box = Boxes.active(game.save)
|
||||
local Printer = require("src.core.Printer")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local h = 32 + math.max(1, #box) * 10
|
||||
local saved, err = Printer.save("box_" .. (game.save.currentBox or 1),
|
||||
160, h, function()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, h)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(Strings("BOX No.%d", game.save.currentBox or 1), 8, 8)
|
||||
if #box == 0 then Font.draw(Strings("Empty."), 8, 24) end
|
||||
for i, mon in ipairs(box) do
|
||||
local def = game.data.pokemon[mon.species]
|
||||
Font.draw(mon.nickname or (def and def.name) or tostring(mon.species),
|
||||
8, 14 + i * 10)
|
||||
Font.draw(Strings(":L%d No.%03d", mon.level or 0,
|
||||
def and def.dex or 0), 88, 14 + i * 10)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end)
|
||||
game.stack:push(TextBox.new(game, saved
|
||||
and Strings("Printed BOX %d!\fSaved as\n%s\vin the save\nfolder.",
|
||||
game.save.currentBox or 1, saved)
|
||||
or Strings("Printer error!\n%s", tostring(err))))
|
||||
end
|
||||
|
||||
function BoxMenu.new(game)
|
||||
Boxes.ensure(game.save)
|
||||
-- bills_pc.asm BillsPCMenu: TextBoxBorder at (0,0) with interior
|
||||
-- 12x10 → total 14x12. "CHANGE BOX" / "WITHDRAW <PK><MN>" need the
|
||||
-- full interior (cursor col + label). keepOpen so WITHDRAW/DEPOSIT/
|
||||
-- RELEASE/CHANGE BOX leave this menu underneath (jp BillsPCMenu).
|
||||
local menu = Menu.new(game, {
|
||||
local items = {
|
||||
{ label = Strings("WITHDRAW <PK><MN>"), keepOpen = true,
|
||||
onSelect = function() withdraw(game) end },
|
||||
{ label = Strings("DEPOSIT <PK><MN>"), keepOpen = true,
|
||||
@@ -235,10 +268,19 @@ function BoxMenu.new(game)
|
||||
onSelect = function() release(game) end },
|
||||
{ label = Strings("CHANGE BOX"), keepOpen = true,
|
||||
onSelect = function() changeBox(game) end },
|
||||
{ label = Strings("SEE YA!") },
|
||||
}
|
||||
-- Yellow's PRINT BOX item (bills_pc.asm _YELLOW -> PrintPCBox): the
|
||||
-- Game Boy Printer box list becomes a PNG under prints/, like the
|
||||
-- Pokédex PRNT stand-in
|
||||
if require("src.core.GameVersion").isYellow() then
|
||||
items[#items + 1] = { label = Strings("PRINT BOX"), keepOpen = true,
|
||||
onSelect = function() printBox(game) end }
|
||||
end
|
||||
items[#items + 1] = { label = Strings("SEE YA!") }
|
||||
local menu = Menu.new(game, items,
|
||||
-- Bill's PC runs silent end to end (BIT_NO_MENU_BUTTON_SOUND,
|
||||
-- engine/menus/pokemon_pc.asm)
|
||||
}, { tx = 0, ty = 0, tw = 14, th = 12, noSound = true })
|
||||
{ tx = 0, ty = 0, tw = 14, th = #items * 2 + 2, noSound = true })
|
||||
local baseDraw = menu.draw
|
||||
function menu:draw()
|
||||
baseDraw(self)
|
||||
|
||||
+13
-7
@@ -55,11 +55,17 @@ function DexEntryMenu:update(dt)
|
||||
end
|
||||
|
||||
function DexEntryMenu:draw()
|
||||
DexEntryMenu.render(self.game, self.def, self.sprite, self.forceOwned)
|
||||
end
|
||||
|
||||
-- Static entry-page renderer, shared with the printer stand-in
|
||||
-- (src/core/Printer.lua renders the same page into a PNG the way
|
||||
-- PrintPokedexEntry rendered it to the Game Boy Printer).
|
||||
function DexEntryMenu.render(game, def, sprite, forceOwned)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
local def = self.def
|
||||
if self.sprite then
|
||||
love.graphics.draw(self.sprite, 8, math.max(0, 60 - self.sprite:getHeight()))
|
||||
if sprite then
|
||||
love.graphics.draw(sprite, 8, math.max(0, 60 - sprite:getHeight()))
|
||||
end
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(def.name, 72, 8)
|
||||
@@ -70,10 +76,10 @@ function DexEntryMenu:draw()
|
||||
Font.draw(e.kind or "?", 72, 20)
|
||||
-- same number width as the list (constants.dexDigits), so a dex past 999
|
||||
-- prints the extra digit everywhere at once
|
||||
local digits = (self.game.data.constants or {}).dexDigits or 3
|
||||
local digits = (game.data.constants or {}).dexDigits or 3
|
||||
Font.draw(("No.%0" .. digits .. "d"):format(def.dex or 0), 72, 32)
|
||||
local owned = self.forceOwned
|
||||
or (self.game.save.pokedex and self.game.save.pokedex.owned[def.id])
|
||||
local owned = forceOwned
|
||||
or (game.save.pokedex and game.save.pokedex.owned[def.id])
|
||||
-- height/weight print only once owned, like the description
|
||||
-- (pokedex.asm: "if the pokemon has not been owned, don't print the
|
||||
-- height, weight, or description")
|
||||
@@ -84,7 +90,7 @@ function DexEntryMenu:draw()
|
||||
Font.draw(Strings("HT %d′%02d″", e.heightFt, e.heightIn or 0), 72, 44)
|
||||
Font.draw(Strings("WT %.1flb", (e.weight or 0) / 10), 72, 54)
|
||||
end
|
||||
local text = owned and e.text and self.game.data.text[e.text] or nil
|
||||
local text = owned and e.text and game.data.text[e.text] or nil
|
||||
local y = 72
|
||||
if text then
|
||||
for line in (text:gsub("\v", "\n"):gsub("\f", "\n") .. "\n"):gmatch("(.-)\n") do
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
-- The dex-completion diploma (engine/events/diploma.asm DisplayDiploma /
|
||||
-- diploma2.asm DisplayDiplomaTop): a bordered certificate page with the
|
||||
-- player's name, shown by the Celadon Mansion 3F game designer once 150
|
||||
-- species are owned. Diploma.render also backs the Yellow-only printed
|
||||
-- copy (engine/printer/printer.asm PrintDiploma -> src/core/Printer.lua).
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local Strings = require("src.core.Strings")
|
||||
|
||||
local Diploma = {}
|
||||
Diploma.__index = Diploma
|
||||
Diploma.isOpaque = true
|
||||
|
||||
function Diploma.new(game, onDone)
|
||||
return setmetatable({ game = game, onDone = onDone }, Diploma)
|
||||
end
|
||||
|
||||
function Diploma:update()
|
||||
local input = self.game.input
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
self.game.stack:pop()
|
||||
if self.onDone then self.onDone() end
|
||||
end
|
||||
end
|
||||
|
||||
-- the DisplayDiplomaTop layout, hlcoord tiles kept as x*8 / y*8 pixels
|
||||
function Diploma.render(game)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("line", 2.5, 2.5, 155, 139)
|
||||
Font.draw(Strings("<Diploma>"), 40, 16) -- hlcoord 5,2
|
||||
Font.draw(Strings("Player"), 24, 32) -- hlcoord 3,4
|
||||
Font.draw(game.save.player.name or "RED", 80, 32) -- hlcoord 10,4
|
||||
local congrats = { -- hlcoord 2,6
|
||||
"Congrats! This", "diploma certifies", "that you have",
|
||||
"completed your", "POKéDEX.",
|
||||
}
|
||||
for i, line in ipairs(congrats) do
|
||||
Font.draw(Strings(line), 16, 48 + (i - 1) * 10)
|
||||
end
|
||||
Font.draw(Strings("GAME FREAK"), 72, 128) -- hlcoord 9,16
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function Diploma:draw()
|
||||
Diploma.render(self.game)
|
||||
end
|
||||
|
||||
return Diploma
|
||||
@@ -104,6 +104,7 @@ PartyMenu.iconFrames = {
|
||||
FAIRY = { rest = 3, alt = 0 }, -- FairySprite tile 12 <-> tile 0
|
||||
BIRD = { rest = 3, alt = 0 }, -- BirdSprite tile 12 <-> tile 0
|
||||
WATER = { rest = 0, alt = 3 }, -- SeelSprite tile 0 <-> tile 12
|
||||
PIKACHU = { rest = 0, alt = 3 }, -- Yellow: PikachuSprite tile 0 <-> 12
|
||||
}
|
||||
|
||||
-- Which 16x16 frame of `name`'s sheet to draw; `ih` (sheet pixel
|
||||
|
||||
+29
-3
@@ -57,7 +57,7 @@ function PokedexMenu.new(game, opts)
|
||||
-- original, QUIT returns to the list
|
||||
local Menu = require("src.ui.Menu")
|
||||
local Screens = require("src.ui.Screens")
|
||||
game.stack:push(Menu.new(game, {
|
||||
local entries = {
|
||||
{ label = Strings("DATA"), onSelect = function()
|
||||
Screens.push(game, "DexEntryMenu", item.value)
|
||||
end },
|
||||
@@ -67,8 +67,34 @@ function PokedexMenu.new(game, opts)
|
||||
{ label = Strings("AREA"), onSelect = function()
|
||||
Screens.push(game, "TownMap", { nestSpecies = item.value })
|
||||
end },
|
||||
{ label = Strings("QUIT") },
|
||||
}, { tx = 12, ty = 8, tw = 8, th = 10 }))
|
||||
}
|
||||
-- Yellow's PRNT item (engine/menus/pokedex.asm PokedexMenuItemsText
|
||||
-- _YELLOW branch -> PrintPokedexEntry): the Game Boy Printer job is
|
||||
-- stood in for by a PNG of the entry page saved under prints/.
|
||||
if require("src.core.GameVersion").isYellow() then
|
||||
entries[#entries + 1] = { label = Strings("PRNT"), onSelect = function()
|
||||
local DexEntryMenu = require("src.ui.DexEntryMenu")
|
||||
local Printer = require("src.core.Printer")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local def = game.data.pokemon[item.value]
|
||||
local path = require("src.pokemon.Sprites").path(
|
||||
game.data, item.value, "front", { kind = "dex" })
|
||||
local ok, sprite = false, nil
|
||||
if path then ok, sprite = pcall(love.graphics.newImage, path) end
|
||||
local saved, err = Printer.save("dex_" .. item.value, 160, 144,
|
||||
function()
|
||||
DexEntryMenu.render(game, def, ok and sprite or nil, false)
|
||||
end)
|
||||
game.stack:push(TextBox.new(game, saved
|
||||
and Strings("Printed %s's\ndata!\fSaved as\n%s\vin the save\nfolder.",
|
||||
def.name, saved)
|
||||
or Strings("Printer error!\n%s", tostring(err))))
|
||||
end }
|
||||
end
|
||||
entries[#entries + 1] = { label = Strings("QUIT") }
|
||||
game.stack:push(Menu.new(game, entries,
|
||||
{ tx = 12, ty = 8, tw = 8,
|
||||
th = #entries * 2 + 2 }))
|
||||
end,
|
||||
})
|
||||
list.sgbPalettes = PokedexMenu.sgbPalettes
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
-- Surfing Pikachu minigame (engine/minigame/surfing_pikachu.asm): the
|
||||
-- Summer Beach House wave run. Paddle for speed, launch off the wave,
|
||||
-- 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
|
||||
-- rotations); high score persists in save.surfingHighScore for the
|
||||
-- beach-house printer.
|
||||
|
||||
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 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
|
||||
local GRAVITY = 0.14
|
||||
local HORIZON = 24 -- sea starts under the sky strip
|
||||
|
||||
-- surf_1b quads: {x, y, w, h} in sheet pixels (pose pitch is 24x24)
|
||||
local B = {
|
||||
digits = { x = 0, y = 104 }, -- "0123456789", 8x8 each
|
||||
good = { 0, 72, 32, 8 },
|
||||
yeah = { 32, 72, 32, 8 },
|
||||
ohno = { 80, 96, 48, 24 },
|
||||
splash = { 48, 80, 32, 24 },
|
||||
cloud = { 96, 112, 32, 8 },
|
||||
paddle = { { 0, 80, 24, 24 }, { 24, 80, 24, 24 } },
|
||||
}
|
||||
-- rotation frames, 45-degree buckets clockwise from upright
|
||||
local POSES = {
|
||||
[0] = { 48, 0, 24, 24 }, -- upright ride
|
||||
[45] = { 24, 0, 24, 24 }, -- nose down
|
||||
[90] = { 0, 48, 24, 24 }, -- board vertical
|
||||
[135] = { 48, 48, 24, 24 }, -- tumbling
|
||||
[180] = { 72, 48, 24, 24 }, -- upside down
|
||||
[225] = { 48, 48, 24, 24 },
|
||||
[270] = { 0, 48, 24, 24 },
|
||||
[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
|
||||
}
|
||||
|
||||
-- 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)
|
||||
local SEA_PAL = { { 255, 255, 255 }, { 112, 184, 248 },
|
||||
{ 56, 120, 216 }, { 0, 0, 0 } }
|
||||
local PIKA_PAL = { { 255, 255, 255 }, { 248, 216, 64 },
|
||||
{ 224, 144, 32 }, { 0, 0, 0 } }
|
||||
|
||||
local function newQuad(spec, img)
|
||||
return love.graphics.newQuad(spec[1], spec[2], spec[3], spec[4],
|
||||
img:getDimensions())
|
||||
end
|
||||
|
||||
function SurfingMinigame.new(game, onDone)
|
||||
local self = setmetatable({ game = game, onDone = onDone }, SurfingMinigame)
|
||||
self.phase = "ride" -- ride | air | wipeout | results
|
||||
self.t = 0
|
||||
self.distance = 0
|
||||
self.speed = 2
|
||||
self.score = 0
|
||||
self.rideTick = 0
|
||||
self.y = 0 -- air offset above the wave (positive = up)
|
||||
self.vy = 0
|
||||
self.rot = 0 -- degrees, accumulates through the air
|
||||
self.spins = 0
|
||||
self.airFrames = 0
|
||||
self.resultShown = 0
|
||||
self.banner = nil -- {quad, frames}: GOOD!/YEAH-/Oh no..
|
||||
|
||||
local function sheet(path)
|
||||
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.aq = {}
|
||||
for k, spec in pairs(A) do self.aq[k] = newQuad(spec, self.bg) end
|
||||
end
|
||||
if self.ob then
|
||||
self.bq = {}
|
||||
for k, spec in pairs(B) do
|
||||
if spec[3] then self.bq[k] = newQuad(spec, self.ob) end
|
||||
end
|
||||
self.bq.paddle = { newQuad(B.paddle[1], self.ob),
|
||||
newQuad(B.paddle[2], self.ob) }
|
||||
self.bq.poses = {}
|
||||
for deg, spec in pairs(POSES) do
|
||||
self.bq.poses[deg] = newQuad(spec, self.ob)
|
||||
end
|
||||
self.bq.digit = {}
|
||||
for d = 0, 9 do
|
||||
self.bq.digit[d] = love.graphics.newQuad(B.digits.x + d * 8,
|
||||
B.digits.y, 8, 8, self.ob:getDimensions())
|
||||
end
|
||||
end
|
||||
Music.play(game.data, "Music_SurfingPikachu")
|
||||
return self
|
||||
end
|
||||
|
||||
-- crest height at screen x for the current scroll (two sines so the
|
||||
-- wave rolls instead of looping visibly)
|
||||
function SurfingMinigame:seaY(x)
|
||||
local s = self.distance + x
|
||||
return 92 - 14 * math.sin(s / 26) - 6 * math.sin(s / 9.5)
|
||||
end
|
||||
|
||||
function SurfingMinigame:finishRun()
|
||||
self.phase = "results"
|
||||
local save = self.game.save
|
||||
self.newRecord = self.score > (save.surfingHighScore or 0)
|
||||
if self.newRecord then save.surfingHighScore = self.score end
|
||||
Music.stop()
|
||||
Sound.play(self.game.data, self.newRecord and "Get_Item1" or "Ball_Poof")
|
||||
end
|
||||
|
||||
function SurfingMinigame:update()
|
||||
local input = self.game.input
|
||||
self.t = self.t + 1
|
||||
if self.banner then
|
||||
self.banner.frames = self.banner.frames - 1
|
||||
if self.banner.frames <= 0 then self.banner = nil end
|
||||
end
|
||||
if self.phase == "results" then
|
||||
self.resultShown = self.resultShown + 1
|
||||
if self.resultShown > 30
|
||||
and (input:wasPressed("a") or input:wasPressed("b")) then
|
||||
self.game.stack:pop()
|
||||
if self.onDone then self.onDone(self.score) end
|
||||
end
|
||||
return
|
||||
end
|
||||
if self.phase == "wipeout" then
|
||||
self.splash = (self.splash or 0) + 1
|
||||
if self.splash > 70 then self:finishRun() end
|
||||
return
|
||||
end
|
||||
|
||||
-- the wave scrolls by the current speed; the beach ends the run
|
||||
self.distance = self.distance + 0.8 + self.speed * 0.35
|
||||
if self.distance >= RUN_DISTANCE then
|
||||
-- rode it all the way in: distance bonus like the original's goal
|
||||
self.score = self.score + 500
|
||||
self:finishRun()
|
||||
return
|
||||
end
|
||||
|
||||
if self.phase == "ride" then
|
||||
-- paddling: mash A for speed, it bleeds off on its own
|
||||
if input:wasPressed("a") and self.speed < 8 then
|
||||
self.speed = self.speed + 1
|
||||
end
|
||||
if self.t % 45 == 0 and self.speed > 2 then
|
||||
self.speed = self.speed - 1
|
||||
end
|
||||
self.rideTick = self.rideTick + 1
|
||||
if self.rideTick % 12 == 0 then self.score = self.score + 1 end
|
||||
-- launch off the lip
|
||||
if input:wasPressed("up") then
|
||||
self.phase = "air"
|
||||
self.vy = 1.6 + self.speed * 0.45
|
||||
self.rot, self.spins, self.airFrames = 0, 0, 0
|
||||
Sound.play(self.game.data, "Ledge_Jump")
|
||||
end
|
||||
elseif self.phase == "air" then
|
||||
self.airFrames = self.airFrames + 1
|
||||
self.vy = self.vy - GRAVITY
|
||||
self.y = self.y + self.vy
|
||||
-- tricks: hold either direction to spin
|
||||
local spin = (input:isDown("left") and -6 or 0)
|
||||
+ (input:isDown("right") and 6 or 0)
|
||||
self.rot = self.rot + spin
|
||||
if math.abs(self.rot) >= (self.spins + 1) * 360 then
|
||||
self.spins = self.spins + 1
|
||||
end
|
||||
if self.y <= 0 and self.vy < 0 then
|
||||
self.y = 0
|
||||
local tilt = math.abs(self.rot) % 360
|
||||
if tilt <= 60 or tilt >= 300 then
|
||||
-- clean landing: airtime + full rotations pay out
|
||||
self.score = self.score + self.spins * 100
|
||||
+ math.floor(self.airFrames / 4)
|
||||
self.phase = "ride"
|
||||
self.banner = { quad = self.spins > 0 and "yeah" or "good",
|
||||
frames = 50 }
|
||||
Sound.play(self.game.data, "Cut")
|
||||
else
|
||||
self.phase = "wipeout"
|
||||
self.splash = 0
|
||||
self.banner = { quad = "ohno", frames = 70 }
|
||||
Sound.play(self.game.data, "Faint_Fall")
|
||||
end
|
||||
end
|
||||
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 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
|
||||
return zones
|
||||
end
|
||||
|
||||
function SurfingMinigame:drawScore(x, y, n)
|
||||
local s = tostring(n)
|
||||
for i = 1, #s do
|
||||
love.graphics.draw(self.ob, self.bq.digit[tonumber(s:sub(i, i))],
|
||||
x + (i - 1) * 8, y)
|
||||
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.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
|
||||
-- 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
|
||||
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)
|
||||
else
|
||||
local quad
|
||||
if self.phase == "ride" and self.speed <= 2
|
||||
and self.distance < 120 then
|
||||
quad = self.bq.paddle[math.floor(self.t / 8) % 2 + 1]
|
||||
else
|
||||
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)
|
||||
end
|
||||
|
||||
-- banner beats: GOOD! / YEAH- / Oh no..
|
||||
if self.banner and self.bq[self.banner.quad] then
|
||||
love.graphics.draw(self.ob, self.bq[self.banner.quad], 60, 40)
|
||||
end
|
||||
|
||||
-- score strip, bottom right: HP: + sheet digits
|
||||
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)
|
||||
|
||||
if self.phase == "results" then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 20, 48, 120, 48)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("line", 20.5, 48.5, 119, 47)
|
||||
Font.draw(Strings("SCORE %d", self.score), 32, 56)
|
||||
if self.newRecord then
|
||||
Font.draw(Strings("New record!"), 32, 68)
|
||||
else
|
||||
Font.draw(Strings("HI %d", self.game.save.surfingHighScore or 0),
|
||||
32, 68)
|
||||
end
|
||||
if self.resultShown > 30 then
|
||||
Font.draw(Strings("A: done"), 32, 82)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
return SurfingMinigame
|
||||
+212
-44
@@ -33,11 +33,25 @@ end
|
||||
|
||||
function TitleState:sgbPalettes(game)
|
||||
local P = require("src.render.PaletteFX")
|
||||
local z = {
|
||||
P.zone(P.pal(game.data, "LOGO2"), 0, 0, 19, 7),
|
||||
P.zone(withPureWhite(P.pal(game.data, "LOGO1")), 0, 8, 19, 9),
|
||||
P.zone(P.pal(game.data, "MEWMON"), 0, 10, 19, 17),
|
||||
}
|
||||
local z
|
||||
if self.yellowLayout then
|
||||
-- Yellow's BlkPacket_Titlescreen (pokeyellow data/sgb/sgb_packets.asm):
|
||||
-- rows 0-7 logo band pal 0 (PAL_LOGO2), rows 8-17 Pikachu + copyright
|
||||
-- pal 2 (PAL_MEWMON), then the two bubble-tail cells at (9,8)-(10,8)
|
||||
-- back on pal 0. No Red/Blue LOGO1 ribbon band.
|
||||
local logoPal = P.pal(game.data, "LOGO2")
|
||||
z = {
|
||||
P.zone(logoPal, 0, 0, 19, 7),
|
||||
P.zone(P.pal(game.data, "MEWMON"), 0, 8, 19, 17),
|
||||
P.zone(logoPal, 9, 8, 10, 8),
|
||||
}
|
||||
else
|
||||
z = {
|
||||
P.zone(P.pal(game.data, "LOGO2"), 0, 0, 19, 7),
|
||||
P.zone(withPureWhite(P.pal(game.data, "LOGO1")), 0, 8, 19, 9),
|
||||
P.zone(P.pal(game.data, "MEWMON"), 0, 10, 19, 17),
|
||||
}
|
||||
end
|
||||
local top = game.stack and game.stack:top()
|
||||
local box = top and top.titleUiBox
|
||||
if box then
|
||||
@@ -61,6 +75,14 @@ local BLUE_CYCLE_SPECIES = {
|
||||
"VULPIX", "CHANSEY", "AERODACTYL", "JOLTEON", "SNORLAX",
|
||||
"GLOOM", "POLIWAG", "DODUO", "PORYGON", "GENGAR", "RAICHU",
|
||||
}
|
||||
-- Yellow has no TitleMons table (engine/movie/title_yellow.asm is a fixed
|
||||
-- Pikachu title). Until field.title.cycleSpecies is imported, keep a short
|
||||
-- Pikachu-centric list so the Red/Blue cycling UI still has something to show.
|
||||
local YELLOW_CYCLE_SPECIES = {
|
||||
"PIKACHU", "EEVEE", "BULBASAUR", "CHARMANDER", "SQUIRTLE",
|
||||
"JIGGLYPUFF", "MEOWTH", "PSYDUCK", "VULPIX", "ABRA",
|
||||
"GROWLITHE", "CUBONE", "GASTLY", "HITMONLEE", "SNORLAX", "DRAGONITE",
|
||||
}
|
||||
local CYCLE_FRAMES = 240 -- the original waits ~4s between picks
|
||||
|
||||
local function tryImage(path)
|
||||
@@ -93,9 +115,36 @@ function TitleState.new(game, opts)
|
||||
or "assets/generated/title/red_version.png")
|
||||
self.player = tryImage("assets/generated/title/player.png")
|
||||
self.blue = GameVersion.isBlue()
|
||||
-- Blue cycles its own title mons and prints its ribbon contiguously; a
|
||||
-- field.title.cycleSpecies override (mods / total conversions) still wins.
|
||||
local defaultCycle = self.blue and BLUE_CYCLE_SPECIES or CYCLE_SPECIES
|
||||
self.yellow = GameVersion.isYellow()
|
||||
or title.layout == "yellow_pikachu"
|
||||
-- Yellow title is a fixed Pikachu composition (title_yellow.asm), not
|
||||
-- TitleMons cycling. Prefer composed pikachu.png from the Yellow import.
|
||||
self.yellowPikachu = self.yellow and tryImage(imagePath(title.pikachu)
|
||||
or "assets/generated/title/pikachu.png") or nil
|
||||
self.yellowBubble = self.yellow and tryImage(imagePath(title.pikaBubble)
|
||||
or "assets/generated/title/pika_bubble.png") or nil
|
||||
self.yellowLayout = self.yellow and self.yellowPikachu ~= nil
|
||||
if self.yellowLayout then
|
||||
-- title.asm boot: hSCY starts at $40 with the logo parked above the
|
||||
-- viewport; .bouncePokemonLogoLoop drops it in with an overshoot
|
||||
-- bounce, then the whoosh, the speech bubble, and PikachuCry1 before
|
||||
-- the title music starts. Blink overlays are the OB tile swaps of
|
||||
-- DoTitleScreenFunction.
|
||||
self.eyesHalf = tryImage("assets/generated/title/eyes_half.png")
|
||||
self.eyesClosed = tryImage("assets/generated/title/eyes_closed.png")
|
||||
self.scy = 0x40
|
||||
self.phase = "drop"
|
||||
self.dropStep, self.dropLeft = 1, nil
|
||||
self.showBubble = false
|
||||
self.blinkTimer = 0
|
||||
self.blinkAt = nil
|
||||
else
|
||||
self.phase = "loop"
|
||||
self.showBubble = true
|
||||
end
|
||||
local defaultCycle = self.yellowLayout and { "PIKACHU" }
|
||||
or (self.yellow and YELLOW_CYCLE_SPECIES)
|
||||
or (self.blue and BLUE_CYCLE_SPECIES or CYCLE_SPECIES)
|
||||
self.cycleSpecies = (type(title.cycleSpecies) == "table"
|
||||
and #title.cycleSpecies > 0)
|
||||
and title.cycleSpecies or defaultCycle
|
||||
@@ -107,6 +156,14 @@ function TitleState.new(game, opts)
|
||||
end
|
||||
|
||||
function TitleState:enter()
|
||||
-- Yellow defers the title theme until after the logo drop and
|
||||
-- Pikachu's cry (title.asm plays MUSIC_TITLE_SCREEN only after
|
||||
-- WaitForSoundToFinish on PikachuCry1)
|
||||
if self.yellowLayout then return end
|
||||
self:startMusic()
|
||||
end
|
||||
|
||||
function TitleState:startMusic()
|
||||
local data = self.game.data
|
||||
local song = self.title.music or "Music_TitleScreen"
|
||||
if data.audio and data.audio.songs and data.audio.songs[song] then
|
||||
@@ -114,6 +171,83 @@ function TitleState:enter()
|
||||
end
|
||||
end
|
||||
|
||||
-- .TitleScreenPokemonLogoYScrolls: { dy per frame, frames }; the -3
|
||||
-- rebound step lands with SFX_INTRO_CRASH
|
||||
local DROP_STEPS = {
|
||||
{ -4, 16 }, { 3, 4 }, { -3, 4 }, { 2, 2 }, { -2, 2 }, { 1, 2 }, { -1, 2 },
|
||||
}
|
||||
|
||||
-- the boot cinematic up to the interactive loop; one call per frame
|
||||
function TitleState:updateSequence()
|
||||
local Sound = require("src.core.Sound")
|
||||
local data = self.game.data
|
||||
if self.phase == "drop" then
|
||||
local step = DROP_STEPS[self.dropStep]
|
||||
if not step then
|
||||
self.phase = "settle"
|
||||
self.timer = 0
|
||||
return
|
||||
end
|
||||
if self.dropLeft == nil then
|
||||
self.dropLeft = step[2]
|
||||
if step[1] == -3 then Sound.play(data, "Intro_Crash") end
|
||||
end
|
||||
self.scy = self.scy + step[1]
|
||||
self.dropLeft = self.dropLeft - 1
|
||||
if self.dropLeft <= 0 then
|
||||
self.dropStep = self.dropStep + 1
|
||||
self.dropLeft = nil
|
||||
end
|
||||
elseif self.phase == "settle" then
|
||||
-- ld c, 36 / DelayFrames, then the whoosh and the bubble
|
||||
self.timer = self.timer + 1
|
||||
if self.timer >= 36 then
|
||||
Sound.play(data, "Intro_Whoosh")
|
||||
self.showBubble = true
|
||||
self.phase = "bubble"
|
||||
self.timer = 0
|
||||
end
|
||||
elseif self.phase == "bubble" then
|
||||
self.timer = self.timer + 1
|
||||
if self.timer >= 3 then
|
||||
self.crySrc = Sound.playPikaCry(data, 1)
|
||||
self.phase = "cry"
|
||||
self.timer = 0
|
||||
end
|
||||
elseif self.phase == "cry" then
|
||||
-- WaitForSoundToFinish before the music starts
|
||||
self.timer = self.timer + 1
|
||||
local playing = self.crySrc and self.crySrc.isPlaying
|
||||
and self.crySrc:isPlaying()
|
||||
if not playing or self.timer > 180 then
|
||||
self.crySrc = nil
|
||||
self:startMusic()
|
||||
self.phase = "loop"
|
||||
self.blinkTimer = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- DoTitleScreenFunction.CheckTimer: an 8-bit frame counter blinks at 0,
|
||||
-- $80 and $90; the blink itself runs half/closed/half over 9 frames
|
||||
function TitleState:updateBlink()
|
||||
local t = self.blinkTimer
|
||||
self.blinkTimer = (t + 1) % 256
|
||||
if t == 0 or t == 0x80 or t == 0x90 then self.blinkAt = 0 end
|
||||
if self.blinkAt then
|
||||
self.blinkAt = self.blinkAt + 1
|
||||
if self.blinkAt > 9 then self.blinkAt = nil end
|
||||
end
|
||||
end
|
||||
|
||||
-- the blink overlay for this frame (nil = open eyes)
|
||||
function TitleState:blinkOverlay()
|
||||
local at = self.blinkAt
|
||||
if not at then return nil end
|
||||
if at <= 3 or at > 6 then return self.eyesHalf end
|
||||
return self.eyesClosed
|
||||
end
|
||||
|
||||
function TitleState:currentSprite()
|
||||
local species = self.cycleSpecies[self.cycleIndex]
|
||||
local cached = self.sprites[species]
|
||||
@@ -219,9 +353,26 @@ function TitleState:openMenu()
|
||||
end
|
||||
|
||||
function TitleState:update(dt)
|
||||
if self.yellowLayout then
|
||||
if self.phase ~= "loop" then
|
||||
self:updateSequence()
|
||||
return -- input is ignored until the cinematic lands (title.asm)
|
||||
end
|
||||
self:updateBlink()
|
||||
local input = self.game.input
|
||||
if input:wasPressed("start") or input:wasPressed("a") then
|
||||
-- .go_to_main_menu voices PikachuCry11 on the way out
|
||||
local Sound = require("src.core.Sound")
|
||||
if not Sound.playPikaCry(self.game.data, 11) then
|
||||
Sound.playCry(self.game.data, "PIKACHU")
|
||||
end
|
||||
self:openMenu()
|
||||
end
|
||||
return
|
||||
end
|
||||
self.timer = self.timer + 1
|
||||
self.blink = (self.blink + 1) % 60
|
||||
if self.timer >= CYCLE_FRAMES then
|
||||
if not self.yellowLayout and self.timer >= CYCLE_FRAMES then
|
||||
self.timer = 0
|
||||
-- random pick that never repeats the current one
|
||||
if #self.cycleSpecies > 1 then
|
||||
@@ -238,9 +389,11 @@ function TitleState:update(dt)
|
||||
end
|
||||
local input = self.game.input
|
||||
if input:wasPressed("start") or input:wasPressed("a") then
|
||||
-- the title mon cries when you leave the title (.finishedWaiting)
|
||||
-- the title mon cries when you leave the title (.finishedWaiting);
|
||||
-- Yellow's fixed Pikachu title always cries Pikachu.
|
||||
require("src.core.Sound").playCry(self.game.data,
|
||||
self.cycleSpecies[self.cycleIndex])
|
||||
self.yellowLayout and "PIKACHU"
|
||||
or self.cycleSpecies[self.cycleIndex])
|
||||
self:openMenu()
|
||||
end
|
||||
end
|
||||
@@ -248,50 +401,65 @@ end
|
||||
-- The original tilemap (engine/movie/title.asm): logo at tile (2,1),
|
||||
-- the version ribbon at (7,8), Red's title art as OAM at px (82,80),
|
||||
-- the title mon in the 7x7 box at tile (5,10), copyright on row 17.
|
||||
-- Yellow (title_yellow.asm): logo (2,1), speech bubble (6,4), Pikachu
|
||||
-- (4,8) 12x9 — no version ribbon, no cycling mon, no Red OAM.
|
||||
function TitleState:draw()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
local scrollY = self.yellowLayout and -(self.scy or 0) or 0
|
||||
if self.logo then
|
||||
love.graphics.draw(self.logo, 16, 8)
|
||||
love.graphics.draw(self.logo, 16, 8 + scrollY)
|
||||
else
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(self.blue and "POKéMON BLUE" or Strings("POKéMON RED"),
|
||||
(160 - 12 * 8) / 2, 24)
|
||||
local brand = self.yellow and "POKéMON YELLOW"
|
||||
or (self.blue and "POKéMON BLUE" or Strings("POKéMON RED"))
|
||||
Font.draw(brand, (160 - 12 * 8) / 2, 24 + scrollY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
if self.version then
|
||||
local iw, ih = self.version:getDimensions()
|
||||
if self.blue then
|
||||
-- Blue prints its ribbon contiguously ("Blue Version", hlcoord 7,8).
|
||||
-- The extracted strip packs those eight glyph tiles into image tiles
|
||||
-- 0..7 (tiles 8..9 are blank), so draw that 64px run at px (56, 64).
|
||||
love.graphics.draw(self.version,
|
||||
love.graphics.newQuad(0, 0, 64, 8, iw, ih), 56, 64)
|
||||
else
|
||||
-- Red's strip holds Red+Green+Version glyphs; the tilemap prints
|
||||
-- tiles $60,$61 ("Red"), a space, then $65-$69 ("Version").
|
||||
love.graphics.draw(self.version,
|
||||
love.graphics.newQuad(0, 0, 16, 8, iw, ih), 56, 64)
|
||||
love.graphics.draw(self.version,
|
||||
love.graphics.newQuad(40, 0, 40, 8, iw, ih), 80, 64)
|
||||
if self.yellowLayout then
|
||||
-- everything scrolls together through the logo drop (rSCY): screen
|
||||
-- y = BG y - SCY, so the composition rides at -scy until it lands
|
||||
local dy = scrollY
|
||||
if self.yellowBubble and self.showBubble then
|
||||
love.graphics.draw(self.yellowBubble, 48, 32 + dy)
|
||||
end
|
||||
-- hlcoord 4,8 → px (32, 64); composed 13x9 tile sprite
|
||||
love.graphics.draw(self.yellowPikachu, 32, 64 + dy)
|
||||
local overlay = self:blinkOverlay()
|
||||
if overlay then
|
||||
-- the eye OAM band sits at (56,80) on the landed screen
|
||||
love.graphics.draw(overlay, 32 + 24, 64 + 16 + dy)
|
||||
end
|
||||
else
|
||||
-- Yellow's Version_GFX slot holds a leftover "Blue Version" ribbon
|
||||
-- (pokeyellow gfx/title/blue_version.png, unreferenced by title code);
|
||||
-- the Yellow fallback layout draws no ribbon at all.
|
||||
if self.version and not self.yellow then
|
||||
local iw, ih = self.version:getDimensions()
|
||||
if self.blue then
|
||||
love.graphics.draw(self.version,
|
||||
love.graphics.newQuad(0, 0, 64, 8, iw, ih), 56, 64)
|
||||
else
|
||||
love.graphics.draw(self.version,
|
||||
love.graphics.newQuad(0, 0, 16, 8, iw, ih), 56, 64)
|
||||
love.graphics.draw(self.version,
|
||||
love.graphics.newQuad(40, 0, 40, 8, iw, ih), 80, 64)
|
||||
end
|
||||
end
|
||||
local sprite = self:currentSprite()
|
||||
if sprite then
|
||||
local w, h = sprite:getDimensions()
|
||||
local slide = (self.slideIn or 0) * 8
|
||||
love.graphics.draw(sprite, 40 + math.floor((56 - w) / 2) + slide,
|
||||
136 - h)
|
||||
end
|
||||
if self.player then
|
||||
love.graphics.draw(self.player, 82, 80)
|
||||
end
|
||||
end
|
||||
local sprite = self:currentSprite()
|
||||
if sprite then
|
||||
local w, h = sprite:getDimensions()
|
||||
local slide = (self.slideIn or 0) * 8 -- scroll in from the right
|
||||
-- bottom-aligned and centered in the (5,10)-(11,16) tile box
|
||||
love.graphics.draw(sprite, 40 + math.floor((56 - w) / 2) + slide,
|
||||
136 - h)
|
||||
end
|
||||
-- Red is OAM in the original: he draws over the mon's box edge
|
||||
if self.player then
|
||||
love.graphics.draw(self.player, 82, 80)
|
||||
end
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
-- the copyright row (tile 2,17); copyrightText because field.title's
|
||||
-- copyright key already names the extracted image strip
|
||||
Font.draw(self.title.copyrightText or Strings("2026 bois club games"), 1, 136)
|
||||
Font.draw(self.title.copyrightText or Strings("2026 bois club games"),
|
||||
1, 136 + scrollY)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
-- Yellow's boot attract movie, a faithful port of PlayIntroScene
|
||||
-- (pokeyellow engine/movie/intro_yellow.asm) over the extracted atlases
|
||||
-- (gfx/intro/yellow_intro_1.2bpp -> intro/yellow_intro_1.png, atlas1,
|
||||
-- 16x8 tiles; yellow_intro_2.2bpp -> yellow_intro_2.png, atlas2, 16x16
|
||||
-- tiles; clouds.2bpp -> intro/clouds.png, two 4-tile frames).
|
||||
--
|
||||
-- Faithful pieces: the 18-scene jumptable with its 128/88-frame timers,
|
||||
-- the animated-object system (YellowIntro_AnimatedObjectSpawnStateData /
|
||||
-- Jumptable / FramesData / OAMData, data/sprite_anims/intro_frames.asm +
|
||||
-- intro_oam.asm), the scene-7 per-scanline SCY sine wave
|
||||
-- (YellowIntro_Copy8BitSineWave, +-4px period 32, rotated 1 line/frame),
|
||||
-- the scene-3 SCX ramp to $68, the scene-11 cloud tile flip every 8
|
||||
-- frames, and the scene-14/15/16 BGP strobe / fade sequences
|
||||
-- (YellowIntroPalSequence_f9dd6 / _f9e0a). BGP composes with the SGB
|
||||
-- colorization through sgbPalettes (PalPacket_Generic = MEWMON,
|
||||
-- PalPacket_PikachusBeach = PIKACHUS_BEACH), like the title screen.
|
||||
--
|
||||
-- Deliberately dropped: the CGB-only OBJ-palette pokes of scenes 7/11
|
||||
-- (Func_f98a2 / Func_f98cb recolor 5-6 tiles of the surf/fly sprite),
|
||||
-- OBP-vs-BGP divergence during the strobes (one whole-screen shade map
|
||||
-- stands in for both), and the never-spawned objects $0/$4 (dead code,
|
||||
-- intro_yellow.asm:173).
|
||||
--
|
||||
-- Any of A/B/START skips the whole movie (PlayIntroScene:16-19). Pops
|
||||
-- itself and calls onDone() when finished or skipped.
|
||||
|
||||
local Music = require("src.core.Music")
|
||||
|
||||
local YellowIntro = {}
|
||||
YellowIntro.__index = YellowIntro
|
||||
YellowIntro.isOpaque = true
|
||||
|
||||
-- ------- data tables (data/sprite_anims/intro_oam.asm) ----------------
|
||||
|
||||
-- OAM lists: rows of { dy, dx, tileDelta, flip }
|
||||
local function grid(rows, cols, dy0, dx0, tileForRC)
|
||||
local list = {}
|
||||
for r = 0, rows - 1 do
|
||||
for c = 0, cols - 1 do
|
||||
list[#list + 1] = { dy0 + r * 8, dx0 + c * 8, tileForRC(r, c), false }
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
local OAM = {}
|
||||
|
||||
-- Unkn_fa17e: 2x2, tiles +0/+1 over +$10/+$11
|
||||
OAM.fa17e = grid(2, 2, -8, -8, function(r, c) return r * 0x10 + c end)
|
||||
|
||||
-- Unkn_fa18f: 16x32; bottom two rows mirror their left half
|
||||
OAM.fa18f = {
|
||||
{ -16, -8, 0x00 }, { -16, 0, 0x01 },
|
||||
{ -8, -8, 0x10 }, { -8, 0, 0x11 },
|
||||
{ 0, -8, 0x20 }, { 0, 0, 0x20, true },
|
||||
{ 8, -8, 0x21 }, { 8, 0, 0x21, true },
|
||||
}
|
||||
|
||||
-- Unkn_fa1b0: 32x40 (16-wide head rows, 32-wide mirrored body rows)
|
||||
OAM.fa1b0 = {
|
||||
{ -24, -8, 0x00 }, { -24, 0, 0x01 },
|
||||
{ -16, -8, 0x02 }, { -16, 0, 0x03 },
|
||||
{ -8, -16, 0x04 }, { -8, -8, 0x05 }, { -8, 0, 0x06 }, { -8, 8, 0x04, true },
|
||||
{ 0, -16, 0x07 }, { 0, -8, 0x08 }, { 0, 0, 0x08, true }, { 0, 8, 0x07, true },
|
||||
{ 8, -16, 0x09 }, { 8, -8, 0x0a }, { 8, 0, 0x0a, true }, { 8, 8, 0x09, true },
|
||||
{ 16, -16, 0x0b }, { 16, -8, 0x0c }, { 16, 0, 0x0c, true }, { 16, 8, 0x0b, true },
|
||||
}
|
||||
|
||||
-- Unkn_fa201: 6x6 = 48x48, row r uses tiles +$r0..+$r5
|
||||
OAM.fa201 = grid(6, 6, -24, -24, function(r, c) return r * 0x10 + c end)
|
||||
|
||||
-- Unkn_fa292: 5x5 = 40x40, row bases $00,$05,$10,$15,$20
|
||||
local FA292_ROW = { 0x00, 0x05, 0x10, 0x15, 0x20 }
|
||||
OAM.fa292 = {}
|
||||
for r = 0, 4 do
|
||||
for c = 0, 4 do
|
||||
OAM.fa292[#OAM.fa292 + 1] =
|
||||
{ -20 + r * 8, -16 + c * 8, FA292_ROW[r + 1] + c, false }
|
||||
end
|
||||
end
|
||||
|
||||
-- Unkn_fa2f7: 32x8 mirrored streak
|
||||
OAM.fa2f7 = {
|
||||
{ -4, -16, 0x00 }, { -4, -8, 0x01 },
|
||||
{ -4, 0, 0x01, true }, { -4, 8, 0x00, true },
|
||||
}
|
||||
|
||||
-- Unkn_fa308: two mirrored 16x16 clusters, 32px apart
|
||||
OAM.fa308 = {
|
||||
{ -8, -24, 0x00 }, { -8, -16, 0x01 },
|
||||
{ 0, -24, 0x02 }, { 0, -16, 0x03 },
|
||||
{ -8, 8, 0x01, true }, { -8, 16, 0x00, true },
|
||||
{ 0, 8, 0x03, true }, { 0, 16, 0x02, true },
|
||||
}
|
||||
|
||||
-- Unkn_fa329: two mirrored 24x16 clusters
|
||||
OAM.fa329 = {
|
||||
{ -8, -40, 0x00 }, { -8, -32, 0x01 }, { -8, -24, 0x02 },
|
||||
{ 0, -40, 0x10 }, { 0, -32, 0x11 }, { 0, -24, 0x12 },
|
||||
{ -8, 16, 0x02, true }, { -8, 24, 0x01, true }, { -8, 32, 0x00, true },
|
||||
{ 0, 16, 0x12, true }, { 0, 24, 0x11, true }, { 0, 32, 0x10, true },
|
||||
}
|
||||
|
||||
-- frameId -> { atlas2 tile offset, OAM list }
|
||||
local FRAMES = {
|
||||
[0x01] = { 0x96, OAM.fa17e }, [0x02] = { 0x98, OAM.fa17e },
|
||||
[0x03] = { 0x9a, OAM.fa17e },
|
||||
[0x04] = { 0x0c, OAM.fa18f }, [0x05] = { 0x0e, OAM.fa18f },
|
||||
[0x06] = { 0x3c, OAM.fa18f },
|
||||
[0x07] = { 0x60, OAM.fa1b0 }, [0x08] = { 0x70, OAM.fa1b0 },
|
||||
[0x09] = { 0x80, OAM.fa1b0 },
|
||||
[0x0a] = { 0x90, OAM.fa201 }, [0x0b] = { 0x00, OAM.fa201 },
|
||||
[0x0c] = { 0x06, OAM.fa201 },
|
||||
[0x0d] = { 0xc6, OAM.fa292 },
|
||||
[0x0e] = { 0x6d, OAM.fa2f7 },
|
||||
[0x0f] = { 0xf0, OAM.fa308 }, [0x10] = { 0xf4, OAM.fa308 },
|
||||
[0x11] = { 0xf8, OAM.fa308 },
|
||||
[0x12] = { 0x9c, OAM.fa329 }, [0x13] = { 0xec, OAM.fa329 },
|
||||
}
|
||||
|
||||
-- frame scripts (intro_frames.asm): { {frameId, duration}, ..., loop=bool }
|
||||
local FRAMESETS = {
|
||||
[1] = { { 0x01, 4 }, { 0x02, 4 }, { 0x03, 4 }, loop = true },
|
||||
[2] = { { 0x04, 4 }, { 0x05, 4 }, { 0x06, 4 }, loop = true },
|
||||
[3] = { { 0x07, 4 }, { 0x08, 4 }, { 0x09, 4 }, loop = true },
|
||||
[5] = { { 0x0b, 32 } },
|
||||
[6] = { { 0x0c, 32 } },
|
||||
[7] = { { 0x0d, 32 } },
|
||||
[8] = { { 0x0e, 32 } },
|
||||
[9] = { { 0x0f, 31 }, { 0x11, 2 }, { 0x0f, 2 }, { 0x11, 2 },
|
||||
{ 0x0f, 31 }, { 0x11, 2 }, { 0x0f, 23 }, { 0x10, 32 } },
|
||||
[10] = { { 0x12, 4 }, { 0x13, 4 }, loop = true },
|
||||
}
|
||||
|
||||
-- object id -> { frameset, seq } (YellowIntro_AnimatedObjectSpawnStateData;
|
||||
-- seq indexes the movement jumptable)
|
||||
local SPAWN = {
|
||||
[1] = { 1, "static" }, [2] = { 2, "static" }, [3] = { 3, "static" },
|
||||
[5] = { 5, "surf" }, [6] = { 6, "fly" }, [7] = { 7, "static" },
|
||||
[8] = { 8, "bar" }, [9] = { 9, "static" }, [10] = { 10, "static" },
|
||||
}
|
||||
|
||||
-- speed-bar spawn rows (YellowIntroFlyingSpeedBarData; first byte is X --
|
||||
-- the source's "; y, x, speed" comment is wrong)
|
||||
local SPEED_BARS = {
|
||||
{ 0xD0, 0x20, 2 }, { 0xF0, 0x30, 4 }, { 0xD0, 0x40, 6 },
|
||||
{ 0xC0, 0x50, 8 }, { 0xE0, 0x60, 8 }, { 0xC0, 0x70, 6 },
|
||||
{ 0xE0, 0x80, 4 }, { 0xF0, 0x90, 2 },
|
||||
}
|
||||
|
||||
-- scene-6 sine (YellowIntro_Copy8BitSineWave.SineWave), signed SCY deltas
|
||||
local WAVE = { 0, 0, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 0,
|
||||
0, 0, -1, -2, -2, -3, -3, -3, -4, -3, -3, -3, -2, -2, -1, 0 }
|
||||
|
||||
-- scene-10 BG tilemaps (gfx/intro/unknown_f9b6e/f9be6/f9bf2.tilemap)
|
||||
local SKY_MAP = {
|
||||
{ 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x60,0x61,0x62 },
|
||||
{ 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x62,0x00,0x00,0x00 },
|
||||
{ 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x62,0x00,0x00,0x00,0x00 },
|
||||
{ 0x60,0x61,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x62,0x00,0x00,0x00,0x00,0x00 },
|
||||
{ 0x00,0x00,0x63,0x60,0x61,0x60,0x61,0x02,0x02,0x02,0x02,0x60,0x61,0x62,0x00,0x00,0x00,0x00,0x00,0x00 },
|
||||
{ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x63,0x62,0x63,0x62,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 },
|
||||
}
|
||||
local BADGE_MAP = {
|
||||
{ 0x30, 0x31, 0x32, 0x33 }, { 0x40, 0x41, 0x42, 0x43 },
|
||||
{ 0x50, 0x51, 0x52, 0x53 },
|
||||
}
|
||||
local MARK_MAP = { { 0x12, 0x13 }, { 0x22, 0x23 } }
|
||||
|
||||
-- scene-14 strobe (YellowIntroPalSequence_f9dd6): 13 groups of
|
||||
-- $e4,$c0,$c0,$e4 with the 52nd byte replaced by the terminator
|
||||
local STROBE_SEQ = {}
|
||||
for i = 1, 51 do
|
||||
local m = (i - 1) % 4
|
||||
STROBE_SEQ[i] = (m == 1 or m == 2) and 0xC0 or 0xE4
|
||||
end
|
||||
-- scene-16 fade to white (YellowIntroPalSequence_f9e0a)
|
||||
local FADE_SEQ = { 0xE4, 0x90, 0x90, 0x40, 0x40, 0x00, 0x00 }
|
||||
|
||||
-- Func_fa079's sine bob (Unkn_fa0aa, sine_table 32). The ROM table's
|
||||
-- `dw sin(x)` truncates the 1.0 peak to $0000, which pops the sprite 8px
|
||||
-- for one frame at every crest (a=16 / a=48) on hardware; the true peak
|
||||
-- is restored here so the balloon glide loops smoothly.
|
||||
local function bobOffset(phase)
|
||||
local a = phase % 64
|
||||
local half = a % 32
|
||||
local v = math.floor(8 * math.sin(math.pi * half / 32))
|
||||
return a < 32 and v or -v
|
||||
end
|
||||
|
||||
local function tryImage(path)
|
||||
local ok, img = pcall(love.graphics.newImage, path)
|
||||
return ok and img or nil
|
||||
end
|
||||
|
||||
-- ------- state --------------------------------------------------------
|
||||
|
||||
function YellowIntro.new(game, onDone)
|
||||
local self = setmetatable({}, YellowIntro)
|
||||
self.game = game
|
||||
self.onDone = onDone
|
||||
self.finished = false
|
||||
self.scene = 0
|
||||
self.timer = 0
|
||||
self.seqIndex = 0
|
||||
self.scx = 0
|
||||
self.bgp = 0xE4
|
||||
self.palName = "MEWMON" -- PalPacket_Generic
|
||||
self.objects = {}
|
||||
self.cloudFrame = 0
|
||||
|
||||
self.atlas1 = tryImage("assets/generated/intro/yellow_intro_1.png")
|
||||
self.atlas2 = tryImage("assets/generated/intro/yellow_intro_2.png")
|
||||
self.clouds = tryImage("assets/generated/intro/clouds.png")
|
||||
self.quads = {}
|
||||
|
||||
-- 32x32 BG tile grid (vBGMap0); signed addressing: id < $80 -> atlas1,
|
||||
-- id >= $80 -> atlas2 (LCDC $e3, bit4 = 0)
|
||||
self.bg = {}
|
||||
self:bgLetterbox()
|
||||
self.bgDirty = true
|
||||
self.wave = nil
|
||||
local ok, canvas = pcall(love.graphics.newCanvas, 256, 256)
|
||||
self.bgCanvas = ok and canvas or nil
|
||||
|
||||
-- Yellow boots exactly like Red up to the attract movie: the copyright
|
||||
-- card and the GAME FREAK shooting-star splash play first. Reuse
|
||||
-- IntroMovie's phases 1-2 and take over where its Gengar fight (phase
|
||||
-- 3) would begin; a skip press during the pre-roll skips everything.
|
||||
local IntroMovie = require("src.ui.IntroMovie")
|
||||
local pre = IntroMovie.new(game, nil)
|
||||
local baseStart = pre.startPhase
|
||||
pre.finish = function(m)
|
||||
if m.finished then return end
|
||||
m.finished = true
|
||||
self.pre = nil
|
||||
self:finish()
|
||||
end
|
||||
pre.startPhase = function(m, phase)
|
||||
if phase == 3 then
|
||||
m.finished = true
|
||||
self.pre = nil
|
||||
self:beginScenes()
|
||||
else
|
||||
baseStart(m, phase)
|
||||
end
|
||||
end
|
||||
self.pre = pre
|
||||
return self
|
||||
end
|
||||
|
||||
function YellowIntro:sgbPalettes(game)
|
||||
if self.pre then return self.pre:sgbPalettes(game) end
|
||||
local P = require("src.render.PaletteFX")
|
||||
local pal = P.pal(game.data, self.palName)
|
||||
if not pal then return nil end
|
||||
-- rBGP composed with the SGB colors: shade i displays palette color
|
||||
-- ((bgp >> 2i) & 3), whole screen (one map stands in for BGP and OBP)
|
||||
local bgp = self.bgp
|
||||
local map = {}
|
||||
for i = 0, 3 do
|
||||
map[i] = math.floor(bgp / 4 ^ i) % 4
|
||||
end
|
||||
return { P.whole(P.permute(pal, map)) }
|
||||
end
|
||||
|
||||
-- ------- BG helpers ---------------------------------------------------
|
||||
|
||||
function YellowIntro:bgFill(id)
|
||||
for y = 0, 31 do
|
||||
local row = self.bg[y] or {}
|
||||
self.bg[y] = row
|
||||
for x = 0, 31 do row[x] = id end
|
||||
end
|
||||
self.bgDirty = true
|
||||
end
|
||||
|
||||
-- Func_f9e5f: rows 0-3 / 14-17 tile $01, rows 4-13 tile $00
|
||||
function YellowIntro:bgLetterbox()
|
||||
self:bgFill(0x01)
|
||||
for y = 4, 13 do
|
||||
for x = 0, 31 do self.bg[y][x] = 0x00 end
|
||||
end
|
||||
for y = 18, 31 do
|
||||
for x = 0, 31 do self.bg[y][x] = 0x00 end
|
||||
end
|
||||
self.bgDirty = true
|
||||
end
|
||||
|
||||
function YellowIntro:bgBlit(col, row, map)
|
||||
for r, line in ipairs(map) do
|
||||
for c, id in ipairs(line) do
|
||||
self.bg[(row + r - 1) % 32][(col + c - 1) % 32] = id
|
||||
end
|
||||
end
|
||||
self.bgDirty = true
|
||||
end
|
||||
|
||||
function YellowIntro:quadFor(image, tile)
|
||||
local key = image
|
||||
local cacheByImage = self.quads[key]
|
||||
if not cacheByImage then
|
||||
cacheByImage = {}
|
||||
self.quads[key] = cacheByImage
|
||||
end
|
||||
local quad = cacheByImage[tile]
|
||||
if not quad then
|
||||
local iw, ih = image:getDimensions()
|
||||
quad = love.graphics.newQuad(
|
||||
(tile % 16) * 8, math.floor(tile / 16) * 8, 8, 8, iw, ih)
|
||||
cacheByImage[tile] = quad
|
||||
end
|
||||
return quad
|
||||
end
|
||||
|
||||
function YellowIntro:rebuildBgCanvas()
|
||||
if not self.bgCanvas then return end
|
||||
love.graphics.push("all")
|
||||
love.graphics.setCanvas(self.bgCanvas)
|
||||
love.graphics.clear(1, 1, 1, 1)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
for y = 0, 31 do
|
||||
for x = 0, 31 do
|
||||
local id = self.bg[y][x]
|
||||
local image, tile
|
||||
if id < 0x80 then
|
||||
image, tile = self.atlas1, id
|
||||
else
|
||||
image, tile = self.atlas2, id
|
||||
end
|
||||
if image then
|
||||
-- scene-11 cloud animation retargets BG tiles $60-$63 at the
|
||||
-- clouds sheet (VBlank copy to $9600); frame = clouds row 0/1
|
||||
if self.clouds and id >= 0x60 and id <= 0x63 then
|
||||
local cw, ch = self.clouds:getDimensions()
|
||||
love.graphics.draw(self.clouds,
|
||||
love.graphics.newQuad((id - 0x60) * 8, self.cloudFrame * 8,
|
||||
8, 8, cw, ch), x * 8, y * 8)
|
||||
else
|
||||
love.graphics.draw(image, self:quadFor(image, tile), x * 8, y * 8)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
love.graphics.pop()
|
||||
self.bgDirty = false
|
||||
end
|
||||
|
||||
-- ------- objects ------------------------------------------------------
|
||||
|
||||
function YellowIntro:spawn(id, x, y)
|
||||
local spec = SPAWN[id]
|
||||
local obj = {
|
||||
id = id, frameset = spec[1], seq = spec[2],
|
||||
x = x, y = y, xoff = 0, yoff = 0,
|
||||
step = 1, wait = 0, held = false,
|
||||
fieldB = 0, fieldC = 0,
|
||||
}
|
||||
local script = FRAMESETS[obj.frameset]
|
||||
obj.wait = script[1][2]
|
||||
self.objects[#self.objects + 1] = obj
|
||||
return obj
|
||||
end
|
||||
|
||||
function YellowIntro:clearObjects()
|
||||
self.objects = {}
|
||||
end
|
||||
|
||||
local function updateFrameScript(obj)
|
||||
if obj.held then return end
|
||||
obj.wait = obj.wait - 1
|
||||
if obj.wait > 0 then return end
|
||||
local script = FRAMESETS[obj.frameset]
|
||||
if obj.step >= #script then
|
||||
if script.loop then
|
||||
obj.step = 1
|
||||
obj.wait = script[1][2]
|
||||
else
|
||||
obj.held = true -- endanim: hold last frame forever
|
||||
end
|
||||
return
|
||||
end
|
||||
obj.step = obj.step + 1
|
||||
obj.wait = script[obj.step][2]
|
||||
end
|
||||
|
||||
function YellowIntro:updateObjects()
|
||||
for _, obj in ipairs(self.objects) do
|
||||
if obj.seq == "bar" then
|
||||
-- Func_fa062: constant velocity, 8-bit wrap
|
||||
obj.x = (obj.x + obj.fieldB) % 256
|
||||
elseif obj.seq == "surf" then
|
||||
-- Func_fa014, including the original's Y = X + 1 quirk: the
|
||||
-- comparison register still holds X when Y is written, so the
|
||||
-- sprite rides a 45-degree diagonal until X parks at $58
|
||||
if obj.x ~= 0x58 then
|
||||
obj.x = (obj.x + 4) % 256
|
||||
obj.y = (obj.x + 1) % 256
|
||||
end
|
||||
elseif obj.seq == "fly" then
|
||||
-- Func_fa02b: rise 2px/frame to Y=$58, then a +-8px sine bob
|
||||
-- with a 64-frame period (and the truncated-peak notch)
|
||||
if obj.fieldB == 0 then
|
||||
if obj.y ~= 0x58 then
|
||||
obj.y = (obj.y - 2) % 256
|
||||
else
|
||||
obj.fieldB = 1
|
||||
end
|
||||
end
|
||||
if obj.fieldB == 1 then
|
||||
obj.yoff = bobOffset(obj.fieldC)
|
||||
obj.fieldC = obj.fieldC + 1
|
||||
end
|
||||
end
|
||||
updateFrameScript(obj)
|
||||
end
|
||||
end
|
||||
|
||||
function YellowIntro:drawObjects()
|
||||
if not self.atlas2 then return end
|
||||
for _, obj in ipairs(self.objects) do
|
||||
local frameId = FRAMESETS[obj.frameset][obj.step][1]
|
||||
local frame = FRAMES[frameId]
|
||||
if frame then
|
||||
local base, list = frame[1], frame[2]
|
||||
for _, entry in ipairs(list) do
|
||||
local dy, dx, delta, flip = entry[1], entry[2], entry[3], entry[4]
|
||||
-- OAM position: screen = (X + dx - 8, Y + dy - 16)
|
||||
local px = (obj.x + obj.xoff + dx - 8) % 256
|
||||
local py = (obj.y + obj.yoff + dy - 16) % 256
|
||||
if px < 160 and py < 144 then
|
||||
local quad = self:quadFor(self.atlas2, base + delta)
|
||||
if flip then
|
||||
love.graphics.draw(self.atlas2, quad, px + 8, py, 0, -1, 1)
|
||||
else
|
||||
love.graphics.draw(self.atlas2, quad, px, py)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ------- scenes -------------------------------------------------------
|
||||
|
||||
-- setup scenes run once and advance immediately; wait scenes count their
|
||||
-- timer down (YellowIntro_CheckFrameTimerDecrement: N running frames,
|
||||
-- expiry actions on frame N+1)
|
||||
function YellowIntro:startScene(scene)
|
||||
self.scene = scene
|
||||
local t = self
|
||||
if scene == 0 then
|
||||
-- running pika 1 over the boot letterbox
|
||||
t.palName = "MEWMON"
|
||||
t.scx = 0
|
||||
t:bgLetterbox()
|
||||
t:spawn(1, 0x58, 0x58)
|
||||
t.timer = 130
|
||||
t.scene = 1
|
||||
elseif scene == 2 then
|
||||
-- pikachu kick: 6x6 atlas2 block parked at BG col 20 row 6 (scrolled
|
||||
-- in by scene 3) + 8 speed bars
|
||||
t:bgFill(0x00)
|
||||
local block = {}
|
||||
for r = 0, 5 do
|
||||
local line = {}
|
||||
for c = 0, 5 do line[c + 1] = 0x90 + r * 0x10 + c end
|
||||
block[r + 1] = line
|
||||
end
|
||||
t:bgBlit(20, 6, block)
|
||||
for _, bar in ipairs(SPEED_BARS) do
|
||||
local obj = t:spawn(8, bar[1], bar[2])
|
||||
obj.fieldB = bar[3]
|
||||
end
|
||||
t.palName = "PIKACHUS_BEACH"
|
||||
t.timer = 128
|
||||
t.scene = 3
|
||||
elseif scene == 4 then
|
||||
-- running pika 2
|
||||
t:clearObjects()
|
||||
t.scx = 0
|
||||
t:bgLetterbox()
|
||||
t:spawn(2, 0x58, 0x58)
|
||||
t.palName = "MEWMON"
|
||||
t.timer = 128
|
||||
t.scene = 5
|
||||
elseif scene == 6 then
|
||||
-- surfing pika over the wavy sea (per-scanline SCY sine)
|
||||
t.scx = 0
|
||||
t.wave = {}
|
||||
for i = 0, 255 do t.wave[i] = WAVE[i % 32 + 1] end
|
||||
t:bgFill(0x10)
|
||||
for y = 0, 2 do
|
||||
for x = 0, 31 do t.bg[y][x] = 0x00 end
|
||||
end
|
||||
for x = 0, 31 do t.bg[3][x] = x % 2 == 0 and 0x20 or 0x21 end
|
||||
t:spawn(5, 0xF8, 0x40)
|
||||
t.palName = "PIKACHUS_BEACH"
|
||||
t.bgDirty = true
|
||||
t.timer = 88
|
||||
t.scene = 7
|
||||
elseif scene == 8 then
|
||||
-- running pika 3
|
||||
t:clearObjects()
|
||||
t.wave = nil
|
||||
t.scx = 0
|
||||
t:bgLetterbox()
|
||||
t:spawn(3, 0x58, 0x58)
|
||||
t.palName = "MEWMON"
|
||||
t.timer = 128
|
||||
t.scene = 9
|
||||
elseif scene == 10 then
|
||||
-- flying pika over clouds + badge + mark
|
||||
t:clearObjects()
|
||||
t.scx = 0
|
||||
t:bgFill(0x00)
|
||||
for y = 0, 7 do
|
||||
for x = 0, 31 do t.bg[y][x] = 0x02 end
|
||||
end
|
||||
t:bgBlit(0, 8, SKY_MAP)
|
||||
t:bgBlit(12, 4, BADGE_MAP)
|
||||
t:bgBlit(3, 7, MARK_MAP)
|
||||
t:spawn(6, 0x58, 0x98)
|
||||
t.palName = "PIKACHUS_BEACH"
|
||||
t.timer = 128
|
||||
t.scene = 11
|
||||
elseif scene == 12 then
|
||||
-- pika close-up: 12x8 atlas1 paste at BG (5,6) + fixups
|
||||
t:clearObjects()
|
||||
t.scx = 0
|
||||
t:bgLetterbox()
|
||||
local paste = {}
|
||||
for r = 0, 7 do
|
||||
local line = {}
|
||||
for c = 0, 11 do line[c + 1] = 0x04 + r * 0x10 + c end
|
||||
paste[r + 1] = line
|
||||
end
|
||||
t:bgBlit(5, 6, paste)
|
||||
t.bg[6][4] = 0x03
|
||||
t.bg[7][4] = 0x74
|
||||
t.bg[13][5] = 0x00
|
||||
t:spawn(9, 0x58, 0x60)
|
||||
t.palName = "MEWMON"
|
||||
t.timer = 128
|
||||
t.scene = 13
|
||||
elseif scene == 14 then
|
||||
-- thunderbolt strobe; timer reused as the sequence index
|
||||
t.seqIndex = 0
|
||||
t.scene = 14
|
||||
elseif scene == 15 then
|
||||
t.timer = 40
|
||||
t.scene = 15
|
||||
elseif scene == 16 then
|
||||
t.seqIndex = 0
|
||||
t.scene = 16
|
||||
elseif scene == 17 then
|
||||
t.timer = 64
|
||||
t.scene = 17
|
||||
end
|
||||
end
|
||||
|
||||
function YellowIntro:enter()
|
||||
if self.pre then return end -- pre-roll first; beginScenes takes over
|
||||
self:beginScenes()
|
||||
end
|
||||
|
||||
-- InitYellowIntroGFXAndMusic: the movie's own music starts with scene 0
|
||||
function YellowIntro:beginScenes()
|
||||
local data = self.game.data
|
||||
local songs = data.audio and data.audio.songs
|
||||
local song = songs and (songs.Music_YellowIntro and "Music_YellowIntro"
|
||||
or songs.Music_IntroBattle and "Music_IntroBattle")
|
||||
if song then pcall(Music.play, data, song, false) end
|
||||
self:startScene(0)
|
||||
end
|
||||
|
||||
function YellowIntro:finish()
|
||||
if self.finished then return end
|
||||
self.finished = true
|
||||
pcall(Music.stop)
|
||||
self.game.stack:pop()
|
||||
if self.onDone then self.onDone() end
|
||||
end
|
||||
|
||||
function YellowIntro:update(dt)
|
||||
if self.finished then return end
|
||||
if self.pre then
|
||||
self.pre:update(dt)
|
||||
return
|
||||
end
|
||||
local input = self.game.input
|
||||
if input:wasPressed("a") or input:wasPressed("b")
|
||||
or input:wasPressed("start") then
|
||||
self:finish()
|
||||
return
|
||||
end
|
||||
|
||||
local scene = self.scene
|
||||
if scene == 1 or scene == 5 or scene == 9 then
|
||||
if self.timer > 0 then
|
||||
self.timer = self.timer - 1
|
||||
else
|
||||
self:clearObjects()
|
||||
self:startScene(scene + 1)
|
||||
end
|
||||
elseif scene == 3 then
|
||||
if self.timer > 0 then
|
||||
self.timer = self.timer - 1
|
||||
if self.scx ~= 0x68 then self.scx = self.scx + 4 end
|
||||
else
|
||||
self:clearObjects()
|
||||
self:startScene(4)
|
||||
end
|
||||
elseif scene == 7 then
|
||||
if self.timer > 0 then
|
||||
self.timer = self.timer - 1
|
||||
self.scx = (self.scx + 2) % 256
|
||||
-- rotate the sine phase 1 scanline per frame
|
||||
local first = self.wave[0]
|
||||
for i = 0, 254 do self.wave[i] = self.wave[i + 1] end
|
||||
self.wave[255] = first
|
||||
else
|
||||
self:clearObjects()
|
||||
self:startScene(8)
|
||||
end
|
||||
elseif scene == 11 then
|
||||
if self.timer > 0 then
|
||||
-- cloud tiles swap every 8 frames (YellowIntroScene11)
|
||||
if self.timer % 8 == 0 then
|
||||
local frame = math.floor(self.timer / 8) % 2
|
||||
if frame ~= self.cloudFrame then
|
||||
self.cloudFrame = frame
|
||||
self.bgDirty = true
|
||||
end
|
||||
end
|
||||
self.timer = self.timer - 1
|
||||
else
|
||||
self:clearObjects()
|
||||
self:startScene(12)
|
||||
end
|
||||
elseif scene == 13 then
|
||||
if self.timer > 0 then
|
||||
self.timer = self.timer - 1
|
||||
else
|
||||
-- spawn the thunderbolt over the close-up (object $A stays with $9)
|
||||
self:spawn(10, 0x58, 0x68)
|
||||
self:startScene(14)
|
||||
end
|
||||
elseif scene == 14 then
|
||||
self.seqIndex = self.seqIndex + 1
|
||||
local v = STROBE_SEQ[self.seqIndex]
|
||||
if v then
|
||||
self.bgp = v
|
||||
else
|
||||
-- .expired: everything despawns, letterbox returns, logo/face
|
||||
-- object $7 appears for the strobe scene
|
||||
self:clearObjects()
|
||||
self:bgLetterbox()
|
||||
self.bgp = 0xE4
|
||||
self:spawn(7, 0x58, 0x58)
|
||||
self:startScene(15)
|
||||
end
|
||||
elseif scene == 15 then
|
||||
if self.timer > 0 then
|
||||
if self.timer % 4 == 0 then
|
||||
-- rBGP ^= $03: flips how the two lightest shades display
|
||||
self.bgp = self.bgp == 0xE4 and 0xE7 or 0xE4
|
||||
end
|
||||
self.timer = self.timer - 1
|
||||
else
|
||||
self.bgp = 0xE4
|
||||
self:startScene(16)
|
||||
end
|
||||
elseif scene == 16 then
|
||||
self.seqIndex = self.seqIndex + 1
|
||||
local v = FADE_SEQ[self.seqIndex]
|
||||
if v then
|
||||
self.bgp = v
|
||||
else
|
||||
self:startScene(17)
|
||||
end
|
||||
elseif scene == 17 then
|
||||
if self.timer > 0 then
|
||||
self.timer = self.timer - 1
|
||||
else
|
||||
self:finish()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
self:updateObjects()
|
||||
if self.bgDirty then self:rebuildBgCanvas() end
|
||||
end
|
||||
|
||||
function YellowIntro:draw()
|
||||
if self.pre then
|
||||
self.pre:draw()
|
||||
return
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
if self.bgCanvas then
|
||||
if self.wave then
|
||||
-- per-scanline SCY override, LY $10-$7F only (scene 7's VBlank
|
||||
-- copy covers just that band; the rest render unshifted). Each
|
||||
-- strip wraps horizontally like the BG map: SCX climbs past 96
|
||||
-- during the scene and a single quad would clamp at the canvas
|
||||
-- edge and smear the right of the screen.
|
||||
local cw, ch = self.bgCanvas:getDimensions()
|
||||
local sx = self.scx % 256
|
||||
local w1 = math.min(160, 256 - sx)
|
||||
for ly = 0, 143 do
|
||||
local dy = (ly >= 16 and ly < 128) and self.wave[ly] or 0
|
||||
local sy = (ly + dy) % 256
|
||||
love.graphics.draw(self.bgCanvas,
|
||||
love.graphics.newQuad(sx, sy, w1, 1, cw, ch), 0, ly)
|
||||
if w1 < 160 then
|
||||
love.graphics.draw(self.bgCanvas,
|
||||
love.graphics.newQuad(0, sy, 160 - w1, 1, cw, ch), w1, ly)
|
||||
end
|
||||
end
|
||||
else
|
||||
local cw, ch = self.bgCanvas:getDimensions()
|
||||
local sx = self.scx % 256
|
||||
love.graphics.draw(self.bgCanvas,
|
||||
love.graphics.newQuad(sx, 0, math.min(160, 256 - sx), 144, cw, ch),
|
||||
0, 0)
|
||||
if sx > 96 then
|
||||
-- horizontal wrap (scene 3 scrolls the kick block in from col 20)
|
||||
love.graphics.draw(self.bgCanvas,
|
||||
love.graphics.newQuad(0, 0, 160 - (256 - sx), 144, cw, ch),
|
||||
256 - sx, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
self:drawObjects()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
return YellowIntro
|
||||
Reference in New Issue
Block a user