Files
gen1recomp/src/ui/gen2/MapRadio.lua
T

222 lines
8.8 KiB
Lua

-- The wall radios (engine/pokegear/pokegear.asm PlayRadio). A house radio's
-- bg event runs `jumpstd Radio1Script` -- `setval MAPRADIO_*` then `special
-- MapRadio` (engine/events/std_scripts.asm, engine/events/specials.asm) --
-- and PlayRadio owns the joypad from there: it resolves the MAPRADIO_* index
-- through PlayRadioStationPointers, prints the station's name in quotes in
-- the caller's text box, waits 100 frames, then runs one PlayRadioShow frame
-- per loop until A or B. The show is the SAME state machine the Pokegear's
-- radio card runs (src/ui/gen2/Pokegear.lua's Radio), and the exit music is
-- the same ExitPokegearRadio_HandleMusic: the channel song the show started
-- stays playing as the map music.
--
-- Pushed by src/script/gen2/Specials.lua H.MapRadio while the script VM is
-- parked on the special; `onDone` resumes it, and `closetext` follows.
local Chrome = require("src.ui.gen2.Chrome")
local Buena = require("src.core.gen2.Buena")
local Nests = require("src.core.gen2.Nests")
local Pokegear = require("src.ui.gen2.Pokegear")
local Runtime = require("src.mods.Runtime")
local MapRadio = {}
MapRadio.__index = MapRadio
-- The map stays on screen under the text box, exactly as the cart leaves it.
MapRadio.isOpaque = false
-- PlayRadioStationPointers, in MAPRADIO_* order (constants/
-- radio_constants.asm). Index 0, LoadStation_PokemonChannel, resolves by
-- region and time of day below rather than to a fixed station.
local STATIONS = {
[1] = "OAKS_POKEMON_TALK",
[2] = "POKEDEX_SHOW",
[3] = "POKEMON_MUSIC",
[4] = "LUCKY_CHANNEL",
[5] = "UNOWN_RADIO",
[6] = "PLACES_AND_PEOPLE",
[7] = "LETS_ALL_SING",
[8] = "ROCKET_RADIO",
}
local POKEGEAR_ONLY_STATIONS = { "POKE_FLUTE_RADIO", "EVOLUTION_RADIO" }
-- The same eight as the `radio_channels` registry sees them (src/mods/
-- Schemas.lua), one of the Gen 2-only six: Red has no radio, so the name is
-- gated under Gen 1 and routed to data.gen2RadioChannels under Gen 2. Id =
-- the station the dial lands on, which is the LoadStation_* id
-- src/ui/gen2/Pokegear.lua's show state machine is keyed by; `channel` is its
-- MAPRADIO_* position, the byte a wall radio's `setval` passes the special.
-- Position 0 is deliberately unregistered: it is not a station, it resolves by
-- region and time of day in resolveStation below.
--
-- src/mods/Builtins.lua seeds these engine-owned, so a mod's register of
-- ROCKET_RADIO collides and has to say override.
function MapRadio.registerInto(registry, data, owner)
local count = 0
for channel, station in pairs(STATIONS) do
registry:register(station, { channel = channel,
name = Pokegear.STATION_NAMES[station] }, owner)
count = count + 1
end
-- Two Pokegear-only signals have no MAPRADIO_* index. They still belong in
-- the same registry because the tuner resolves their display names by id;
-- leaving `channel` absent keeps wall-radio lookup unambiguous.
for _, station in ipairs(POKEGEAR_ONLY_STATIONS) do
registry:register(station, { name = Pokegear.STATION_NAMES[station] }, owner)
count = count + 1
end
local metadata = Buena.metadata(data)
if metadata then
registry:register("BUENAS_PASSWORD", { name = metadata.stationName }, owner)
count = count + 1
end
return count
end
-- The merged record for a dial position, or nil. Read through data rather
-- than through STATIONS so a registered station is on the dial for real; the
-- module's own table is the fallback for a boot with no loader.
function MapRadio.channelRecord(data, channel)
local rows = data and data.gen2RadioChannels
if type(rows) == "table" then
for station, record in pairs(rows) do
if type(record) == "table" and record.channel == channel then
return record, station
end
end
return nil
end
local station = STATIONS[channel]
if not station then return nil end
return { channel = channel, name = Pokegear.STATION_NAMES[station] }, station
end
-- The player's landmark, the way Game2:currentLandmark reads it: the map
-- header's landmark byte resolved through the `landmarks` registry
-- (src/core/gen2/Nests.lua). IsInJohto and the Rocket takeover both branch
-- on it.
local function landmarkOf(game)
local world = game and game.world
local map = world and world.map and world.map.def
return Nests.landmarkId(game and game.data, map and map.landmark)
end
-- opts: channel (the MAPRADIO_* index setval left in wScriptVar), onDone(),
-- and for tests: save, currentLandmark, radioData, radioRng.
function MapRadio.new(game, opts)
opts = opts or {}
local self = setmetatable({}, MapRadio)
self.game = game
self.onDone = opts.onDone
-- The Pokegear owns the radio data assembly (Pokegear:radioData) and the
-- region/time reads; a gear instance that is never drawn keeps one copy of
-- that wiring.
self.gear = Pokegear.new(game, {
save = opts.save or (game and game.save),
currentLandmark = opts.currentLandmark or landmarkOf(game),
radioData = opts.radioData,
radioRng = opts.radioRng,
})
self.station, self.stationName = self:resolveStation(opts.channel or 0)
self.radio = Pokegear.Radio.new({
data = self.gear:radioData(), rng = opts.radioRng,
})
self.radio:tune(self.station)
-- radio.channel, a Gen 2 invention: Gen 1 has no radio, so there is no name
-- to share. Raised on the tune rather than per show line, because the
-- station is what a mod reasons about -- the show under it is the same state
-- machine the Pokegear's radio card runs and reports its song through
-- music.started like everything else.
--
-- station the LoadStation_* id the dial landed on
-- channel the MAPRADIO_* index the script's setval passed in; 0 is the
-- Pokemon Channel position, which resolves by region and hour
-- name the station's display name, the one quoted in the text box
-- source "map_radio" -- the wall radio, as against the Pokegear card
if Runtime.wants("radio.channel") then
Runtime.emit("radio.channel", {
station = self.station, channel = opts.channel or 0,
name = self:name(),
source = "map_radio",
})
end
-- .PlayStation parks ENTER_MAP_MUSIC before the show's own
-- RadioMusicRestartDE lands the channel song, so backing out before the
-- show starts still restores the map theme.
self.radioMusicPlaying = "enterMap"
self.radioSong = nil
-- `ld c, 100 / call DelayFrames`: the name sits alone in the box before
-- the show's first line, and no button is read during the delay.
self.hold = 100
return self
end
-- LoadStation_PokemonChannel: in Johto the morning airs the Pokedex Show and
-- the rest of the day Oak's Pokemon Talk; from Kanto the same dial position
-- carries Places & People. Answers the station and, when the record carries
-- one, its own display name.
function MapRadio:resolveStation(channel)
if channel ~= 0 then
local record, station = MapRadio.channelRecord(
self.game and self.game.data, channel)
if station then return station, record and record.name end
return "OAKS_POKEMON_TALK", nil
end
if self.gear:region() ~= "johto" then return "PLACES_AND_PEOPLE" end
if (self.gear:timeOfDayIndex() or 0) == 0 then return "POKEDEX_SHOW" end
return "OAKS_POKEMON_TALK"
end
-- The name quoted in the text box. A loaded game resolves it through the
-- same registry as the Pokegear; STATION_NAMES is only the loader-free fallback.
function MapRadio:name()
if self.stationName then return self.stationName end
return self.gear and self.gear:stationName(self.station)
or Pokegear.STATION_NAMES[self.station]
end
function MapRadio:close()
Pokegear.exitRadioMusic(self.game, self.radioMusicPlaying)
self.radioMusicPlaying = nil
local stack = self.game and self.game.stack
if stack then stack:pop() end
if self.onDone then self.onDone() end
end
function MapRadio:update(_dt)
if self.hold > 0 then
self.hold = self.hold - 1
return
end
local input = self.game and self.game.input
if input and (input:wasPressed("a") or input:wasPressed("b")) then
self:close()
return
end
self.radio:step()
local song = self.radio.music
if song and song ~= self.radioSong then
self.radioSong = song
self.radioMusicPlaying = Pokegear.radioPlayingValue(song)
local data = self.game and self.game.data
if data then pcall(require("src.core.Music").play, data, song) end
end
end
function MapRadio:draw()
-- PlayRadio's own frame: Textbox at (0,12), 4 rows by 18, with the station
-- name quoted at (2,14) until the show's first line scrolls in.
Chrome.textbox(0, 12, 18, 4)
local radio = self.radio
if radio.top == "" and radio.bottom == "" then
local name = self:name() or ""
Chrome.print("“" .. name .. "”", 1, 14)
return
end
if radio.top ~= "" then Chrome.print(radio.top, 1, 14) end
if radio.bottom ~= "" then Chrome.print(radio.bottom, 1, 16) end
end
MapRadio.STATIONS = STATIONS
return MapRadio