Files
gen1recomp/tests/engine/ss_anne_captain_face_bug2189.lua
2026-09-05 09:03:47 -04:00

173 lines
6.6 KiB
Lua

-- pokered engine/overworld/movement.asm:406-414
-- scripts/SSAnneCaptainsRoom.asm:5-10,45-68
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
love = love or require("tests.love_stub")
local OW = require("src.world.OverworldController")
local Commands = require("src.script.Commands")
local GameVersion = require("src.core.GameVersion")
local story = require("data.scripts.story")
local player = { cellX = 3, cellY = 5 }
local function newNpc()
return { facing = "up",
facePlayer = function(self) self.facing = "down" end }
end
local ow = setmetatable({ player = player }, { __index = OW })
local npc = newNpc()
ow.noNpcFacePlayer = true
ow:makeNpcFacePlayer(npc)
eq(npc.facing, "up", "the no-face flag leaves the NPC's facing alone")
ow.noNpcFacePlayer = nil
ow:makeNpcFacePlayer(npc)
eq(npc.facing, "down", "with the flag clear the NPC turns to the player")
ow.noNpcFacePlayer = true
ow:makeNpcFacePlayer(nil)
local ctx = { overworld = ow }
Commands.no_npc_face_player(ctx, true)
eq(ow.noNpcFacePlayer, true, "no_npc_face_player true sets the flag")
Commands.no_npc_face_player(ctx, false)
eq(ow.noNpcFacePlayer, nil, "no_npc_face_player false clears the flag")
Commands.no_npc_face_player({}, true)
local room = story.SS_ANNE_CAPTAINS_ROOM
check(type(room.onEnter) == "function", "SS_ANNE_CAPTAINS_ROOM has a map script")
local saved = GameVersion.get()
local function entered(version, flags)
GameVersion.set(version)
local state = {}
room.onEnter({ save = { flags = flags } }, state)
return state.noNpcFacePlayer
end
eq(entered("yellow", {}), true, "yellow sets the bit before HM01")
eq(entered("yellow", { EVENT_RUBBED_CAPTAINS_BACK = true }), true,
"yellow gates on EVENT_GOT_HM01, not the rub")
eq(entered("yellow", { EVENT_GOT_HM01 = true }), nil, "yellow clears it after HM01")
eq(entered("red", {}), true, "red sets the bit before the rub")
eq(entered("red", { EVENT_RUBBED_CAPTAINS_BACK = true }), nil,
"red gates on EVENT_RUBBED_CAPTAINS_BACK")
GameVersion.set(saved)
local rows = room.talk.TEXT_SSANNECAPTAINSROOM_CAPTAIN
local rubAt, optsAt, clearAt, rubbedAt, giveAt, rearmAt, gotAt, finalClearAt
for i, row in ipairs(rows) do
check(row[1] ~= "play_once",
"no bare play_once row survives -- the jingle rides the box")
if row[1] == "show_text"
and row[2] == "_SSAnneCaptainsRoomRubCaptainsBackText" then rubAt = i end
if row[1] == "text_opts" then optsAt = i end
if row[1] == "no_npc_face_player" then
if row[2] == true then rearmAt = i
elseif clearAt then finalClearAt = i
else clearAt = i end
end
if row[1] == "set_flag" and row[2] == "EVENT_RUBBED_CAPTAINS_BACK" then
rubbedAt = i
end
if row[1] == "set_flag" and row[2] == "EVENT_GOT_HM01" then gotAt = i end
if row[1] == "give_item" then giveAt = i end
end
check(rubAt, "the rub text is still shown")
eq(optsAt, rubAt - 1, "a text_opts row arms the rub box")
check(rubbedAt and rubbedAt > rubAt, "EVENT_RUBBED_CAPTAINS_BACK is set after the rub")
check(clearAt, "a no_npc_face_player row clears the bit")
eq(rows[clearAt][2], false, "that row clears rather than sets")
eq(clearAt, rubbedAt + 1, "the rub tail clears the bit before the gift (SSAnneCaptainsRoom.asm:64-66)")
check(finalClearAt and gotAt and finalClearAt == gotAt + 1,
"the success path clears it again after EVENT_GOT_HM01 (:31-33)")
if require("src.core.GameVersion").isYellow() then
eq(rearmAt, nil, "Yellow never re-arms the bit: a full bag still turns the captain")
else
eq(rearmAt, giveAt - 1, "Red re-arms the bit right before GiveItem so a full bag halts with him back-turned (pokered :34-37)")
end
local auto = rows[optsAt][2].auto
eq(auto.wait, false, "the rub box never waits for A (bare text terminator)")
eq(auto.delay, 0, "WaitForSoundToFinish has no trailing Delay3")
check(type(auto.sound) == "function", "the rub box starts the jingle itself")
local Music = require("src.core.Music")
local playedSong
local realPlayOnce, realOnePlaying = Music.playOnce, Music.oneShotPlaying
local playing = true
Music.playOnce = function(_, song) playedSong = song; return true end
Music.oneShotPlaying = function() return playing end
local src = auto.sound()
eq(playedSong, "Music_PkmnHealed", "the rub box plays MUSIC_PKMN_HEALED")
check(src and src.isPlaying(src), "the box holds while the jingle plays")
eq(src.getDuration(src), 10, "the source reports a duration past the 180-frame ceiling")
playing = false
eq(src.isPlaying(src), false, "and lets go once the jingle ends")
Music.playOnce = function() return false end
eq(auto.sound(), nil, "a jingle that never started drops the hold")
Music.playOnce, Music.oneShotPlaying = realPlayOnce, realOnePlaying
for i, row in ipairs(rows) do
if row[1] == "jump" or row[1]:sub(1, 8) == "jump_if_" then
local target = row[#row]
check(target == "end" or (type(target) == "number" and target >= 1 and target <= #rows),
string.format("row %d jumps to a real row (%s)", i, tostring(target)))
end
end
eq(rows[2][2], #rows, "the already-healed branch jumps to the last row")
eq(rows[#rows - 1][2], "end", "the healed path halts with the reserved end target")
eq(#require("src.script.ScriptRunner").validate(rows), 0, "the captain rows pass validate")
local function setUpvalue(fn, name, value)
local i = 1
while true do
local n = debug.getupvalue(fn, i)
if not n then return false end
if n == name then
debug.setupvalue(fn, i, value)
return true
end
i = i + 1
end
end
local talkDone
local function talkTo(state, who)
talkDone = nil
state:showMapText("TEXT_X", who, function() end)
return talkDone
end
check(setUpvalue(OW.showMapText, "mapScripts", {
talkScript = function() return function(_, _, _, onDone) talkDone = onDone end end,
talkSource = function() end,
}), "showMapText reads the script registry through an upvalue")
local room2 = setmetatable({ player = player, map = { id = 1, def = { label = "X" } } },
{ __index = OW })
room2.noNpcFacePlayer = true
local captain = newNpc()
local done = talkTo(room2, captain)
eq(captain.facing, "up", "the captain keeps his back turned at talk time")
room2.noNpcFacePlayer = nil
done()
eq(captain.facing, "down", "and turns once the conversation ends with the bit cleared")
room2.noNpcFacePlayer = nil
local other = newNpc()
done = talkTo(room2, other)
eq(other.facing, "down", "an ordinary NPC faces the player at talk time")
other.facing = "left"
done()
eq(other.facing, "left", "and is not re-faced when the conversation ends")
room2.noNpcFacePlayer = true
done = talkTo(room2, nil)
done()
T.finish("ss_anne_captain_face_bug2189")