Files
gen1recomp/tests/game3_scripted_warp_fade_test.lua
T
2026-09-23 16:34:15 -04:00

310 lines
11 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/scrcmd.c:719
-- pokefirered/src/field_fadetransition.c:535
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local MB_WARP_DOOR, MB_CAVE_DOOR, MB_NORMAL = 0x69, 0x60, 0x00
local sounds, musicFades, loads = {}, {}, {}
local Audio = {
playSe = function(id) sounds[#sounds + 1] = id end,
fadeOutBgm = function(speed) musicFades[#musicFades + 1] = speed end,
}
package.loaded["src.core.game3.audio"] = Audio
local Player = {
facing = "up", cellX = 6, cellY = 6, visible = true, spriteYOffset = 0, steps = {},
}
function Player.setVisible(v) Player.visible = v and true or false end
function Player.forceStep(dir, cb)
Player.facing = dir
Player.steps[#Player.steps + 1] = { dir = dir, cb = cb, left = 16 }
return true
end
package.loaded["src.core.game3.player"] = Player
local Fade = require("src.ui.game3.fade")
local Map = { current = "A" }
function Map.load(_, _, id, opts)
loads[#loads + 1] = { id = id, x = opts.x, y = opts.y, facing = opts.facing,
fadeT = Fade.t, fadeActive = Fade.active, visible = Player.visible,
parked = package.loaded["src.core.game3.field"]._fieldCallback }
Map.current = id
Player.cellX, Player.cellY = opts.x, opts.y
Player.facing = opts.facing
end
package.loaded["src.core.game3.map"] = Map
package.loaded["src.ui.game3.map_preview_screen"] = {
has = function() return false end,
isForestActive = function() return false end,
dismiss = function() end,
}
package.loaded["src.import.gba.map_preview_extract"] = { TYPE_CAVE = 1 }
local doorCalls = {}
local Doors = {}
function Doors.open(mapId, x, y, _, cb)
doorCalls[#doorCalls + 1] = { "open", mapId, x, y }
if cb then cb() end
end
function Doors.close(mapId, x, y, _, cb)
doorCalls[#doorCalls + 1] = { "close", mapId, x, y }
if cb then cb() end
end
function Doors.holdOpen(mapId, x, y)
doorCalls[#doorCalls + 1] = { "hold", mapId, x, y }
end
function Doors.closeAfterDelay(mapId, x, y, _, _, cb)
doorCalls[#doorCalls + 1] = { "close", mapId, x, y }
if cb then cb() end
end
function Doors.getSoundForWarp() return 0 end
Doors.SOUND_EXIT = require("src.core.game3.se_ids").SE_EXIT
function Doors.reset() end
package.loaded["src.core.game3.doors"] = Doors
local Flags = require("src.core.game3.scripting.flags")
package.loaded["src.core.game3.scripting.space"] = { store = Flags.newStore() }
local Field = { _fieldCallback = false, locked = false }
function Field.lock() Field.locked = true end
function Field.unlock() Field.locked = false end
package.loaded["src.core.game3.field"] = Field
local Collision = require("src.core.game3.collision")
local destBehavior = MB_NORMAL
Collision.behavior = function() return destBehavior end
local Task = require("src.core.game3.task")
local SE = require("src.core.game3.se_ids")
local Warp = require("src.core.game3.warp")
local game = { data = { maps = {
A = { mapType = 3, regionMapSectionId = 1, music = 100 },
B = { mapType = 8, regionMapSectionId = 1, music = 200 },
C = { mapType = 3, regionMapSectionId = 1, music = 100 },
} } }
local function frame()
Fade.tick(1 / 60)
Task.update(1 / 60)
for i = #Player.steps, 1, -1 do
local s = Player.steps[i]
s.left = s.left - 1
if s.left <= 0 then
table.remove(Player.steps, i)
if s.cb then s.cb() end
end
end
end
local function reset(from)
sounds, musicFades, loads, doorCalls = {}, {}, {}, {}
Fade.clear()
Task.clear()
Warp.clear()
Map.current = from or "A"
Player.steps = {}
Player.visible = true
Player.facing = "up"
destBehavior = MB_NORMAL
Audio._currentSong = { id = 100 }
Audio._fadeOut = nil
end
local function has(list, v)
for _, x in ipairs(list) do if x == v then return true end end
return false
end
local function run(kind, dest, maxFrames)
local done, doneAt, frames = false, nil, 0
local fadeAtDone
local peak = Fade.t or 0
Warp.scripted(nil, game, kind, dest, 6, 6, "up", function()
done = true
doneAt = frames
fadeAtDone = { t = Fade.t, active = Fade.active, parked = Field._fieldCallback }
end)
local startMode, startActive = Fade.mode, Fade.active
while not done and frames < (maxFrames or 400) do
frames = frames + 1
frame()
if (Fade.t or 0) > peak then peak = Fade.t end
end
return { done = done, doneAt = doneAt, fadeAtDone = fadeAtDone, peak = peak,
startMode = startMode, startActive = startActive }
end
print("[test] 1. warp fades to black with SE_EXIT, swaps the map under black, fades back in")
reset()
local r = run("warp", "B")
check(r.startActive and r.startMode == Fade.MODE.TO_BLACK, "a FADE_TO_BLACK starts on the warp frame")
check(has(sounds, SE.SE_EXIT), "SE_EXIT plays")
check(#musicFades == 1 and musicFades[1] == 2, "the old map music fades at the indoor speed 2")
check(#loads == 1 and loads[1].fadeT == 16 and not loads[1].fadeActive,
"the map loads only once the screen is fully black (t=" .. tostring(loads[1] and loads[1].fadeT) .. ")")
check(r.done and r.fadeAtDone.t == 0 and not r.fadeAtDone.active,
"done fires only after the fade-in finishes")
check(r.doneAt and r.doneAt >= 32, "the whole transition takes the fade out + in (frames=" .. tostring(r.doneAt) .. ")")
check(Warp.isBusy() == false, "Warp is released")
check(loads[1] and loads[1].parked == true and r.fadeAtDone.parked == false,
"ON_FRAME stays parked from the load until the fade-in ends")
print("[test] 2. warpsilent fades without SE_EXIT; same song keeps playing")
reset()
r = run("warpsilent", "C")
check(not has(sounds, SE.SE_EXIT), "no SE_EXIT")
check(#musicFades == 0, "same map song is not faded")
check(#loads == 1 and loads[1].fadeT == 16, "load under black")
check(r.done and r.fadeAtDone.t == 0, "fade-in done before done()")
print("[test] 3. FLAG_DONT_TRANSITION_MUSIC keeps the music")
reset()
local Space = package.loaded["src.core.game3.scripting.space"]
Flags.setFlag(Space.store, nil, "FLAG_DONT_TRANSITION_MUSIC", true)
r = run("warp", "B")
check(#musicFades == 0, "no music fade with the flag set")
Flags.setFlag(Space.store, nil, "FLAG_DONT_TRANSITION_MUSIC", false)
print("[test] 4. a covered screen is not faded out again")
reset()
Fade.t = 16
Fade.mode = Fade.MODE.TO_BLACK
Fade.active = false
r = run("seagallop", "C")
check(#loads == 1 and loads[1].fadeT == 16, "loaded under the existing black")
check(r.peak == 16 and r.done and r.fadeAtDone.t == 0, "faded in once")
check(not has(sounds, SE.SE_EXIT), "the ferry kind leaves SE_EXIT to the ferry")
print("[test] 5. a door destination runs the exit-door step")
reset()
destBehavior = MB_WARP_DOOR
r = run("warp", "C")
check(r.done, "done fired")
local opened, closed = false, false
for _, c in ipairs(doorCalls) do
if c[1] == "open" and c[2] == "C" and c[3] == 6 and c[4] == 6 then opened = true end
if c[1] == "close" and c[2] == "C" then closed = true end
end
check(opened and closed, "the destination door opened and closed")
check(Player.facing == "down" and Player.visible, "the player stepped out facing down")
check(r.fadeAtDone and r.fadeAtDone.t == 0, "the slow fade-in finished first")
print("[test] 6. a non-animated door destination steps the player out after the fade")
reset()
destBehavior = MB_CAVE_DOOR
r = run("warp", "C")
check(r.done and Player.visible and #Player.steps == 0, "stepped out and done")
print("[test] 7. warpspinenter loads at once and spins the player in with SE_WARP_OUT")
reset()
Player.facing = "right"
local doneFlag = false
Warp.scripted(nil, game, "warpspinenter", "C", 6, 6, nil, function() doneFlag = true end)
check(#loads == 1, "no fade-out before the load")
check(Fade.mode == Fade.MODE.FROM_BLACK and Fade.active, "fades in from black")
check(has(sounds, SE.SE_WARP_OUT), "SE_WARP_OUT plays")
check(Player.spriteYOffset < 0, "the player starts above the screen")
for _ = 1, 200 do
if doneFlag then break end
frame()
end
check(doneFlag and Player.spriteYOffset == 0 and Player.facing == "right",
"landed facing the saved direction (" .. tostring(Player.facing) .. ")")
print("[test] 8. warpteleport spins out with SE_WARP_IN before fading")
reset()
local tDone = false
Warp.scripted(nil, game, "warpteleport", "C", 6, 6, nil, function() tDone = true end)
check(has(sounds, SE.SE_WARP_IN), "SE_WARP_IN plays")
check(not Fade.active, "no fade while the player rises")
for _ = 1, 400 do
if tDone then break end
frame()
end
check(tDone and #loads == 1 and loads[1].fadeT == 16, "loaded under black after the spin-out")
print("[test] 10. the music check compares with the song actually playing")
reset()
Audio._currentSong = { id = 305 }
r = run("warp", "C")
check(#musicFades == 1, "surf music fades into a map whose default matches the old map's default")
reset()
Audio._currentSong = { id = 200 }
r = run("warp", "B")
check(#musicFades == 0, "the destination song already playing is not faded")
print("[test] 11. a scripted warp resets the player to face south")
reset()
Player.facing = "up"
r = run("warp", "C")
check(loads[1] and loads[1].facing == "down",
"loaded facing down, not the pre-warp facing (got " .. tostring(loads[1] and loads[1].facing) .. ")")
reset()
r = run("warpsilent", "B")
check(loads[1] and loads[1].facing == "down", "warpsilent also faces down")
print("[test] 12. an exit mat runs the same door exit as a scripted warp")
reset()
destBehavior = MB_WARP_DOOR
local exitDone = false
Warp.startDoorExit(nil, game, "C", 6, 6, 3, 7)
check(Field.locked, "the field is locked for the exit")
local exitOpenedAt, exitFrames = nil, 0
for _ = 1, 400 do
exitFrames = exitFrames + 1
frame()
for _, c in ipairs(doorCalls) do
if not exitOpenedAt and c[1] == "open" and c[2] == "C" and c[3] == 6 and c[4] == 6 then exitOpenedAt = exitFrames end
end
if not Warp.isBusy() then exitDone = true break end
end
local exitClosed = false
for _, c in ipairs(doorCalls) do
if c[1] == "close" and c[2] == "C" and c[3] == 6 and c[4] == 6 then exitClosed = true end
end
check(has(sounds, SE.SE_EXIT), "SE_EXIT plays on the exit mat")
check(exitOpenedAt ~= nil and exitOpenedAt >= 15 + 25, "the destination door opens 25 frames into the fade-in (frame "
.. tostring(exitOpenedAt) .. ")")
check(exitClosed and Player.facing == "down" and Player.visible, "stepped out facing down and the door closed")
check(exitDone and not Field.locked, "the exit finishes and releases the field")
print("[test] 9. the warp opcode hands its kind to the adapter")
local Vm = require("src.core.game3.scripting.vm")
for _, op in ipairs({ "warp", "warpsilent", "warpspinenter", "warpteleport" }) do
local gotKind
local vm = Vm.new({
store = Flags.newStore(),
scripts = { s = { { op = op, [1] = 1, [2] = 2, [3] = 255, [4] = 3, [5] = 4 }, { op = "waitstate" }, { op = "end" } } },
adapters = {
log = function() end,
warp = function(_, _, _, _, _, done, kind)
gotKind = kind
if done then done() end
end,
},
})
vm:start("s")
for _ = 1, 5 do vm:tick() end
check(gotKind == op, op .. " passes its kind (got " .. tostring(gotKind) .. ")")
end
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)