mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
G2 support
This commit is contained in:
@@ -0,0 +1,648 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user