Files
gen1recomp/tests/drivers/gold_exp_participants_bug1513.lua
T
bryanthaboi c0f4315cd8 Fix Gen 1/2 battle, menu, PC and audio parity gaps; wire luacheck into CI
CLOSES #1484, CLOSES #1486, CLOSES #1513, CLOSES #1517, CLOSES #1556, CLOSES #1564, CLOSES #1567
2026-08-21 19:22:34 -04:00

198 lines
7.4 KiB
Lua

-- #1513: exp credit has to be per THIS appearance of the enemy's active mon.
-- ResetBattleParticipants falls through into AddBattleParticipant
-- (engine/battle/core.asm:3033 and :3037), and AI_Switch farcalls it right
-- after EnemySwitch (engine/battle/ai/items.asm:697), so the moment the rival
-- rotates, the only mon still credited is the one the player has on the field.
--
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 POKEPORT_IDENTITY=gold-dev \
-- POKEPORT_DRIVER=tests/drivers/gold_exp_participants_bug1513.lua love .
--
-- What to look for, in order:
-- 00 CYNDAQUIL is out against JOEY's GEODUDE.
-- 01 TOTODILE has been switched in through the real party menu, so BOTH
-- party mons have now "met" GEODUDE.
-- 02 "JOEY withdrew GEODUDE!" -- the rotation the ticket is about.
-- 03 the exp lines for the KO of the mon that came IN (PIDGEY). Only
-- TOTODILE may be named here. A "CYNDAQUIL gained N EXP. Points!" line
-- in this shot is the bug.
-- 04 the party screen: CYNDAQUIL's level and EXP bar are pixel-identical to
-- shot 01's, TOTODILE's have moved.
-- 05 GEODUDE comes back out and is KO'd. CYNDAQUIL still gains nothing --
-- that half is correct cart behavior (ram/wram.asm:775-776, "All bits
-- cleared if the enemy faints") and must NOT be "fixed".
local U = require("tests.drivers.util")
local Mon = require("src.battle.gen2.Mon")
local function battleScreen(game)
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 runToPhase(game, screen, phase, frames)
for _ = 1, (frames or 600) do
if screen.phase == phase then return true end
if screen.battle.over then return false end
U.tap(game, "a")
U.wait(3)
end
return false
end
-- Page the queue out, shooting the first box that carries `needle`.
local function pageUntilMenu(game, screen, out, needles)
local shot = {}
for _ = 1, 1200 do
local message = screen.message
if message then
for needle, path in pairs(needles) do
if not shot[needle] and message:find(needle, 1, true) then
U.shot(game, out .. "/" .. path)
shot[needle] = message
end
end
end
if screen.phase == "menu" or screen.phase == "done"
or screen.battle.over then
break
end
U.tap(game, "a")
U.wait(3)
end
return shot
end
return function(game)
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1513"
U.wait(45)
local world = game.world
assert(world and world.map, "gold world did not boot")
local failures = {}
local function check(ok, what)
print((ok and "[ok] " or "[FAIL] ") .. what)
if not ok then failures[#failures + 1] = what end
end
local lead = Mon.new(game.data, "CYNDAQUIL", 20)
lead.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
local bench = Mon.new(game.data, "TOTODILE", 20)
bench.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
game.save.party = { lead, bench }
local foe1 = Mon.new(game.data, "GEODUDE", 8)
foe1.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
local foe2 = Mon.new(game.data, "PIDGEY", 8)
foe2.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
-- attributes[6] is the low byte of TRNATTR_AI_ITEM_SWITCH: OFTEN
-- (src/battle/gen2/Ai.lua:1307). The real Rival1 class is SWITCH_SOMETIMES
-- (pokegold:data/trainers/attributes.asm), the same path at a lower roll.
assert(world:startBattle({ trainer = { class = "YOUNGSTER", name = "JOEY",
party = { foe1, foe2 }, attributes = { 0, 0, 0, 0, 0, 0x01, 0 } } }),
"trainer startBattle failed")
local screen = battleScreen(game)
assert(runToPhase(game, screen, "menu", 400), "never reached the menu")
U.shot(game, out .. "/00-lead-out.png")
------------------------------------------------- the player's own switch
-- Through the real party menu, so the human sees the same path they used.
screen:chooseMenu("party")
for _ = 1, 240 do
if screen.battle.player == bench then break end
local top = game.stack:top()
if top ~= screen then
U.tap(game, "down")
U.wait(3)
U.tap(game, "a")
U.wait(6)
U.tap(game, "a")
U.wait(6)
else
U.wait(2)
end
end
check(screen.battle.player == bench, "TOTODILE was switched in")
check(screen.battle.participants[1] and screen.battle.participants[2],
"both party mons are credited against GEODUDE")
pageUntilMenu(game, screen, out, {})
U.shot(game, out .. "/01-switched.png")
print(("[driver] before the rotation: cyndaquil %d totodile %d")
:format(lead.experience, bench.experience))
local leadExp = lead.experience
local benchExp = bench.experience
------------------------------------------------------- the AI's rotation
-- Perish Song at one turn left is CheckAbleToSwitch's maximum score, the
-- cheapest way to make the AI commit to the rotation on demand.
screen.battle:volatile(screen.battle.enemy).perish = 1
screen:submit({ kind = "move", move = "TACKLE" })
local seen = pageUntilMenu(game, screen, out, {
["withdrew"] = "02-rival-withdrew.png",
["gained"] = "03-exp-lines.png",
})
check(seen["withdrew"] ~= nil, "JOEY withdrew GEODUDE")
check(screen.battle.enemy == foe2, "and sent PIDGEY out")
check(screen.battle.participants[1] == nil,
"CYNDAQUIL lost its credit when the rival rotated")
check(screen.battle.participants[2] == true,
"and TOTODILE, the mon on the field, kept it")
------------------------------------------------------------ the KO payout
if not screen.battle.over and (foe2.hp or 0) > 0 then
if screen.phase ~= "menu" then
runToPhase(game, screen, "menu", 400)
end
foe2.hp = 1
screen:submit({ kind = "move", move = "TACKLE" })
local paid = pageUntilMenu(game, screen, out, {
["gained"] = "03-exp-lines.png",
})
if paid["gained"] then print("[driver] exp box: " .. paid["gained"]) end
end
print(("[driver] after the PIDGEY KO: cyndaquil %d totodile %d")
:format(lead.experience, bench.experience))
check(lead.experience == leadExp,
"CYNDAQUIL earned NOTHING from the mon it never faced")
check(bench.experience > benchExp, "TOTODILE was paid for it")
if screen.phase == "menu" then
screen:chooseMenu("party")
U.wait(20)
U.shot(game, out .. "/04-party-exp.png")
U.tap(game, "b")
U.wait(10)
if game.stack:top() ~= screen then
U.tap(game, "b")
U.wait(10)
end
end
------------------------------------------- GEODUDE comes back and faints
-- Correct cart behavior: no residual credit for a previous appearance.
leadExp = lead.experience
benchExp = bench.experience
if not screen.battle.over then
runToPhase(game, screen, "menu", 600)
if screen.phase == "menu" and screen.battle.enemy == foe1 then
foe1.hp = 1
screen:submit({ kind = "move", move = "TACKLE" })
pageUntilMenu(game, screen, out, { ["gained"] = "05-geodude-ko.png" })
print(("[driver] after the GEODUDE KO: cyndaquil %d totodile %d")
:format(lead.experience, bench.experience))
check(lead.experience == leadExp,
"CYNDAQUIL still earns nothing when GEODUDE comes back (correct)")
check(bench.experience > benchExp, "and TOTODILE takes the whole share")
else
print("[driver] GEODUDE never came back out; second half skipped")
end
end
if #failures > 0 then
error(#failures .. " exp participant checks failed")
end
print("[driver] #1513 fixed; shots in " .. out)
end