-- Parity: Team Rocket leaves Silph Co, and the president keeps talking (#392). -- -- Oracle: scripts/SilphCo11F.asm SilphCo11FTeamRocketLeavesScript walks -- .HideToggleableObjectIDs (TOGGLE_SILPH_CO_2F_2..11F_3 plus the Saffron -- street set, which M.SAFFRON_CITY handles) through predef HideObject just -- before SetEvent EVENT_BEAT_SILPH_CO_GIOVANNI. The ordinals come from -- constants/toggle_constants.asm and name the objects listed per floor in -- data/maps/toggleable_objects.asm, so the item balls, the rescued 2F/10F -- workers and the 7F rival keep their sprites. HideObject writes -- wMissableObjectFlags, which the port keeps in save.objectToggles. -- -- Oracle: scripts/SilphCo11F.asm SilphCo11FSilphPresidentText branches on -- EVENT_GOT_MASTER_BALL alone -- nz jumps to .got_item, which prints -- _SilphCo11FSilphPresidentMasterBallDescriptionText -- so every later talk -- prints something. -- -- Self-contained: `luajit tests/parity_silph_rockets.lua`; also globbed by -- tests/run_tests.lua. package.path = "./?.lua;./?/init.lua;" .. package.path if not _G.love then _G.love = require("tests.love_stub") end local Data = require("src.core.Data") if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end local S = require("tests.harness").suite("parity silph rockets") local check, eq = S.check, S.eq local MapScripts = require("src.script.MapScripts") require("data.scripts.init") local Commands = require("src.script.Commands") local ScriptRunner = require("src.script.ScriptRunner") local OverworldState = require("src.world.OverworldController") -- .HideToggleableObjectIDs, floor by floor, in the order the asm lists them local HIDDEN = { { "SILPH_CO_2F", { "SILPHCO2F_SCIENTIST1", "SILPHCO2F_SCIENTIST2", "SILPHCO2F_ROCKET1", "SILPHCO2F_ROCKET2" } }, { "SILPH_CO_3F", { "SILPHCO3F_ROCKET", "SILPHCO3F_SCIENTIST" } }, { "SILPH_CO_4F", { "SILPHCO4F_ROCKET1", "SILPHCO4F_SCIENTIST", "SILPHCO4F_ROCKET2" } }, { "SILPH_CO_5F", { "SILPHCO5F_ROCKET1", "SILPHCO5F_SCIENTIST", "SILPHCO5F_ROCKER", "SILPHCO5F_ROCKET2" } }, { "SILPH_CO_6F", { "SILPHCO6F_ROCKET1", "SILPHCO6F_SCIENTIST", "SILPHCO6F_ROCKET2" } }, { "SILPH_CO_7F", { "SILPHCO7F_ROCKET1", "SILPHCO7F_SCIENTIST", "SILPHCO7F_ROCKET2", "SILPHCO7F_ROCKET3" } }, { "SILPH_CO_8F", { "SILPHCO8F_ROCKET1", "SILPHCO8F_SCIENTIST", "SILPHCO8F_ROCKET2" } }, { "SILPH_CO_9F", { "SILPHCO9F_ROCKET1", "SILPHCO9F_SCIENTIST", "SILPHCO9F_ROCKET2" } }, { "SILPH_CO_10F", { "SILPHCO10F_ROCKET", "SILPHCO10F_SCIENTIST" } }, { "SILPH_CO_11F", { "SILPHCO11F_GIOVANNI", "SILPHCO11F_ROCKET1", "SILPHCO11F_ROCKET2" } }, } -- toggleable objects the hide list deliberately skips local KEPT = { { "SILPH_CO_2F", "SILPHCO2F_SILPH_WORKER_F" }, { "SILPH_CO_3F", "SILPHCO3F_HYPER_POTION" }, { "SILPH_CO_5F", "SILPHCO5F_CARD_KEY" }, { "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, { "SILPH_CO_7F", "SILPHCO7F_TM_SWORDS_DANCE" }, { "SILPH_CO_10F", "SILPHCO10F_SILPH_WORKER_F" }, { "SILPH_CO_11F", "SILPHCO11F_BEAUTY" }, } local function objOf(mapId, name) for _, o in ipairs(Data.maps[mapId].objects) do if o.name == name then return o end end end local function newSave(beat) return { flags = beat and { EVENT_BEAT_SILPH_CO_GIOVANNI = true } or {}, inventory = {}, objectToggles = {}, itemsTaken = {}, defeatedTrainers = {}, } end -- every name in both lists has to be a real object_event, or the toggle -- write lands on a key nothing reads for _, floor in ipairs(HIDDEN) do for _, name in ipairs(floor[2]) do check(objOf(floor[1], name) ~= nil, name .. " is an object_event on " .. floor[1]) end end for _, kept in ipairs(KEPT) do check(objOf(kept[1], kept[2]) ~= nil, kept[2] .. " is an object_event on " .. kept[1]) end -- ---------------------------------------------------------------- hide pass -- 11F's onEnter is the whole pass: it runs on the post-battle callback's -- floor and repairs saves that beat Giovanni before the list existed local function enter(mapId, save) local view = MapScripts.get(mapId) check(view and type(view.onEnter) == "function", mapId .. " has an onEnter hook") if view and view.onEnter then view.onEnter({ save = save }, nil) end end do local save = newSave(true) enter("SILPH_CO_11F", save) local hidden = 0 for _, floor in ipairs(HIDDEN) do for _, name in ipairs(floor[2]) do eq(save.objectToggles[floor[1]] and save.objectToggles[floor[1]][name], false, name .. " hidden by the 11F pass") check(not OverworldState.objectVisible(save, floor[1], objOf(floor[1], name)), name .. " no longer spawns") hidden = hidden + 1 end end eq(hidden, 31, "the asm hides 31 Silph objects on 2F-11F") for _, kept in ipairs(KEPT) do eq(save.objectToggles[kept[1]] and save.objectToggles[kept[1]][kept[2]], nil, kept[2] .. " is not in .HideToggleableObjectIDs") check(OverworldState.objectVisible(save, kept[1], objOf(kept[1], kept[2])), kept[2] .. " still spawns") end end -- each floor repairs itself on entry and touches no other floor, so a save -- that walks back in one elevator ride at a time still clears for _, floor in ipairs(HIDDEN) do local save = newSave(true) enter(floor[1], save) for _, name in ipairs(floor[2]) do eq(save.objectToggles[floor[1]][name], false, name .. " hidden on entering " .. floor[1]) end if floor[1] ~= "SILPH_CO_11F" then local others = 0 for mapId in pairs(save.objectToggles) do if mapId ~= floor[1] then others = others + 1 end end eq(others, 0, floor[1] .. " onEnter writes only its own floor") end end -- before the win nothing is hidden: the grunts are still battleable for _, floor in ipairs(HIDDEN) do local save = newSave(false) enter(floor[1], save) eq(next(save.objectToggles), nil, floor[1] .. " hides nothing while the event is unset") for _, name in ipairs(floor[2]) do check(OverworldState.objectVisible(save, floor[1], objOf(floor[1], name)), name .. " still spawns before Giovanni falls") end end -- ---------------------------------------------------------------- president local rows = MapScripts.get("SILPH_CO_11F").talk.TEXT_SILPHCO11F_SILPH_PRESIDENT check(type(rows) == "table", "the president has a hand-ported talk script") -- the shipped bug: the last row jumped past the end of the script, which -- validate reports and every branch walked into local problems = ScriptRunner.validate(rows) eq(#problems, 0, "president script validates: " .. table.concat(problems, "; ")) for _, row in ipairs(rows) do check(row[2] ~= "EVENT_BEAT_SILPH_CO_GIOVANNI", "no Giovanni gate: the teleport pads reach him without the trigger") end for _, key in ipairs({ "_SilphCo11FSilphPresidentText", "_SilphCo11FSilphPresidentReceivedMasterBallText", "_SilphCo11FSilphPresidentMasterBallDescriptionText" }) do local body = Data.text[key] check(type(body) == "string" and body ~= "", key .. " is extracted") end -- run the rows through the real interpreter with only the leaf commands -- stubbed, so the branch arithmetic is what is under test. Commands is a -- shared module singleton read by every later suite dofile'd into this -- same process (tests/run_tests.lua), so the three stubs must be restored -- before this file falls off the end -- an unrestored give_item stub -- silently swallows any later test's item grants, matching the save/ -- restore parity_gift_atomicity.lua already does for show_text. local origFacePlayer, origShowText, origGiveItem = Commands.face_player, Commands.show_text, Commands.give_item local shown, given Commands.face_player = function() end Commands.show_text = function(_, key) shown[#shown + 1] = key end Commands.give_item = function(_, item, count) given = { item, count } end local function talk(save) shown, given = {}, nil local runner = ScriptRunner.new({ data = Data, save = save }, nil) runner:exec(rows, runner:makeContext({})) return shown, given end do local save = newSave(true) local first = talk(save) eq(first[1], "_SilphCo11FSilphPresidentText", "first talk thanks the player") eq(given and given[1], "MASTER_BALL", "first talk hands over a MASTER BALL") eq(given and given[2], 1, "one ball, as in lb bc, MASTER_BALL, 1") eq(first[2], "_SilphCo11FSilphPresidentReceivedMasterBallText", "the got-item line prints after the give (sound_get_key_item rides it)") eq(#first, 2, "the description is not printed on the same talk") eq(save.flags.EVENT_GOT_MASTER_BALL, true, "SetEvent EVENT_GOT_MASTER_BALL") local second, secondGive = talk(save) eq(second[1], "_SilphCo11FSilphPresidentMasterBallDescriptionText", "second talk describes the ball instead of going silent") eq(#second, 1, "and prints nothing else") eq(secondGive, nil, "a second ball is not handed out") end -- the ball does not depend on Giovanni's coordinate trigger having fired do local fresh = talk(newSave(false)) eq(fresh[1], "_SilphCo11FSilphPresidentText", "an unbeaten-Giovanni save still gets the thank-you and the ball") end Commands.face_player, Commands.show_text, Commands.give_item = origFacePlayer, origShowText, origGiveItem S.finish()