mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 08:11:35 +02:00
649 lines
22 KiB
Lua
649 lines
22 KiB
Lua
-- Gen 2 Oak speech (pokegold OakSpeech in engine/menus/intro_menu.asm).
|
|
-- Retail order: InitClock → Oak → Marill wipe (+ cry) → Oak → player pic →
|
|
-- NamePlayer → ready → ShrinkPlayer. Gender select is Crystal-later;
|
|
-- InitClock is not, it is the very first thing OakSpeech farcalls
|
|
-- (engine/menus/intro_menu.asm) and it is src/ui/gen2/InitClock.lua.
|
|
--
|
|
-- The beats are a DATA TABLE for the same reason Gen 1's are
|
|
-- (src/ui/OakSpeech.lua): Gold has a real Oak speech, so it is the same
|
|
-- extension point under the same names rather than a second one. Concretely
|
|
-- this screen carries:
|
|
--
|
|
-- hook intro.oak_speech.build (steps, speech) -> steps
|
|
-- event intro.oak_speech.started { speech, steps }
|
|
-- event intro.oak_speech.step { speech, step, index }
|
|
-- event intro.oak_speech.answered { speech, step, index, label, value,
|
|
-- saveKey }
|
|
-- event intro.oak_speech.finished { speech, answers }
|
|
--
|
|
-- Same names, same payload keys, same moments in the sequence as the Gen 1
|
|
-- site: `started` before the first beat runs, `step` immediately before each
|
|
-- beat, `answered` whenever a beat produces a value (the name menu, and any
|
|
-- choice a mod inserted), `finished` once, on the way out. Step ids match
|
|
-- Gen 1's wherever the moment is the same one -- oak_welcome, demo_mon,
|
|
-- world_spiel, ask_player_name, name_player, legend, shrink -- so a mod that
|
|
-- does insertBefore("name_player", ...) lands in the right place in both
|
|
-- games. The two ids with no Gen 1 counterpart are Gold's own beats:
|
|
-- `init_clock` (the farcall InitClock this speech opens with) and `oak_study`
|
|
-- (the return to Oak for _OakText5, which Red's speech does not have).
|
|
--
|
|
-- Gen 1 has no confirm_player_name / ask_rival_name / name_rival /
|
|
-- confirm_rival_name equivalents here on purpose: Gold's Oak never says the
|
|
-- name back and never asks for the rival's, so inventing those anchors would
|
|
-- promise moments that do not exist (the rival is named by the CopScript in
|
|
-- maps/ElmsLab.asm, hours later).
|
|
|
|
-- src/render/Assets.lua is the mod-override choke point: a raw
|
|
-- love.graphics.newImage skips overrides/ and AssetTransform output.
|
|
local Assets = require("src.render.Assets")
|
|
local Chrome = require("src.ui.gen2.Chrome")
|
|
local Font = require("src.render.Font")
|
|
local GbcPalette = require("src.render.GbcPalette")
|
|
local Logger = require("src.core.Logger")
|
|
local Music = require("src.core.Music")
|
|
local Palettes = require("src.world.gen2.Palettes")
|
|
local Runtime = require("src.mods.Runtime")
|
|
local Screens = require("src.ui.Screens")
|
|
local Sound = require("src.core.Sound")
|
|
local Strings = require("src.core.Strings")
|
|
local TextBox = require("src.render.TextBox")
|
|
|
|
local OakSpeech = {}
|
|
OakSpeech.__index = OakSpeech
|
|
OakSpeech.isOpaque = true
|
|
|
|
local FADE_FRAMES = 24
|
|
local WIPE_FRAMES = 32
|
|
|
|
local FALLBACKS = {
|
|
_OakText1 = Strings("Hello! Sorry to\nkeep you waiting!\fWelcome to the\nworld of POKéMON!\fMy name is OAK.\fPeople call me the\nPOKéMON PROF."),
|
|
_OakText2 = Strings("This world is in-\nhabited by crea-\vtures that we call\vPOKéMON."),
|
|
_OakText4 = Strings("People and POKéMON\nlive together by\fsupporting each\nother.\fSome people play\nwith POKéMON, some\vbattle with them."),
|
|
_OakText5 = Strings("But we don't know\neverything about\vPOKéMON yet.\fThere are still\nmany mysteries to\vsolve.\fThat's why I study\nPOKéMON every day."),
|
|
_OakText6 = Strings("Now, what did you\nsay your name was?"),
|
|
_OakText7 = Strings("{PLAYER}, are you\nready?\fYour very own\nPOKéMON story is\vabout to unfold.\fYou'll face fun\ntimes and tough\vchallenges.\fA world of dreams\nand adventures\fwith POKéMON\nawaits! Let's go!\fI'll be seeing you\nlater!"),
|
|
}
|
|
|
|
local function tryImage(path)
|
|
if not path then return nil end
|
|
local ok, image = pcall(Assets.image, path)
|
|
return ok and image or nil
|
|
end
|
|
|
|
-- Naming presets are boot config the same way Gen 1 reads them
|
|
-- (field.boot.namePresets), so a total conversion that replaces the list once
|
|
-- replaces it for both games; NamePick.PRESETS (data/player_names.asm
|
|
-- PlayerNameArray) is Gold's fallback.
|
|
local function namePresets(game, who, fallback)
|
|
local boot = game and game.data and game.data.field
|
|
and game.data.field.boot
|
|
local presets = boot and boot.namePresets and boot.namePresets[who]
|
|
if type(presets) == "table" and #presets > 0 then return presets end
|
|
return fallback
|
|
end
|
|
|
|
function OakSpeech:wantsFillScale() return true end
|
|
function OakSpeech:drawsWidescreen() return true end
|
|
|
|
function OakSpeech.new(game, opts)
|
|
opts = opts or {}
|
|
local self = setmetatable({}, OakSpeech)
|
|
self.game = game
|
|
self.onDone = opts.onDone
|
|
local data = opts.data or {}
|
|
self.cfg = data
|
|
self.texts = data.text or {}
|
|
self.oakPic = tryImage(data.oakPic or "assets/generated/intro/oak.png")
|
|
-- player.sprite, where Gen 1's Oak speech raises it (src/ui/OakSpeech.lua:121).
|
|
self.playerPic = tryImage(require("src.pokemon.Sprites").playerPic(
|
|
data.playerPic or "assets/generated/intro/cal.png",
|
|
{ side = "front", kind = "intro", data = game and game.data }))
|
|
self.marillPic = tryImage(data.marillPic
|
|
or "assets/generated/battle/front/marill.png")
|
|
self.shrinkPic1 = tryImage(data.shrink1 or "assets/generated/intro/shrink1.png")
|
|
self.shrinkPic2 = tryImage(data.shrink2 or "assets/generated/intro/shrink2.png")
|
|
self.music = data.music or "Music_Route30"
|
|
self.demoSpecies = data.demoSpecies or "MARILL"
|
|
-- Every pic on this screen is loaded under SCGB_TRAINER_OR_MON_FRONTPIC_PALS,
|
|
-- which is _CGB_PlayerOrMonFrontpicPals -- the pic's own two shipped colours
|
|
-- bracketed by white and black, exactly as a battle pic gets them. The
|
|
-- speech names the class the ASM names: POKEMON_PROF for Oak, CAL for the
|
|
-- player (Chris shares Cal's colours, which is why the extractor calls row 0
|
|
-- PLAYER). The shrink frames are the player still, so they wear his.
|
|
local palettes = (game and game.data and game.data.gen2Palettes) or nil
|
|
self.palettes = palettes
|
|
self.oakColors = Palettes.trainerColors(palettes, "POKEMON_PROF")
|
|
self.playerColors = Palettes.trainerColors(palettes, "CAL")
|
|
self.marillColors = Palettes.monColors(palettes, self.demoSpecies)
|
|
self.picColors = nil
|
|
self.fontOk = false
|
|
local font = opts.font
|
|
if font then
|
|
local ok = pcall(Font.load, { font = font })
|
|
self.fontOk = ok
|
|
end
|
|
self.step = 0
|
|
self.steps = nil
|
|
self.answers = {}
|
|
self.pic = nil
|
|
self.picFlip = false
|
|
self.picReveal = nil
|
|
self.busy = false
|
|
self.shrink = nil
|
|
self.shrinkText = nil
|
|
return self
|
|
end
|
|
|
|
function OakSpeech:text(key)
|
|
local t = self.texts[key]
|
|
if type(t) == "string" and #t > 0 then return t end
|
|
return FALLBACKS[key] or ""
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ steps
|
|
|
|
-- The vanilla beat list. Ids are the stable anchors a build wrapper inserts
|
|
-- around; see the header for which of them are shared with Gen 1.
|
|
function OakSpeech.defaultSteps(_speech)
|
|
return {
|
|
-- `farcall InitClock` is the first line of OakSpeech.
|
|
{ id = "init_clock", kind = "initclock" },
|
|
-- Intro_PrepTrainerPic POKEMON_PROF, FadeInIntroPic, OakText1.
|
|
{ id = "oak_welcome", kind = "say", textKey = "_OakText1",
|
|
pic = "oak", reveal = "fade" },
|
|
-- The Marill show-off: MovePicRight wipes it in, then its cry, then
|
|
-- OakText2. Gen 1's demo_mon beat with a different mon.
|
|
{ id = "demo_mon", kind = "demo" },
|
|
-- OakText4 over the same pic, exactly as Gen 1's world_spiel prints
|
|
-- OakSpeechText2B over the NIDORINO already on screen.
|
|
{ id = "world_spiel", kind = "say", textKey = "_OakText4" },
|
|
-- Back to Oak for OakText5. Red's speech never returns to him, so this
|
|
-- id is Gold's own.
|
|
{ id = "oak_study", kind = "say", textKey = "_OakText5",
|
|
pic = "oak", reveal = "fade" },
|
|
-- The CAL frontpic comes up under the question NamePlayer answers.
|
|
{ id = "ask_player_name", kind = "say", textKey = "_OakText6",
|
|
pic = "player", reveal = "fade" },
|
|
{ id = "name_player", kind = "name", who = "player", saveKey = "name" },
|
|
-- OakText7 with the pic already up: NamePlayer walked it back itself
|
|
-- (MovePlayerPicLeft), so there is nothing to reveal here.
|
|
{ id = "legend", kind = "say", textKey = "_OakText7", pic = "player" },
|
|
{ id = "shrink", kind = "shrink", textKey = "_OakText7" },
|
|
}
|
|
end
|
|
|
|
local function sameSteps(steps) return steps end
|
|
|
|
-- intro.oak_speech.build, the same hook Gen 1 offers and with the same
|
|
-- contract: a wrapper is handed the step list and the speech and returns a
|
|
-- step list. Anything else degrades to vanilla with a logged line rather
|
|
-- than dropping the player into a speech that cannot run.
|
|
function OakSpeech:buildSteps()
|
|
local steps = OakSpeech.defaultSteps(self)
|
|
if not Runtime.wantsHook("intro.oak_speech.build") then return steps end
|
|
local hooked = Runtime.call("intro.oak_speech.build", sameSteps, steps, self)
|
|
if type(hooked) ~= "table" then
|
|
Logger.error("intro.oak_speech.build returned %s; keeping vanilla steps",
|
|
type(hooked))
|
|
return steps
|
|
end
|
|
return hooked
|
|
end
|
|
|
|
function OakSpeech:enter()
|
|
local data = self.game and self.game.data
|
|
if data and data.audio and data.audio.runtime then
|
|
Music.play(data, self.music, true, { reason = "oak_speech" })
|
|
end
|
|
self.step = 0
|
|
self.answers = {}
|
|
self.steps = self:buildSteps()
|
|
if Runtime.wants("intro.oak_speech.started") then
|
|
Runtime.emit("intro.oak_speech.started", { speech = self, steps = self.steps })
|
|
end
|
|
self:advance()
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ pics
|
|
|
|
-- A step's `pic`. The three names are this speech's own art; the table forms
|
|
-- are for a build wrapper bringing its own, and they carry their palette the
|
|
-- same way every pic on this screen does (two shipped colours, bracketed).
|
|
function OakSpeech:resolvePic(desc)
|
|
if desc == "oak" then return self.oakPic, self.oakColors end
|
|
if desc == "player" then return self.playerPic, self.playerColors end
|
|
if desc == "demo" then return self.marillPic, self.marillColors end
|
|
if type(desc) ~= "table" then return nil, nil end
|
|
if desc.type == "pokemon" then
|
|
local mon = self.game and self.game.data and self.game.data.pokemon
|
|
local def = mon and mon[desc.id]
|
|
return tryImage(def and def.spriteFront),
|
|
desc.colors or Palettes.monColors(self.palettes, desc.id)
|
|
end
|
|
if desc.type == "trainer" then
|
|
return tryImage(desc.path),
|
|
desc.colors or Palettes.trainerColors(self.palettes, desc.id)
|
|
end
|
|
-- { type = "image", path = ..., colors = ... }, and a pre-loaded image.
|
|
if desc.path then return tryImage(desc.path), desc.colors end
|
|
return desc.image, desc.colors
|
|
end
|
|
|
|
-- picFlip belongs to the pic and not to the step: only a beat that changes
|
|
-- the pic may change it, so a text-only beat leaves whatever is on screen
|
|
-- exactly as the beat before it left it.
|
|
function OakSpeech:applyPic(step)
|
|
if step.pic == nil then return end
|
|
local img, colors = self:resolvePic(step.pic)
|
|
self.pic = img
|
|
self.picColors = colors
|
|
self.picFlip = step.flip and true or false
|
|
end
|
|
|
|
function OakSpeech:reveal(kind, next)
|
|
self.picReveal = {
|
|
kind = kind,
|
|
t = 0,
|
|
dur = kind == "wipe" and WIPE_FRAMES or FADE_FRAMES,
|
|
next = next,
|
|
}
|
|
end
|
|
|
|
function OakSpeech:afterReveal(step, fn)
|
|
if step.reveal then
|
|
self:reveal(step.reveal, fn)
|
|
else
|
|
fn()
|
|
end
|
|
end
|
|
|
|
function OakSpeech:showPic(img, reveal, next, colors)
|
|
self.pic = img
|
|
self.picColors = colors
|
|
self.picFlip = false
|
|
if reveal then
|
|
self:reveal(reveal, next)
|
|
elseif next then
|
|
next()
|
|
end
|
|
end
|
|
|
|
function OakSpeech:playCry(species)
|
|
local data = self.game and self.game.data
|
|
if data and data.audio and data.audio.cries and data.audio.cries[species] then
|
|
Sound.playCry(data, species)
|
|
end
|
|
end
|
|
|
|
function OakSpeech:playMarillCry()
|
|
self:playCry(self.demoSpecies)
|
|
end
|
|
|
|
-- A step's own cry: `cry = true` means "the mon this step's pic shows".
|
|
function OakSpeech:runCry(step)
|
|
local cry = step.cry
|
|
if not cry then return end
|
|
if cry == true then
|
|
if type(step.pic) == "table" and step.pic.type == "pokemon" then
|
|
cry = step.pic.id
|
|
elseif step.pic == "demo" then
|
|
cry = self.demoSpecies
|
|
else
|
|
return
|
|
end
|
|
end
|
|
self:playCry(cry)
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ beats
|
|
|
|
function OakSpeech:stepText(step)
|
|
if step.text then return step.text end
|
|
if step.textKey then return self:text(step.textKey) end
|
|
return ""
|
|
end
|
|
|
|
function OakSpeech:sayText(text, next, opts)
|
|
self.busy = true
|
|
self.game.stack:push(TextBox.new(self.game, text, function()
|
|
self.busy = false
|
|
if next then next() end
|
|
end, opts))
|
|
end
|
|
|
|
function OakSpeech:say(key, next)
|
|
self:sayText(self:text(key), next)
|
|
end
|
|
|
|
-- `farcall InitClock` does not return until both halves are confirmed, so the
|
|
-- port pushes the screen and only advances on its way out. A build with no
|
|
-- stack (a headless logic test) skips straight to Oak, the way every other
|
|
-- pushed screen here does.
|
|
function OakSpeech:openInitClock()
|
|
local game = self.game
|
|
if not (game and game.stack) then return self:advance() end
|
|
self.busy = true
|
|
local pushed = Screens.push(game, "Gen2InitClock", {
|
|
mode = "clock",
|
|
save = game.save,
|
|
autoConfirm = self.autoConfirm,
|
|
onDone = function()
|
|
game.stack:pop()
|
|
self.busy = false
|
|
self:advance()
|
|
end,
|
|
})
|
|
if not pushed then
|
|
self.busy = false
|
|
self:advance()
|
|
end
|
|
end
|
|
|
|
function OakSpeech:openNamePick(step)
|
|
self.busy = true
|
|
local NamePick = require("src.ui.gen2.NamePick")
|
|
Screens.push(self.game, "Gen2NamePick", {
|
|
font = self.game.fontData,
|
|
-- NamePlayer opens with MovePlayerPicRight, so the name menu owns the
|
|
-- pic while it is up: it is the same CAL frontpic this speech has been
|
|
-- showing, walked over to make room for the box.
|
|
pic = self.playerPic,
|
|
picColors = self.playerColors,
|
|
presets = step.presets
|
|
or namePresets(self.game, step.presetsWho or step.who or "player",
|
|
step.presetsFallback or NamePick.PRESETS),
|
|
onDone = function(name)
|
|
name = name or "GOLD"
|
|
self.game.save.player.name = name
|
|
self.game.stack:pop() -- NamePick
|
|
self.busy = false
|
|
self:recordAnswer(step, 1, name, name)
|
|
self:advance()
|
|
end,
|
|
})
|
|
end
|
|
|
|
function OakSpeech:lastPageLines(key)
|
|
local body = self:text(key)
|
|
local pages = {}
|
|
for page in (body .. "\f"):gmatch("(.-)\f") do
|
|
pages[#pages + 1] = page
|
|
end
|
|
local last = pages[#pages] or body
|
|
local lines = {}
|
|
for line in (last .. "\n"):gmatch("(.-)\n") do
|
|
if line ~= "" then lines[#lines + 1] = line end
|
|
end
|
|
if #lines == 0 and last ~= "" then lines[1] = last end
|
|
return lines
|
|
end
|
|
|
|
function OakSpeech:startShrink(step)
|
|
self.shrinkText = self:lastPageLines((step and step.textKey) or "_OakText7")
|
|
self.shrink = { frame = 0 }
|
|
local data = self.game and self.game.data
|
|
if data and data.audio and data.audio.sfx
|
|
and data.audio.sfx.Sfx_EscapeRope then
|
|
Sound.play(data, "Sfx_EscapeRope")
|
|
end
|
|
end
|
|
|
|
-- A beat that produced a value: the name menu, and any choice/yesno a build
|
|
-- wrapper inserted. Same store and same event payload as Gen 1's, so a mod
|
|
-- reading answers off intro.oak_speech.answered needs no second listener.
|
|
function OakSpeech:recordAnswer(step, index, label, value)
|
|
if value == nil then value = label end
|
|
if step.saveKey then
|
|
self.answers[step.saveKey] = value
|
|
end
|
|
if Runtime.wants("intro.oak_speech.answered") then
|
|
Runtime.emit("intro.oak_speech.answered", {
|
|
speech = self,
|
|
step = step,
|
|
index = index,
|
|
label = label,
|
|
value = value,
|
|
saveKey = step.saveKey,
|
|
})
|
|
end
|
|
end
|
|
|
|
-- The choice menu a build wrapper's `choice` step opens: VerticalMenu, which
|
|
-- is Gold's generic list (src/ui/gen2/ScriptMenu.lua) and so already carries
|
|
-- the shared ui.list_menu hook. Vanilla Gold has no such beat -- this is the
|
|
-- Gen 1 step kind honoured on Gold rather than skipped with a warning.
|
|
function OakSpeech:openChoice(step)
|
|
local labels = step.choices or {}
|
|
local left, top = step.tx or 0, step.ty or 0
|
|
local width = step.tw or 10
|
|
local pushed = Screens.push(self.game, "Gen2ScriptMenu", {
|
|
header = {
|
|
left = left, top = top,
|
|
right = left + width, bottom = top + #labels * 2 + 1,
|
|
items = labels,
|
|
-- STATICMENU_CURSOR; B is left enabled only when the step asks for it.
|
|
dataFlags = 0x80,
|
|
cursor = 1,
|
|
},
|
|
style = "vertical",
|
|
onChoose = function(index)
|
|
self.game.stack:pop()
|
|
self.busy = false
|
|
if index == 0 or index == nil then
|
|
if step.cancelable then return self:advance() end
|
|
index = 1
|
|
end
|
|
local label = labels[index]
|
|
local value = label
|
|
if step.values and step.values[index] ~= nil then
|
|
value = step.values[index]
|
|
end
|
|
self:recordAnswer(step, index, label, value)
|
|
self:advance()
|
|
end,
|
|
})
|
|
if not pushed then
|
|
self.busy = false
|
|
self:advance()
|
|
end
|
|
end
|
|
|
|
function OakSpeech:runStep(step)
|
|
local kind = step.kind or "say"
|
|
if kind == "initclock" then
|
|
self:openInitClock()
|
|
elseif kind == "say" then
|
|
self:applyPic(step)
|
|
self:afterReveal(step, function()
|
|
self:runCry(step)
|
|
self:sayText(self:stepText(step), function() self:advance() end)
|
|
end)
|
|
elseif kind == "demo" then
|
|
-- Intro_PrepMonFrontpic + MovePicRight + the cry, then OakText2.
|
|
self:showPic(self.marillPic, "wipe", function()
|
|
self:playMarillCry()
|
|
self:say("_OakText2", function() self:advance() end)
|
|
end, self.marillColors)
|
|
elseif kind == "pic" then
|
|
self:applyPic(step)
|
|
self:afterReveal(step, function()
|
|
self:runCry(step)
|
|
self:advance()
|
|
end)
|
|
elseif kind == "name" then
|
|
self:openNamePick(step)
|
|
elseif kind == "yesno" then
|
|
self:applyPic(step)
|
|
self:afterReveal(step, function()
|
|
self:runCry(step)
|
|
self.busy = true
|
|
self:sayText(self:stepText(step), nil, {
|
|
instant = true,
|
|
choice = function(yes)
|
|
self.busy = false
|
|
local label = yes and "YES" or "NO"
|
|
local value = yes
|
|
if step.values then
|
|
value = yes and step.values[1] or step.values[2]
|
|
end
|
|
self:recordAnswer(step, yes and 1 or 2, label, value)
|
|
self:advance()
|
|
end,
|
|
})
|
|
end)
|
|
elseif kind == "choice" then
|
|
self:applyPic(step)
|
|
self:afterReveal(step, function()
|
|
self:runCry(step)
|
|
local text = self:stepText(step)
|
|
if text ~= "" then
|
|
self:sayText(text, function() self:openChoice(step) end)
|
|
else
|
|
self:openChoice(step)
|
|
end
|
|
end)
|
|
elseif kind == "shrink" then
|
|
self:startShrink(step)
|
|
elseif kind == "fn" then
|
|
-- full escape hatch: step.run(speech, done)
|
|
if type(step.run) == "function" then
|
|
step.run(self, function() self:advance() end)
|
|
else
|
|
self:advance()
|
|
end
|
|
else
|
|
Logger.warn("oak speech unknown step kind %s (id=%s); skipping",
|
|
tostring(kind), tostring(step.id))
|
|
self:advance()
|
|
end
|
|
end
|
|
|
|
function OakSpeech:advance()
|
|
self.step = self.step + 1
|
|
local steps = self.steps
|
|
if not steps then
|
|
-- enter() builds the list; keep a path for callers that advance early.
|
|
steps = self:buildSteps()
|
|
self.steps = steps
|
|
end
|
|
local step = steps[self.step]
|
|
if not step then return self:finish() end
|
|
if Runtime.wants("intro.oak_speech.step") then
|
|
Runtime.emit("intro.oak_speech.step", {
|
|
speech = self, step = step, index = self.step,
|
|
})
|
|
end
|
|
self:runStep(step)
|
|
end
|
|
|
|
function OakSpeech:finish()
|
|
-- Guarded like the Gen 1 site (#308): the shrink timeline keeps ticking
|
|
-- while a `finished` listener does whatever it does, and a second emit from
|
|
-- the same run would look like a second speech.
|
|
if self.finished then return end
|
|
self.finished = true
|
|
if Runtime.wants("intro.oak_speech.finished") then
|
|
Runtime.emit("intro.oak_speech.finished", {
|
|
speech = self, answers = self.answers,
|
|
})
|
|
end
|
|
if self.onDone then self.onDone() end
|
|
end
|
|
|
|
function OakSpeech:update(_dt)
|
|
local r = self.picReveal
|
|
if r then
|
|
r.t = r.t + 1
|
|
if r.t >= r.dur then
|
|
self.picReveal = nil
|
|
if r.next then r.next() end
|
|
end
|
|
return
|
|
end
|
|
-- ShrinkPlayer timeline (intro_menu.asm ShrinkPlayer): pic1 → pic2 →
|
|
-- clear → chris sprite beat → fade music → overworld.
|
|
local s = self.shrink
|
|
if not s then return end
|
|
s.frame = s.frame + 1
|
|
if s.frame == 8 then
|
|
self.pic = self.shrinkPic1 or self.pic
|
|
elseif s.frame == 16 then
|
|
self.pic = self.shrinkPic2 or self.pic
|
|
elseif s.frame == 24 then
|
|
self.pic = nil
|
|
elseif s.frame == 32 then
|
|
Music.fadeOut(10)
|
|
elseif s.frame >= 80 then
|
|
self.shrink = nil
|
|
self.shrinkText = nil
|
|
self:finish()
|
|
end
|
|
end
|
|
|
|
function OakSpeech:drawPic()
|
|
if not self.pic then return end
|
|
local G = love.graphics
|
|
local w, h = self.pic:getDimensions()
|
|
-- Intro_PrepTrainerPic / PrepMonFrontpic: 7x7 cell at hlcoord 6,4.
|
|
local x = 48 + math.floor((8 - w / 8) / 2) * 8
|
|
local y = 32 + (7 - h / 8) * 8
|
|
local reveal = self.picReveal
|
|
local off = 0
|
|
if reveal and reveal.kind == "fade" then
|
|
G.setColor(1, 1, 1, math.min(1, reveal.t / reveal.dur))
|
|
elseif reveal and reveal.kind == "wipe" then
|
|
off = math.floor((160 - x) * (1 - math.min(1, reveal.t / reveal.dur)))
|
|
else
|
|
G.setColor(1, 1, 1, 1)
|
|
end
|
|
local function body()
|
|
if self.picFlip then
|
|
G.draw(self.pic, x + off + w, y, 0, -1, 1)
|
|
else
|
|
G.draw(self.pic, x + off, y)
|
|
end
|
|
end
|
|
-- A "fade" reveal is an alpha ramp, and GbcPalette multiplies the tint into
|
|
-- its own output, so the two compose: the shader picks the colour and
|
|
-- setColor's alpha still fades it in.
|
|
if self.picColors and GbcPalette.available() then
|
|
GbcPalette.with(self.picColors, body)
|
|
else
|
|
body()
|
|
end
|
|
G.setColor(1, 1, 1, 1)
|
|
end
|
|
|
|
function OakSpeech:drawPanel()
|
|
local G = love.graphics
|
|
G.setColor(1, 1, 1, 1)
|
|
G.rectangle("fill", 0, 0, 160, 144)
|
|
self:drawPic()
|
|
if self.shrinkText and self.fontOk then
|
|
G.setColor(0, 0, 0, 1)
|
|
for i, line in ipairs(self.shrinkText) do
|
|
Font.draw(line, 16, 104 + (i - 1) * 16)
|
|
end
|
|
G.setColor(1, 1, 1, 1)
|
|
end
|
|
end
|
|
|
|
function OakSpeech:draw()
|
|
self:drawPanel()
|
|
end
|
|
|
|
function OakSpeech:drawWidescreen(winW, winH)
|
|
local G = love.graphics
|
|
G.setColor(1, 1, 1, 1)
|
|
G.rectangle("fill", 0, 0, winW, winH)
|
|
local scale = Chrome.fitScale(winW, winH)
|
|
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
|
|
G.push()
|
|
G.translate(ox, oy)
|
|
G.scale(scale, scale)
|
|
self:drawPanel()
|
|
G.pop()
|
|
end
|
|
|
|
return OakSpeech
|