mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
261 lines
9.5 KiB
Lua
261 lines
9.5 KiB
Lua
-- Parity: Route 22 + Cerulean rival post-battle exits (#154 / #168).
|
||
--
|
||
-- pokered Route22Rival{1,2}ExitMovementData* and CeruleanCityMovement3/4
|
||
-- plus CeruleanCityRivalText's Bill line after EVENT_BEAT_CERULEAN_RIVAL.
|
||
--
|
||
-- luajit tests/parity_rival_walkoff.lua
|
||
|
||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||
if not _G.love then _G.love = require("tests.love_stub") end
|
||
local S = require("tests.harness").suite("parity rival walkoff")
|
||
local check, eq = S.check, S.eq
|
||
|
||
-- restored at the bottom for the suites run after this file
|
||
local realMusic = package.loaded["src.core.Music"]
|
||
local realCommands = package.loaded["src.script.Commands"]
|
||
local realPicBox = package.loaded["src.ui.PicBox"]
|
||
package.loaded["src.core.Music"] = {
|
||
play = function() end, playOnce = function() return true end, stop = function() end,
|
||
playMap = function() end,
|
||
}
|
||
package.loaded["src.script.Commands"] = {
|
||
hide_object = function() end,
|
||
}
|
||
package.loaded["src.ui.PicBox"] = { new = function() return {} end }
|
||
|
||
local story5 = dofile("data/scripts/story5.lua")
|
||
local story = dofile("data/scripts/story.lua")
|
||
|
||
local function dirsEqual(a, b)
|
||
if type(a) ~= "table" or #a ~= #b then return false end
|
||
for i = 1, #b do if a[i] ~= b[i] then return false end end
|
||
return true
|
||
end
|
||
|
||
local function findWalk(rows)
|
||
for _, r in ipairs(rows) do
|
||
if r[1] == "walk_npc" then return r end
|
||
end
|
||
end
|
||
|
||
local function lastWalk(rows)
|
||
local found
|
||
for _, r in ipairs(rows) do
|
||
if r[1] == "walk_npc" then found = r end
|
||
end
|
||
return found
|
||
end
|
||
|
||
local function findRow(rows, name)
|
||
for i, r in ipairs(rows) do
|
||
if r[1] == name then return r, i end
|
||
end
|
||
end
|
||
|
||
local function texts(rows)
|
||
local out = {}
|
||
for _, r in ipairs(rows) do
|
||
if r[1] == "show_text" then out[#out + 1] = r[2] end
|
||
end
|
||
return out
|
||
end
|
||
|
||
local function capture(mapMod, game, x, y)
|
||
local rows
|
||
local ow = {
|
||
runner = {
|
||
isRunning = function() return false end,
|
||
run = function(_, r) rows = r end,
|
||
},
|
||
player = { facing = "down" },
|
||
npcByIndex = function() return { def = { name = "X" } } end,
|
||
}
|
||
check(mapMod.onStep(game, ow, x, y),
|
||
("onStep fires at (%d,%d)"):format(x, y))
|
||
check(rows ~= nil, "onStep queued rows")
|
||
return rows
|
||
end
|
||
|
||
-- Route22Rival1ExitMovementData1 / Data2, keyed on wSavedCoordIndex (which
|
||
-- Route22RivalBattleCoords entry matched, counted from 1) and NOT on the
|
||
-- rival's own row: index 1 is the (29,4) tile, where he stops BELOW the
|
||
-- player on (29,5) and leaves east; index 2 is the (29,5) tile, where he
|
||
-- stops LEFT of him on (28,5) and must step UP to row 4 to get around him.
|
||
-- This mapping was backwards until #236 (the y=4 walk began UP into the
|
||
-- cliff cell (28,3)), so these constants flipped with the fix.
|
||
local R1_Y4 = { "right", "right", "down", "down", "down", "down", "down" }
|
||
local R1_Y5 = { "up", "right", "right", "right",
|
||
"down", "down", "down", "down", "down", "down" }
|
||
-- Route22Rival2ExitMovementData1 falls through Data2, so index 1 (y=4) is
|
||
-- LEFT x4 from (29,5) and index 2 (y=5) LEFT x3 from (28,5)
|
||
local R2_Y4 = { "left", "left", "left", "left" }
|
||
local R2_Y5 = { "left", "left", "left" }
|
||
local CER_X20 = { "right", "down", "down", "down", "down", "down", "down" }
|
||
local CER_X21 = { "left", "down", "down", "down", "down", "down", "down" }
|
||
|
||
do
|
||
local game = { save = { flags = { EVENT_GOT_POKEDEX = true } }, data = {} }
|
||
local w5 = findWalk(capture(story5.ROUTE_22, game, 29, 5))
|
||
local w4 = findWalk(capture(story5.ROUTE_22, game, 29, 4))
|
||
check(dirsEqual(w5[3], R1_Y5), "Rival1 y=5 goes U,R×3,D×6 around the player")
|
||
check(dirsEqual(w4[3], R1_Y4), "Rival1 y=4 exits toward Viridian (R,R,D×5)")
|
||
local rows = capture(story5.ROUTE_22,
|
||
{ save = { flags = { EVENT_GOT_POKEDEX = true } }, data = {} }, 29, 5)
|
||
for _, r in ipairs(rows) do
|
||
check(not (r[1] == "move_npc_to" and r[3] == 25 and r[4] == 5),
|
||
"Rival1 must not retreat to spawn (25,5)")
|
||
end
|
||
end
|
||
|
||
do
|
||
local flags = {
|
||
EVENT_BEAT_BROCK = true,
|
||
EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE = true,
|
||
EVENT_BEAT_GIOVANNI = true,
|
||
}
|
||
local w5 = findWalk(capture(story5.ROUTE_22, { save = { flags = flags }, data = {} }, 29, 5))
|
||
local w4 = findWalk(capture(story5.ROUTE_22, { save = { flags = flags }, data = {} }, 29, 4))
|
||
check(dirsEqual(w5[3], R2_Y5), "Rival2 y=5 exits left×3 toward League")
|
||
check(dirsEqual(w4[3], R2_Y4), "Rival2 y=4 exits left×4 toward League")
|
||
end
|
||
|
||
do
|
||
local rows = capture(story5.CERULEAN_CITY, { save = { flags = {} }, data = {} }, 20, 6)
|
||
local t = texts(rows)
|
||
local hasBill = false
|
||
for _, id in ipairs(t) do
|
||
if id == "_CeruleanCityRivalIWentToBillsText" then hasBill = true end
|
||
end
|
||
check(hasBill, "Cerulean post-battle includes Bill dialogue")
|
||
local order = {
|
||
"_CeruleanCityRivalPreBattleText",
|
||
"_CeruleanCityRivalIWentToBillsText",
|
||
}
|
||
local pi = 1
|
||
for _, id in ipairs(t) do
|
||
if pi <= #order and id == order[pi] then pi = pi + 1 end
|
||
end
|
||
eq(pi, #order + 1, "Cerulean text order: pre, Bill")
|
||
check(dirsEqual(lastWalk(rows)[3], CER_X20),
|
||
"Cerulean x=20 exit: right then into town")
|
||
for _, r in ipairs(rows) do
|
||
check(not (r[1] == "move_npc_to" and r[4] == 2),
|
||
"Cerulean must not walk back north to (20,2)")
|
||
end
|
||
end
|
||
|
||
do
|
||
local rows = capture(story5.CERULEAN_CITY, { save = { flags = {} }, data = {} }, 21, 6)
|
||
check(dirsEqual(lastWalk(rows)[3], CER_X21),
|
||
"Cerulean x=21 exit: left then into town")
|
||
end
|
||
|
||
-- #2149 scripts/CeruleanCity.asm:84-99
|
||
for _, px in ipairs({ 20, 21 }) do
|
||
local rows = capture(story5.CERULEAN_CITY, { save = { flags = {} }, data = {} }, px, 6)
|
||
local place = findRow(rows, "place_npc")
|
||
check(place ~= nil, ("Cerulean x=%d places the rival before he walks"):format(px))
|
||
eq(place[2], 1, "place_npc addresses CERULEANCITY_RIVAL (object 1)")
|
||
eq(place[3], px, ("the rival spawns in the player's column (x=%d)"):format(px))
|
||
eq(place[4], 2, "the rival spawns on the object_event home row 2")
|
||
check(dirsEqual(findWalk(rows)[3], { "down", "down", "down" }),
|
||
("Cerulean x=%d approach is CeruleanCityMovement1"):format(px))
|
||
check(findRow(rows, "move_npc_to") == nil,
|
||
"the BFS walk is gone, so no fourth sideways step")
|
||
end
|
||
|
||
-- #2151 scripts/CeruleanCity.asm:141
|
||
local function assertEndBattleText(rows, what)
|
||
local save, si = findRow(rows, "save_end_battle_text")
|
||
check(save ~= nil, what .. " arms save_end_battle_text")
|
||
eq(save[2], "_CeruleanCityRivalDefeatedText", what .. " arms the loss line")
|
||
local _, bi = findRow(rows, "rival_battle")
|
||
eq(si, bi - 1, what .. " arms it on the row before rival_battle")
|
||
for _, r in ipairs(rows) do
|
||
check(not (r[1] == "show_text" and r[2] == "_CeruleanCityRivalDefeatedText"),
|
||
what .. " no longer prints the loss line from the script queue")
|
||
end
|
||
end
|
||
|
||
do
|
||
local rows = capture(story5.CERULEAN_CITY, { save = { flags = {} }, data = {} }, 21, 6)
|
||
assertEndBattleText(rows, "the Cerulean ambush")
|
||
assertEndBattleText(story.CERULEAN_CITY.talk.TEXT_CERULEANCITY_RIVAL,
|
||
"the Cerulean talk path")
|
||
end
|
||
|
||
do
|
||
local ScriptRunner = require("src.script.ScriptRunner")
|
||
local anyVerb = function() return true end
|
||
local scenes = {
|
||
["Cerulean ambush x=20"] =
|
||
capture(story5.CERULEAN_CITY, { save = { flags = {} }, data = {} }, 20, 6),
|
||
["Cerulean ambush x=21"] =
|
||
capture(story5.CERULEAN_CITY, { save = { flags = {} }, data = {} }, 21, 6),
|
||
["Cerulean talk"] = story.CERULEAN_CITY.talk.TEXT_CERULEANCITY_RIVAL,
|
||
["S.S. Anne 2F x=36"] =
|
||
capture(story5.SS_ANNE_2F, { save = { flags = {} }, data = {} }, 36, 8),
|
||
["S.S. Anne 2F x=37"] =
|
||
capture(story5.SS_ANNE_2F, { save = { flags = {} }, data = {} }, 37, 8),
|
||
}
|
||
for name, rows in pairs(scenes) do
|
||
local problems = ScriptRunner.validate(rows, anyVerb)
|
||
eq(#problems, 0, name .. " jump targets resolve: "
|
||
.. table.concat(problems, "; "))
|
||
end
|
||
end
|
||
|
||
-- #2164 scripts/SSAnne2F.asm:73
|
||
do
|
||
local game = { save = { flags = {} }, data = {} }
|
||
local kept
|
||
local function captureFacing(x)
|
||
local rows
|
||
local ow = {
|
||
runner = {
|
||
isRunning = function() return false end,
|
||
run = function(_, r) rows = r end,
|
||
},
|
||
player = { facing = "right" },
|
||
npcByIndex = function() return { def = { name = "X" } } end,
|
||
}
|
||
check(story5.SS_ANNE_2F.onStep(game, ow, x, 8),
|
||
("SS Anne onStep fires at (%d,8)"):format(x))
|
||
kept = ow.player.facing
|
||
return rows
|
||
end
|
||
|
||
local right = captureFacing(37)
|
||
eq(kept, "right", "the player keeps his walking facing while the rival comes down")
|
||
local turn, ti = findRow(right, "face_player_dir")
|
||
check(turn ~= nil, "x=37 turns the player as a scripted row")
|
||
eq(turn[2], "left", "x=37 turns the player left")
|
||
local _, mi = findRow(right, "move_npc_to")
|
||
local _, xi = findRow(right, "show_text")
|
||
check(ti > mi, "the turn happens after the rival's walk")
|
||
check(ti < xi, "the turn happens before the dialogue box")
|
||
|
||
local left = captureFacing(36)
|
||
eq(kept, "right", "x=36 leaves the player's facing alone")
|
||
check(findRow(left, "face_player_dir") == nil,
|
||
"x=36 never writes wPlayerMovingDirection")
|
||
end
|
||
|
||
do
|
||
local talk = story.CERULEAN_CITY.talk.TEXT_CERULEANCITY_RIVAL
|
||
check(talk ~= nil, "talk script for CERULEANCITY_RIVAL exists")
|
||
local hasBill = false
|
||
for _, r in ipairs(talk) do
|
||
if r[1] == "show_text" and r[2] == "_CeruleanCityRivalIWentToBillsText" then
|
||
hasBill = true
|
||
end
|
||
end
|
||
check(hasBill, "talk path also exposes Bill dialogue")
|
||
end
|
||
|
||
package.loaded["src.core.Music"] = realMusic
|
||
package.loaded["src.script.Commands"] = realCommands
|
||
package.loaded["src.ui.PicBox"] = realPicBox
|
||
|
||
S.finish()
|