Files
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

240 lines
8.5 KiB
Lua

-- Gold's intro seams: the Oak speech under Gen 1's names, and the GS boot
-- cinema under its own.
--
-- Two different claims are being made here, and they are deliberately not the
-- same claim:
--
-- * src/ui/gen2/OakSpeech.lua reuses `intro.oak_speech.build` and the four
-- `intro.oak_speech.*` events VERBATIM -- same names, same payload keys,
-- same moments -- because Gold has a real Oak speech and a mod written for
-- Red's must land on Gold's without a second listener.
-- * the copyright card, the GAME FREAK splash, the attract movie and the
-- Ho-Oh title have NO Gen 1 counterpart, so they take new `intro.boot.*`
-- names rather than borrowing one that would then mean two things.
--
-- Both halves are also the parity case the no-mod gate wants: with nobody
-- subscribed every one of these sites must be inert.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Events = require("src.mods.Events")
local Hooks = require("src.mods.Hooks")
local Logger = require("src.core.Logger")
local Runtime = require("src.mods.Runtime")
local CopyrightSplash = require("src.ui.gen2.CopyrightSplash")
local GameFreakPresents = require("src.ui.gen2.GameFreakPresents")
local GoldSilverIntro = require("src.ui.gen2.GoldSilverIntro")
local OakSpeech = require("src.ui.gen2.OakSpeech")
local TitleState = require("src.ui.gen2.TitleState")
-- The shared names, spelled out so a rename has to come through this file.
local SPEECH_EVENTS = {
"intro.oak_speech.started",
"intro.oak_speech.step",
"intro.oak_speech.answered",
"intro.oak_speech.finished",
}
-- ...and the Gen 2-only ones.
local BOOT_EVENTS = {
"intro.boot.copyright",
"intro.boot.gamefreak",
"intro.boot.movie",
"intro.boot.movie_ended",
"intro.boot.title",
}
local savedEvents, savedHooks = Runtime.events, Runtime.hooks
local function logged(fragment)
for _, line in ipairs(Logger.history or {}) do
if line:find(fragment, 1, true) then return true end
end
return false
end
local function fakeGame()
return { data = {}, save = { player = {} } }
end
-- ------- 1. no-mod parity: every site cold
do
Runtime.events, Runtime.hooks = savedEvents, savedHooks
for _, name in ipairs(SPEECH_EVENTS) do
T.eq(Runtime.wants(name), false, "no subscriber leaves " .. name .. " cold")
end
for _, name in ipairs(BOOT_EVENTS) do
T.eq(Runtime.wants(name), false, "no subscriber leaves " .. name .. " cold")
end
T.eq(Runtime.wantsHook("intro.oak_speech.build"), false,
"no wrapper leaves intro.oak_speech.build cold")
-- the four boot cards still run their moment with nobody listening
CopyrightSplash.new(fakeGame(), {}):enter()
GameFreakPresents.new(fakeGame(), {}):enter()
local movie = GoldSilverIntro.new(fakeGame(), {})
movie:enter()
movie:finish()
TitleState.new(fakeGame(), {}):enter()
T.check(true, "the boot cinema runs unsubscribed")
end
-- ------- 2. the Oak speech step list
do
local speech = OakSpeech.new(fakeGame(), {})
local steps = OakSpeech.defaultSteps(speech)
T.eq(#steps, 9, "Gold's vanilla speech has nine beats")
T.eq(steps[1].id, "init_clock",
"the speech opens on the farcall InitClock beat")
T.eq(steps[#steps].id, "shrink", "and ends on ShrinkPlayer")
-- the anchors a Gen 1 mod already knows how to aim at
local byId = {}
for index, step in ipairs(steps) do byId[step.id] = index end
for _, id in ipairs({ "oak_welcome", "demo_mon", "world_spiel",
"ask_player_name", "name_player", "legend",
"shrink" }) do
T.check(byId[id] ~= nil,
"Gold keeps Gen 1's step anchor `" .. id .. "`")
end
T.check(byId.oak_welcome < byId.demo_mon
and byId.demo_mon < byId.name_player
and byId.name_player < byId.shrink,
"and keeps them in Gen 1's order")
end
-- ------- 3. intro.oak_speech.build, the hook Gen 1 already ships
do
Runtime.events, Runtime.hooks = Events.new(), Hooks.new()
local speech = OakSpeech.new(fakeGame(), {})
Runtime.hooks:wrap("intro.oak_speech.build", function(nextFn, steps, sp)
steps = nextFn(steps, sp)
table.insert(steps, 3, { id = "extra_q", kind = "choice",
saveKey = "mood", choices = { "FINE", "TIRED" } })
return steps
end, 0, "fixture")
local built = speech:buildSteps()
T.eq(built[3].id, "extra_q",
"intro.oak_speech.build can insert a beat into Gold's speech")
T.eq(built[4].id, "demo_mon", "later vanilla beats shift down")
Runtime.hooks:removeOwner("fixture")
Runtime.hooks:wrap("intro.oak_speech.build", function() return 42 end, 0, "bad")
built = speech:buildSteps()
T.eq(built[1].id, "init_clock",
"a non-table intro.oak_speech.build result degrades to vanilla")
T.check(logged("intro.oak_speech.build returned"),
"and the degrade is logged, as it is under Gen 1")
Runtime.hooks:removeOwner("bad")
end
-- ------- 4. the four lifecycle events, with the Gen 1 payload keys
do
Runtime.events, Runtime.hooks = Events.new(), Hooks.new()
local speech = OakSpeech.new(fakeGame(), {})
-- Replace the beats with two that need no stack, so the lifecycle can be
-- driven headlessly: `fn` is the escape hatch a build wrapper gets.
Runtime.hooks:wrap("intro.oak_speech.build", function()
return {
{ id = "probe_one", kind = "fn",
run = function(sp, done) sp:recordAnswer({ id = "probe_one",
saveKey = "mood" }, 2, "TIRED", "TIRED") done() end },
{ id = "probe_two", kind = "fn", run = function(_, done) done() end },
}
end, 0, "fixture")
local started, stepped, answered, finished = nil, {}, nil, nil
Runtime.events:on("intro.oak_speech.started", function(ev)
started = ev
end, 0, "fixture")
Runtime.events:on("intro.oak_speech.step", function(ev)
stepped[#stepped + 1] = ev
end, 0, "fixture")
Runtime.events:on("intro.oak_speech.answered", function(ev)
answered = ev
end, 0, "fixture")
Runtime.events:on("intro.oak_speech.finished", function(ev)
finished = ev
end, 0, "fixture")
speech:enter()
T.check(started ~= nil and started.speech == speech
and type(started.steps) == "table",
"intro.oak_speech.started carries { speech, steps }")
T.eq(#stepped, 2, "intro.oak_speech.step fires once per beat")
T.eq(stepped[1].index, 1, "and carries the 1-based index")
T.eq(stepped[1].step.id, "probe_one", "and the step itself")
T.check(answered ~= nil and answered.saveKey == "mood"
and answered.value == "TIRED" and answered.index == 2
and answered.label == "TIRED" and answered.speech == speech,
"intro.oak_speech.answered carries Gen 1's six keys")
T.eq(speech.answers.mood, "TIRED", "and the answer is stored on the speech")
T.check(finished ~= nil and finished.answers == speech.answers,
"intro.oak_speech.finished carries the answer table")
-- #308's guard, ported: a second finish is not a second speech.
local firstFinish = finished
finished = nil
speech:finish()
T.eq(finished, nil, "finished fires exactly once per speech")
T.check(firstFinish ~= nil, "and it did fire the first time")
Runtime.events:removeOwner("fixture")
Runtime.hooks:removeOwner("fixture")
end
-- ------- 5. the GS boot cinema's own names
do
Runtime.events, Runtime.hooks = Events.new(), Hooks.new()
local seen = {}
for _, name in ipairs(BOOT_EVENTS) do
Runtime.events:on(name, function(ev) seen[name] = ev end, 0, "fixture")
end
local game = fakeGame()
CopyrightSplash.new(game, {}):enter()
T.check(seen["intro.boot.copyright"] ~= nil,
"the copyright card announces itself")
T.check(seen["intro.boot.copyright"].screen ~= nil,
"with the screen in the payload")
GameFreakPresents.new(game, {}):enter()
T.check(seen["intro.boot.gamefreak"] ~= nil,
"the GAME FREAK splash announces itself")
local movie = GoldSilverIntro.new(game, {})
movie:enter()
T.check(seen["intro.boot.movie"] ~= nil, "the attract movie announces itself")
movie:skip()
T.check(seen["intro.boot.movie_ended"] ~= nil,
"and announces its end")
T.eq(seen["intro.boot.movie_ended"].skipped, true,
"intro.boot.movie_ended reports a button skip")
-- a movie that plays out reports the other answer
seen["intro.boot.movie_ended"] = nil
local watched = GoldSilverIntro.new(game, {})
watched:enter()
watched:finish()
T.eq(seen["intro.boot.movie_ended"].skipped, false,
"and reports a movie that was watched to the end")
TitleState.new(game, {}):enter()
T.check(seen["intro.boot.title"] ~= nil, "the title screen announces itself")
Runtime.events:removeOwner("fixture")
end
Runtime.events, Runtime.hooks = savedEvents, savedHooks
T.finish("gen2_intro_seams")