Files
gen1recomp/tests/drivers/verify_battle_screen.lua
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

250 lines
9.1 KiB
Lua

-- Independent verification probe for the Gold battle screen lane.
-- Temporary: attacks the same claims from a different angle than
-- tests/drivers/gold_battle_screen_probe.lua.
--
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/verify_battle_screen.lua \
-- POKEPORT_VPROBE=switch1 POKEPORT_SHOT_DIR=/tmp/verify-battle/v love .
local U = require("tests.drivers.util")
local Mon = require("src.battle.gen2.Mon")
local OUT = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/verify-battle/v"
local hostVisible = love.visible
love.visible = function(v) if hostVisible then pcall(hostVisible, v) end end
local function tap(game, button, frames)
game.input.pressQueue[#game.input.pressQueue + 1] = button
game.input.state[button] = true
U.wait(2)
game.input.state[button] = false
U.wait(frames or 6)
end
local function openBattle(game, opts)
assert(game.world:startBattle(opts), "startBattle failed")
for _ = 1, 900 do
local top = game.stack:top()
if top and top.battle then return top end
U.wait(1)
end
error("battle screen never came up")
end
local function toMenu(game, battle, limit)
for _ = 1, (limit or 400) do
if battle.phase == "menu" then return true end
if battle.battle.over then return false end
tap(game, "a", 2)
end
return battle.phase == "menu"
end
local V = {}
-- Bug 4, the real player route: after the faint, move the cursor to a HEALTHY
-- mon and press A exactly ONCE. The claim under test is that the switch takes
-- on that press; the author's probe only ever proved the refusal LINE.
function V.switch1(game)
local weak = Mon.new(game.data, "CYNDAQUIL", 5)
weak.hp = 1
game.save.party = { weak, Mon.new(game.data, "TOTODILE", 30) }
game.save.inventory = {}
local entry = game.world:trainerParty(36, 1)
local Trainers = require("src.world.gen2.Trainers")
entry.party = Trainers.party(game.data, entry)
local battle = openBattle(game, { trainer = entry })
assert(toMenu(game, battle), "never reached the battle menu")
local aPresses, listOpens = 0, 0
local realOpen = battle.openParty
battle.openParty = function(self, forced)
if forced then listOpens = listOpens + 1 end
return realOpen(self, forced)
end
local switched, moved = false, false
for _ = 1, 1200 do
if battle.battle.over then break end
local top = game.stack:top()
if top ~= battle then
-- The forced list. Move down to the healthy mon FIRST, then one A.
if not moved then tap(game, "down", 4); moved = true end
aPresses = aPresses + 1
tap(game, "a", 6)
U.wait(20)
if battle.battle.player and (battle.battle.player.hp or 0) > 0
and battle.battle.player.species == "TOTODILE" then
switched = true
break
end
elseif battle.phase == "menu" then
tap(game, "a", 2); U.wait(3); tap(game, "a", 2)
else
tap(game, "a", 2)
end
end
print(("[v] switch1: listOpens=%d aPressesOnList=%d switched=%s active=%s")
:format(listOpens, aPresses, tostring(switched),
tostring(battle.battle.player and battle.battle.player.species)))
U.shot(game, OUT .. "/switch1-after.png")
end
-- Bug 3, the NORMAL save: currentBox is 1, the value every fresh Gold save and
-- every save the engine itself writes carries. If the catch only reached a box
-- because of the clamp, this is where that shows.
function V.box1(game)
local party = {}
for _ = 1, 6 do party[#party + 1] = Mon.new(game.data, "CYNDAQUIL", 30) end
game.save.party = party
game.save.boxes = nil
game.save.currentBox = tonumber(os.getenv("POKEPORT_VBOX") or "1")
game.save.inventory = { MASTER_BALL = 5 }
local battle = openBattle(game, { wild = Mon.new(game.data, "PIDGEY", 5) })
assert(toMenu(game, battle), "never reached the battle menu")
battle:useItem("MASTER_BALL")
for _ = 1, 600 do
if not battle.anim then break end
U.wait(1)
end
for _ = 1, 300 do
if battle.phase == "ask-nickname" then tap(game, "b", 4)
else tap(game, "a", 3) end
if battle.phase == "done" or not game.stack:top() then break end
end
local total, where = 0, "nowhere"
for i, box in pairs(game.save.boxes or {}) do
total = total + #box
if #box > 0 then where = "box" .. tostring(i) end
end
print(("[v] box1: currentBox=%s party=%d anywhere=%d landedIn=%s boxFilled=%s")
:format(tostring(game.save.currentBox), #game.save.party, total, where,
tostring(battle.battle.boxFilled)))
end
-- The picHidden REGRESSION risk: a trainer whose first mon faints must send its
-- SECOND one out and that mon must be visible. A latch that a send-out fails
-- to clear leaves an invisible opponent for the rest of the battle.
function V.secondmon(game)
game.save.party = { Mon.new(game.data, "TOTODILE", 40) }
game.save.inventory = {}
-- Find a trainer entry that actually carries two mons.
local Trainers = require("src.world.gen2.Trainers")
local entry, size
for class = 1, 60 do
for member = 1, 6 do
local ok, e = pcall(game.world.trainerParty, game.world, class, member)
if ok and e then
local party = Trainers.party(game.data, e)
if party and #party >= 2 then
entry, size = e, #party
e.party = party
break
end
end
end
if entry then break end
end
assert(entry, "no multi-mon trainer found")
print(("[v] secondmon: trainer class=%s members=%d")
:format(tostring(entry.classId or entry.class), size))
local battle = openBattle(game, { trainer = entry })
assert(toMenu(game, battle), "never reached the battle menu")
local sawSecond, hiddenAtSecond, shot = false, nil, 0
for _ = 1, 2000 do
if battle.battle.over then break end
if battle.battle.enemyIndex and battle.battle.enemyIndex > 1
and not sawSecond then
sawSecond = true
-- Let the send-out settle, then look at the latch and shoot it.
U.wait(90)
hiddenAtSecond = battle.picHidden.enemy
U.shot(game, OUT .. "/secondmon-out.png")
end
if battle.phase == "menu" then
tap(game, "a", 2); U.wait(3); tap(game, "a", 2)
else
if shot < 3 and battle.faintSlide then
U.shot(game, ("%s/secondmon-faint-%02d.png"):format(OUT,
battle.faintSlide.frames))
shot = shot + 1
end
tap(game, "a", 2)
end
end
print(("[v] secondmon: sawSecond=%s picHidden.enemy@second=%s outcome=%s")
:format(tostring(sawSecond), tostring(hiddenAtSecond),
tostring(battle.battle.outcome)))
U.shot(game, OUT .. "/secondmon-end.png")
end
-- Bug 2, the half that is cache-side: what the DEFAULT identity's Gold cache
-- actually carries, and what the class key resolves to.
function V.trainerpic(game)
local hud = game.data.gen2MenuGfx and game.data.gen2MenuGfx.battleHud
local pics = hud and hud.trainerPics
local n = 0
local sample
for k in pairs(pics or {}) do n = n + 1; sample = sample or k end
print(("[v] trainerpic: cache trainerPics=%s count=%d sample=%s")
:format(tostring(pics ~= nil), n, tostring(sample)))
local Trainers = require("src.world.gen2.Trainers")
local entry = game.world:trainerParty(36, 1)
print(("[v] trainerpic: lookup(36,1) class=%s classId=%s className=%s")
:format(tostring(entry and entry.class), tostring(entry and entry.classId),
tostring(entry and entry.className)))
entry.party = Trainers.party(game.data, entry)
local battle = openBattle(game, { trainer = entry })
print(("[v] trainerpic: enemyTrainerClass=%s showEnemyTrainer=%s path=%s")
:format(tostring(battle.enemyTrainerClass),
tostring(battle.showEnemyTrainer), tostring(battle.enemyTrainerPath)))
for i = 0, 8 do
U.shot(game, ("%s/trainerpic-%02d.png"):format(OUT, i))
U.wait(10)
end
end
-- The counterfactual to bug 1: a ball that FAILS. BattleAnim_ThrowPokeBall's
-- break-out arm puts the mon back on the field, so a latch set anywhere but on
-- the caught arm would make a wild mon vanish for the rest of the battle.
function V.missball(game)
game.save.party = { Mon.new(game.data, "TOTODILE", 40) }
game.save.inventory = { POKE_BALL = 30 }
local wild = Mon.new(game.data, "ONIX", 40) -- full HP, low catch rate
local battle = openBattle(game, { wild = wild })
assert(toMenu(game, battle), "never reached the battle menu")
local tries, escaped = 0, false
for _ = 1, 20 do
tries = tries + 1
battle:useItem("POKE_BALL")
for _ = 1, 600 do
if not battle.anim then break end
U.wait(1)
end
U.wait(40)
if battle.battle.outcome ~= "caught" and not battle.battle.over then
escaped = true
break
end
if battle.battle.over then break end
end
print(("[v] missball: tries=%d escaped=%s picHidden.enemy=%s outcome=%s")
:format(tries, tostring(escaped), tostring(battle.picHidden.enemy),
tostring(battle.battle.outcome)))
for i = 0, 3 do
U.shot(game, ("%s/missball-%02d.png"):format(OUT, i))
tap(game, "a", 8)
end
end
local name = os.getenv("POKEPORT_VPROBE") or "switch1"
return function(game)
U.wait(45)
assert(game.world and game.world.map, "gold world did not boot")
print("[v] ---- " .. name)
assert(V[name], "no such probe: " .. name)(game)
print("[v] done, shots in " .. OUT)
love.event.quit()
end