fixing bugs

This commit is contained in:
bryanthaboi
2026-07-25 12:35:00 -04:00
parent 9307488dc1
commit 347838619b
83 changed files with 5625 additions and 1160 deletions
+28
View File
@@ -0,0 +1,28 @@
-- Driver: #187 S.S. Anne cabin door entry. Entering a room from the
-- corridor should land one tile south of the door (pokered
-- PlayerStepOutFromDoor), not leave the player on the door tile.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
-- SS_ANNE_1F warp at (31,8) -> SS_ANNE_1F_ROOMS (rightmost cabin)
U.teleport(game, "SS_ANNE_1F", 31, 9, "up")
U.shot(game, DIR .. "/anne_0_before_enter.png")
local ow = game.overworld
U.log("before map:", ow.map.id, "pos:", ow.player.cellX, ow.player.cellY,
"facing:", ow.player.facing)
U.hold(game, "up", 20)
for _ = 1, 90 do
U.wait(1)
if ow.map.id == "SS_ANNE_1F_ROOMS" and not ow.transitioning
and #ow.scriptMoves == 0 and not ow.player.moving then
break
end
end
U.wait(8)
U.shot(game, DIR .. "/anne_1_after_enter.png")
U.log("after map:", ow.map.id, "pos:", ow.player.cellX, ow.player.cellY,
"facing:", ow.player.facing,
"onDoor:", tostring(ow.map:isWarpTileCell(ow.player.cellX, ow.player.cellY)))
end
@@ -0,0 +1,58 @@
-- Driver: mid-battle PKMN opens SWITCH / STATS / CANCEL (#180).
-- SHOT_DIR=/tmp/shots POKEPORT_DRIVER=tests/drivers/battle_party_submenu_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local BattleState = require("src.battle.BattleState")
game.save.party = {
Pokemon.new(game.data, "CHARMANDER", 12),
Pokemon.new(game.data, "SQUIRTLE", 10),
}
U.teleport(game, "ROUTE_1", 5, 5, "down")
local ow = game.overworld
local battle = BattleState.newWild(game, "PIDGEY", 8)
battle.onFinish = function() end
ow:pushBattle(battle)
local function mashUntil(cond, max)
for _ = 1, max or 80 do
if cond() then return true end
U.tap(game, "a")
U.wait(4)
end
return false
end
mashUntil(function() return battle.phase == "menu" end)
U.shot(game, DIR .. "/battle_party_0_menu.png")
-- FIGHT/PKMN/ITEM/RUN: right to PKMN, then A
U.tap(game, "right"); U.wait(4)
U.tap(game, "a"); U.wait(12)
U.shot(game, DIR .. "/battle_party_1_list.png")
local pm = game.stack:top()
U.log("party open:", pm and pm.onSwitch ~= nil)
U.tap(game, "a"); U.wait(8)
pm = game.stack:top()
U.shot(game, DIR .. "/battle_party_2_switch_stats.png")
local labels = {}
if pm and pm.submenu and pm.subItems then
for _, e in ipairs(pm.subItems) do
table.insert(labels, e.label)
end
end
U.log("submenu:", table.concat(labels, "/"))
U.log("order ok:", labels[1] == "SWITCH" and labels[2] == "STATS"
and labels[3] == "CANCEL")
-- STATS
U.tap(game, "down"); U.wait(4)
U.tap(game, "a"); U.wait(14)
U.shot(game, DIR .. "/battle_party_3_stats.png")
U.log("stats open:", game.stack:top() ~= pm)
end
+53 -1
View File
@@ -1,10 +1,12 @@
-- Driver: the two reworked battle flows.
-- Driver: reworked battle flows.
-- A) Old man catch tutorial (DisplayBattleMenu's old-man script):
-- scripted cursor FIGHT(80f) -> ITEM(50f), forced item menu with
-- one POKé BALL x50 -- itself scripted (list_menu.asm:65-80):
-- '▶' hover 80f, auto-A leaves the hollow '▷', always-caught throw.
-- B) Mimic's MID-move copy menu (MimicEffect): the chooser opens only
-- after the hit test, at (0,7) like MoveSelectionMenu .mimicmenu.
-- C) #162 trainer SHIFT offer: after KO'ing the first enemy mon,
-- "about to use" + "Will … change POKéMON?" with YES/NO.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
@@ -83,5 +85,55 @@ return function(game)
U.shot(game, DIR .. "/mimic_5_moves_after.png") -- slot now holds the copy
while game.stack:top() ~= ow do game.stack:pop() end
U.wait(5)
-- ------------------------------------------------ C) SHIFT about-to-use
-- Inject the foe-faint path at the menu so screenshots don't depend on
-- accuracy rolls; party has 2 slots so TrainerAboutToUseText fires.
local ChoiceBox = require("src.ui.ChoiceBox")
local PartyMenu = require("src.ui.PartyMenu")
game.save.options = game.save.options or {}
game.save.options.battleStyle = "shift"
game.save.options.animations = false
game.save.party = {
Pokemon.new(game.data, "CHARIZARD", 50),
Pokemon.new(game.data, "BLASTOISE", 50),
}
local trainer = BattleState.newTrainer(game, "OPP_YOUNGSTER", 1)
trainer.onFinish = function() end
ow:pushBattle(trainer)
mashUntil(function() return trainer.phase == "menu" end)
U.shot(game, DIR .. "/shift_0_menu.png")
trainer:markParticipant()
trainer.enemy.mon.hp = 0
trainer.enemyParty[1].hp = 0
trainer.phase = "messages"
trainer.afterQueue = "menu"
trainer:onFaint(trainer.enemy)
local function topIs(cls)
return getmetatable(game.stack:top()) == cls
end
local function textDone(needle)
local t = trainer.current and trainer.current.text
return t and t:find(needle, 1, true)
and (trainer.charIndex or 0) >= (trainer.total or 0)
end
mashUntil(function() return textDone("about to use") end, 200)
U.shot(game, DIR .. "/shift_1_about_to_use.png")
mashUntil(function() return textDone("change POKéMON") end, 80)
U.wait(20) -- ChoiceBox auto-opens once the page has typed out
mashUntil(function() return topIs(ChoiceBox) end, 40)
U.shot(game, DIR .. "/shift_2_will_change.png")
U.shot(game, DIR .. "/shift_3_yes_no.png")
U.tap(game, "a"); U.wait(8) -- YES
mashUntil(function() return topIs(PartyMenu) end, 40)
U.shot(game, DIR .. "/shift_4_party.png")
U.tap(game, "down"); U.wait(4)
U.tap(game, "a"); U.wait(8) -- BLASTOISE
mashUntil(function() return textDone("sent") end, 120)
U.shot(game, DIR .. "/shift_5_enemy_sent.png")
mashUntil(function() return textDone("BLASTOISE") end, 120)
U.shot(game, DIR .. "/shift_6_player_sent.png")
while game.stack:top() ~= ow do game.stack:pop() end
U.wait(5)
love.event.quit()
end
+120
View File
@@ -0,0 +1,120 @@
-- Driver: Bill's PC (#177) — chrome (What? / BOX No. / <PK><MN>) and
-- withdraw/deposit returning to BillsPCMenu instead of closing the PC.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Boxes = require("src.pokemon.Boxes")
local Menu = require("src.ui.Menu")
local ListMenu = require("src.ui.ListMenu")
local TextBox = require("src.render.TextBox")
local Pokemon = require("src.pokemon.Pokemon")
local pass = true
local function check(cond, msg)
if cond then U.log("PASS: " .. msg) else pass = false; U.log("FAIL: " .. msg) end
end
local function topIs(mt) return getmetatable(game.stack:top()) == mt end
local function topMenu()
local top = game.stack:top()
return getmetatable(top) == Menu and top or nil
end
local function mash(btn, cond, frames)
for _ = 1, frames or 120 do
if cond() then return true end
U.tap(game, btn)
U.wait(3)
end
return false
end
-- party + box stocked so withdraw and deposit both work
game.save.party = {
Pokemon.new(game.data, "PIKACHU", 12),
Pokemon.new(game.data, "EEVEE", 10),
}
Boxes.ensure(game.save)
game.save.currentBox = 1
game.save.boxes[1] = {
Pokemon.new(game.data, "SPEAROW", 8),
Pokemon.new(game.data, "RATTATA", 6),
}
game.save.flags = game.save.flags or {}
game.save.flags.EVENT_MET_BILL = true
-- PC tile at (13,3); stand on (13,4) facing up
U.teleport(game, "VIRIDIAN_POKECENTER", 13, 4, "up")
local ow = game.overworld
U.tap(game, "a") -- open PC
U.wait(6)
check(topIs(Menu), "PC main menu opened")
U.tap(game, "a") -- BILL'S PC
U.wait(8)
local bills = topMenu()
check(bills ~= nil, "Bill's PC menu opened")
check(bills and bills.items[1] and bills.items[1].label:find("<PK>", 1, true),
"WITHDRAW label includes <PK><MN> icon tokens")
U.shot(game, DIR .. "/bills_pc_1_menu.png")
-- WITHDRAW first box mon
U.tap(game, "a") -- WITHDRAW
U.wait(8)
check(topIs(ListMenu), "withdraw list opened")
U.tap(game, "a") -- pick SPEAROW
U.wait(6)
check(topIs(Menu), "withdraw mon submenu opened")
U.tap(game, "a") -- WITHDRAW action
U.wait(6)
check(mash("a", function() return topIs(TextBox) end, 60),
"withdraw shows taken-out text")
-- let the typewriter finish so the shot shows the taken-out line
for _ = 1, 90 do
local tb = game.stack:top()
if getmetatable(tb) == TextBox and (tb.waiting or tb.done) then break end
U.wait(2)
end
U.shot(game, DIR .. "/bills_pc_2_withdraw_text.png")
check(mash("a", function()
local m = topMenu()
return m and m.items and m.items[1]
and tostring(m.items[1].label):find("WITHDRAW", 1, true)
end, 120), "after withdraw, back on Bill's PC menu")
check(#game.save.party == 3, "party gained the withdrawn mon")
check(#game.save.boxes[1] == 1, "box lost the withdrawn mon")
U.shot(game, DIR .. "/bills_pc_3_menu_after_withdraw.png")
-- DEPOSIT party lead (PIKACHU)
U.tap(game, "down") -- DEPOSIT
U.wait(2)
U.tap(game, "a")
U.wait(8)
check(topIs(ListMenu), "deposit list opened")
U.tap(game, "a") -- first party mon
U.wait(6)
check(topIs(Menu), "deposit mon submenu opened")
U.tap(game, "a") -- DEPOSIT action
U.wait(6)
check(mash("a", function() return topIs(TextBox) end, 60),
"deposit shows stored text")
for _ = 1, 90 do
local tb = game.stack:top()
if getmetatable(tb) == TextBox and (tb.waiting or tb.done) then break end
U.wait(2)
end
U.shot(game, DIR .. "/bills_pc_4_deposit_text.png")
check(mash("a", function()
local m = topMenu()
return m and m.items and m.items[1]
and tostring(m.items[1].label):find("WITHDRAW", 1, true)
end, 120), "after deposit, back on Bill's PC menu")
check(#game.save.party == 2, "party lost the deposited mon")
check(#game.save.boxes[1] == 2, "box gained the deposited mon")
U.shot(game, DIR .. "/bills_pc_5_menu_after_deposit.png")
-- still inside the PC (not back on the overworld)
check(game.stack:top() ~= ow, "PC still open after transfers")
check(not topIs(ListMenu), "not stuck in a transfer list")
U.log(pass and "RESULT: ALL PASS" or "RESULT: SEE FAILURES ABOVE")
end
+95 -1
View File
@@ -1,15 +1,35 @@
-- Driver: throw Poké Balls and capture the catch suspense sequence
-- (toss -> poof -> mon hides -> ball shakes -> breakout or capture).
-- (toss -> poof -> mon hides -> ball shakes -> breakout or capture),
-- #159 nickname ask (text + white field, YES then NO), and #172
-- full-party catch nickname prompt before box transfer.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local ChoiceBox = require("src.ui.ChoiceBox")
table.insert(game.save.party, Pokemon.new(game.data, "CHARMANDER", 12))
U.teleport(game, "ROUTE_1", 5, 5, "down")
local ow = game.overworld
local BattleState = require("src.battle.BattleState")
local function topIs(cls)
return getmetatable(game.stack:top()) == cls
end
local function mashUntil(cond, n)
for _ = 1, (n or 400) do
if cond() then return true end
U.tap(game, "a")
U.wait(3)
end
return false
end
local function onNicknameChoice()
return topIs(ChoiceBox)
end
-- one throw, screenshotting every 20 frames through the chain
local function throwAndShoot(tag, rng)
local battle = BattleState.newWild(game, "PIDGEY", 8)
@@ -36,4 +56,78 @@ return function(game)
throwAndShoot("break", function(a, b) return b end)
-- rng low: clean capture, ball stays shut
throwAndShoot("caught", function(a, b) return a end)
-- #159: party catch -> nickname ask on white (YES cursor, then NO)
game.save.party = { Pokemon.new(game.data, "PIKACHU", 5) }
game.save.pokedex = game.save.pokedex or { owned = {}, seen = {} }
game.save.pokedex.owned.RATTATA = true
game.save.pokedex.seen.RATTATA = true
local battle = BattleState.newWild(game, "RATTATA", 3)
battle.onFinish = function() end
battle.rng = function(a, b) return a end
ow:pushBattle(battle)
for _ = 1, 14 do U.tap(game, "a"); U.wait(6) end
battle.phase = "messages"
battle.afterQueue = "menu"
battle:throwBall("POKE_BALL")
U.log("nick YES:", mashUntil(onNicknameChoice, 600))
U.shot(game, DIR .. "/catch_nickname_yes.png")
U.tap(game, "down")
U.wait(4)
U.shot(game, DIR .. "/catch_nickname_no.png")
U.tap(game, "a") -- confirm NO
U.wait(8)
for _ = 1, 20 do U.tap(game, "a"); U.wait(4) end
while game.stack:top() ~= ow do game.stack:pop() end
U.wait(5)
-- #172: full party of 6 -> nickname ask, then "sent to BOX" / PC text
game.save.party = {}
for _ = 1, 6 do
table.insert(game.save.party, Pokemon.new(game.data, "RATTATA", 5))
end
game.save.pokedex.owned.PIDGEY = true
game.save.pokedex.seen.PIDGEY = true
game.save.flags = game.save.flags or {}
game.save.flags.EVENT_MET_BILL = true
battle = BattleState.newWild(game, "PIDGEY", 8)
battle.onFinish = function() end
battle.rng = function(a, b) return a end
ow:pushBattle(battle)
for _ = 1, 14 do U.tap(game, "a"); U.wait(6) end
battle.phase = "messages"
battle.afterQueue = "menu"
battle:throwBall("POKE_BALL")
U.log("full-party nick ask:", mashUntil(onNicknameChoice, 600))
U.wait(4)
U.shot(game, DIR .. "/catch_fullparty_nickname.png")
-- decline nickname (NO), then wait (no A/B) for ItemUseBallText07
if topIs(ChoiceBox) then
U.tap(game, "b") -- NO
end
-- drain the B edge before the transfer text can see it
for _ = 1, 8 do U.wait(1) end
local sawBox = false
for _ = 1, 400 do
if game.stack:top() == battle and battle.current and battle.current.text
and battle.current.text:find("transferred", 1, true) then
sawBox = true
-- wait until the typewriter has drawn most of the line
if (battle.charIndex or 0) >= math.floor((battle.total or 0) * 0.7) then
break
end
end
U.wait(1)
end
U.log("full-party box text:", sawBox,
battle.current and battle.current.text or "nil")
U.shot(game, DIR .. "/catch_fullparty_box.png")
for _ = 1, 30 do U.tap(game, "a"); U.wait(4) end
while game.stack:top() ~= ow do game.stack:pop() end
end
+70
View File
@@ -0,0 +1,70 @@
-- Driver: Cerulean TM28 Rocket despawn fade (#170).
-- Skips the fight (EVENT_BEAT set), talks through the TM return, and
-- captures a mid-fade frame while CeruleanHideRocket runs.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
table.insert(game.save.party, Pokemon.new(game.data, "BLASTOISE", 50))
game.save.flags.EVENT_BEAT_CERULEAN_ROCKET_THIEF = true
-- Rocket at (30,8); stand south and face him
U.teleport(game, "CERULEAN_CITY", 30, 9, "up")
local ow = game.overworld
local function mashUntil(cond, label, cap)
for _ = 1, cap or 600 do
if cond() then return true end
if game.stack:top() ~= ow then U.tap(game, "a") end
U.wait(3)
end
U.log("TIMEOUT waiting for " .. label)
return false
end
U.shot(game, DIR .. "/cerulean_rocket_00_before.png")
U.tap(game, "a")
U.wait(20)
U.shot(game, DIR .. "/cerulean_rocket_01_return_tm.png")
local function rocketAlive()
for _, n in ipairs(ow.npcs or {}) do
if n.def and n.def.name == "CERULEANCITY_ROCKET" then return true end
end
return false
end
-- Prefer mid fade-in (rocket already toggled off, screen still dark),
-- matching pokered's post-HideObject GBFadeInFromBlack frames.
local midShot = false
for _ = 1, 900 do
local overlay = ow.fadeOverlay
local alpha = overlay and overlay.alpha or 0
if not midShot and alpha > 0.35 and alpha < 0.95 and not rocketAlive() then
U.shot(game, DIR .. "/cerulean_rocket_02_mid_fade.png")
midShot = true
end
if midShot and not ow.fadeOverlay and game.stack:top() == ow
and not ow.runner:isRunning() then
break
end
if game.stack:top() ~= ow then U.tap(game, "a") end
U.wait(2)
end
U.log("mid-fade shot:", tostring(midShot))
mashUntil(function()
return game.stack:top() == ow and not ow.runner:isRunning()
and not ow.fadeOverlay
end, "idle after hide", 200)
U.wait(10)
U.shot(game, DIR .. "/cerulean_rocket_03_after.png")
local names = {}
for _, n in ipairs(ow.npcs or {}) do
names[#names + 1] = n.def and n.def.name or "?"
end
local toggles = game.save.objectToggles and game.save.objectToggles.CERULEAN_CITY
U.log("rocket alive:", tostring(rocketAlive()))
U.log("rocket toggle:", tostring(toggles and toggles.CERULEANCITY_ROCKET))
U.log("got TM28:", tostring(game.save.flags.EVENT_GOT_TM28))
U.log("npcs:", table.concat(names, ","))
end
+104
View File
@@ -0,0 +1,104 @@
-- Driver: Copycat TM31 MIMIC trade (scripts/CopycatsHouse2F.asm).
-- Teleport to 2F with a POKE DOLL, talk through the mimic dialogue + gift,
-- screenshot the TM receive box, and assert flag/inventory.
--
-- SHOT_DIR=/tmp/copycat_tm31 POKEPORT_DRIVER=tests/drivers/copycat_tm31_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Bag = require("src.inventory.Bag")
local TextBox = require("src.render.TextBox")
Bag.add(game.save, "POKE_DOLL", 1)
U.teleport(game, "COPYCATS_HOUSE_2F", 4, 4, "up")
local ow = game.overworld
for _, npc in ipairs(ow.npcs) do
if npc.def and npc.def.text == "TEXT_COPYCATSHOUSE2F_COPYCAT" then
npc.wanders, npc.moving = false, false
npc.cellX, npc.cellY = 4, 3
npc.px, npc.py = npc.cellX * 16, npc.cellY * 16
npc.facing = "down"
end
end
U.wait(5)
U.shot(game, DIR .. "/copycat_0_room.png")
local function topBox()
local top = game.stack:top()
return getmetatable(top) == TextBox and top or nil
end
local function pageText(box)
if not box then return "" end
return table.concat(box.pages[box.pageIndex] or {}, "\n")
end
-- Advance until a fully-typed page matches pred(pageText), then stop
-- without consuming that page (so the shot catches it). Last pages
-- set done (not waiting); mid-text page breaks set waiting.
local function mashToPage(pred)
for _ = 1, 500 do
local box = topBox()
if box and (box.waiting or box.done) and pred(pageText(box)) then
return true
end
U.tap(game, "a")
U.wait(3)
end
return false
end
U.tap(game, "a")
U.wait(20)
U.log("mimic page:", mashToPage(function(s)
return s:find("like POKéMON", 1, true) ~= nil
or s:find("like POK", 1, true) ~= nil
end))
U.shot(game, DIR .. "/copycat_1_mimic.png")
U.log("pre-receive page:", mashToPage(function(s)
return s:find("DOLL", 1, true) ~= nil
end))
U.shot(game, DIR .. "/copycat_2_prereceive.png")
U.log("TM receive page:", mashToPage(function(s)
return s:find("received", 1, true) ~= nil
and s:find("TM31", 1, true) ~= nil
end))
U.shot(game, DIR .. "/copycat_3_received_tm31.png")
local function mash(btn, cond)
for _ = 1, 400 do
if cond() then return true end
U.tap(game, btn)
U.wait(3)
end
return false
end
U.log("dialogue done:", mash("a", function()
return game.stack:top() == ow
end))
U.shot(game, DIR .. "/copycat_4_done.png")
U.log("EVENT_GOT_TM31:", tostring(game.save.flags.EVENT_GOT_TM31))
U.log("bag TM_MIMIC:", tostring(game.save.inventory.TM_MIMIC),
"POKE_DOLL:", tostring(game.save.inventory.POKE_DOLL))
assert(game.save.flags.EVENT_GOT_TM31, "EVENT_GOT_TM31 not set")
assert((game.save.inventory.TM_MIMIC or 0) >= 1, "TM_MIMIC missing from bag")
assert((game.save.inventory.POKE_DOLL or 0) == 0, "POKE_DOLL not taken")
U.tap(game, "a")
U.wait(20)
U.log("thanks page:", mashToPage(function(s)
return s:find("Thanks for TM31", 1, true) ~= nil
end))
U.wait(2)
U.shot(game, DIR .. "/copycat_5_thanks.png")
mash("a", function() return game.stack:top() == ow end)
U.log("DONE")
love.event.quit()
end
+75
View File
@@ -0,0 +1,75 @@
-- Driver (#184): Magikarp Day Care 13→19 should learn Tackle at 15.
-- Runs the DAYCARE retrieve script (same path as the gentleman NPC),
-- logs the moveset, then screenshots status page 2 (moves + PP).
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local Growth = require("src.pokemon.Growth")
local SummaryMenu = require("src.ui.SummaryMenu")
local story2 = require("data.scripts.story2")
local def = game.data.pokemon.MAGIKARP
local karp = Pokemon.new(game.data, "MAGIKARP", 13)
karp.nickname = "FISHY"
game.save.party = { Pokemon.new(game.data, "RATTATA", 5) }
game.save.money = 10000
game.save.player.name = "RED"
local steps = Growth.expForLevel(def.growthRate, 19)
- Growth.expForLevel(def.growthRate, 13)
game.save.daycare = {
mon = karp, steps = steps, depositLevel = 13,
}
local before = {}
for _, mv in ipairs(karp.moves) do before[#before + 1] = mv.id end
U.log("before: Lv", karp.level, "moves", table.concat(before, ","))
U.teleport(game, "DAYCARE", 2, 2, "up")
-- Resolve dialogue synchronously (parity_daycare style) without stacking
-- fake TextBoxes over the overworld.
local realTextBox = package.loaded["src.render.TextBox"]
package.loaded["src.render.TextBox"] = {
new = function(_, text, onDone, opts)
if opts and opts.choice then
opts.choice(true)
elseif onDone then
onDone()
end
return { text = text }
end,
}
local realPush = game.stack.push
game.stack.push = function() end
local done = false
story2.DAYCARE.talk.TEXT_DAYCARE_GENTLEMAN(
game, game.overworld, nil, function() done = true end)
game.stack.push = realPush
package.loaded["src.render.TextBox"] = realTextBox
local mon = game.save.party[#game.save.party]
local ids = {}
for _, mv in ipairs(mon.moves or {}) do ids[#ids + 1] = mv.id end
U.log("after: Lv", mon.level, "moves", table.concat(ids, ","),
"done=", done, "daycare_cleared=", game.save.daycare == nil)
local hasTackle = false
for _, id in ipairs(ids) do if id == "TACKLE" then hasTackle = true end end
U.log("Magikarp 13→19 learned Tackle:", hasTackle)
local summary = SummaryMenu.new(game, mon)
game.stack:push(summary)
U.wait(4)
U.shot(game, DIR .. "/daycare_00_summary_p1.png")
summary.page = 2
U.wait(2)
U.shot(game, DIR .. "/daycare_01_moves.png")
U.log("shots under", DIR)
if not (mon.species == "MAGIKARP" and mon.level == 19 and hasTackle) then
error("daycare #184 failed: Lv" .. tostring(mon.level)
.. " [" .. table.concat(ids, ",") .. "]")
end
end
+97
View File
@@ -0,0 +1,97 @@
-- Driver: #163 cont (\\v) must show ▼ and wait for A before scrolling.
-- Talks to VIRIDIANFOREST_YOUNGSTER5 (cont mid-page) and the nearby
-- TRAINER TIPS1 sign (para then conts). Screenshots the ▼ wait frames.
--
-- SHOT_DIR=/tmp/dialogue_cont POKEPORT_DRIVER=tests/drivers/dialogue_cont_wait_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/dialogue_cont"
local TextBox = require("src.render.TextBox")
local pass = true
local function check(cond, msg)
if cond then U.log("PASS: " .. msg) else pass = false; U.log("FAIL: " .. msg) end
end
local function topIs(mt) return getmetatable(game.stack:top()) == mt end
local function waitForTextBox(frames)
for _ = 1, frames or 60 do
if topIs(TextBox) then return game.stack:top() end
U.wait(1)
end
return nil
end
-- type out until waiting/done without mashing A (held A would only
-- speed the typewriter; wasPressed edges are what clear waits)
local function waitUntilPrompt(box, frames)
for _ = 1, frames or 240 do
if not topIs(TextBox) then return false end
if box.waiting or box.done then return true end
U.wait(1)
end
return box.waiting or box.done
end
-- ▼ blinks half the time (blink < 30); settle so the shot shows it.
-- Keep POKEPORT_SPEED=1: scriptedIterations>1 can advance past the
-- wait in the same rendered frame before love.draw captures.
local function shotPrompt(box, path)
for _ = 1, 60 do
if not (box.waiting or box.done) then break end
if box.blink < 20 then break end
U.wait(1)
end
U.shot(game, path)
U.wait(2)
end
local function dismissText()
for _ = 1, 80 do
if not topIs(TextBox) then return end
U.tap(game, "a")
U.wait(2)
end
end
-- Youngster5 @ (27,40): "I ran out of POKé / BALLs to catch" then
-- \v "POKéMON with!" — the bug auto-scrolled past this with no ▼.
U.teleport(game, "VIRIDIAN_FOREST", 27, 41, "up")
U.tap(game, "a")
local box = waitForTextBox(90)
check(box ~= nil, "youngster5 opened a TextBox")
if box then
check(waitUntilPrompt(box, 300), "youngster5 reached a prompt")
check(box.waiting and box.contAdvance,
"youngster5 first prompt is a cont wait (▼)")
shotPrompt(box, DIR .. "/dialogue_cont_00_youngster5_arrow.png")
U.tap(game, "a")
U.wait(3)
check(waitUntilPrompt(box, 300), "youngster5 reached para/done after cont")
shotPrompt(box, DIR .. "/dialogue_cont_01_youngster5_after.png")
dismissText()
end
-- Trainer Tips1 sign @ (24,40): para after title, then two conts.
U.teleport(game, "VIRIDIAN_FOREST", 24, 41, "up")
U.tap(game, "a")
box = waitForTextBox(90)
check(box ~= nil, "tips1 sign opened a TextBox")
if box then
check(waitUntilPrompt(box, 300), "tips1 reached first prompt")
-- first prompt is the para after "TRAINER TIPS"
check(box.waiting and not box.contAdvance,
"tips1 first prompt is a para wait")
shotPrompt(box, DIR .. "/dialogue_cont_02_tips1_para.png")
U.tap(game, "a")
U.wait(3)
check(waitUntilPrompt(box, 300), "tips1 typed into page 2")
check(box.waiting and box.contAdvance,
"tips1 page2 pauses on cont with ▼")
shotPrompt(box, DIR .. "/dialogue_cont_03_tips1_cont_arrow.png")
dismissText()
end
U.log(pass and "RESULT: ALL PASS" or "RESULT: SEE FAILURES ABOVE")
end
+18 -2
View File
@@ -1,5 +1,5 @@
-- Driver: reproduce the door-warp flow. Teleports to Pallet Town in
-- front of Red's house, walks in, tries to move inside, walks back out.
-- Driver: door-warp flow (Pallet house) plus #187 S.S. Anne enter-from-above.
-- Anne-only shots: tests/drivers/anne_door_test.lua
return function(game)
local U = dofile("tests/drivers/util.lua")
@@ -32,4 +32,20 @@ return function(game)
U.shot(game, DIR .. "/door_9_back_outside.png")
U.log("final map:", ow.map.id, "pos:", ow.player.cellX, ow.player.cellY,
"transitioning:", tostring(ow.transitioning))
-- #187: enter SS Anne cabin from above; expect one tile south of door
U.teleport(game, "SS_ANNE_1F", 31, 9, "up")
U.shot(game, DIR .. "/door_10_anne_before.png")
U.hold(game, "up", 20)
for _ = 1, 90 do
U.wait(1)
if ow.map.id == "SS_ANNE_1F_ROOMS" and not ow.transitioning
and #ow.scriptMoves == 0 and not ow.player.moving then
break
end
end
U.wait(8)
U.shot(game, DIR .. "/door_11_anne_after.png")
U.log("anne map:", ow.map.id, "pos:", ow.player.cellX, ow.player.cellY,
"facing:", ow.player.facing)
end
+93
View File
@@ -0,0 +1,93 @@
-- Driver: gym-leader post-battle dialogue chain (#164).
-- Invokes checkVictoryRewards for Brock then Misty (same path as a win)
-- and screenshots pages that must include badge-effect + TM explanation
-- text — not just a synthetic "received badge/TM" stub.
--
-- SHOT_DIR=/tmp/gym164 POKEPORT_DRIVER=tests/drivers/gym_leader_victory_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local TextBox = require("src.render.TextBox")
local OW = require("src.world.OverworldController")
local function currentPageText()
local top = game.stack:top()
if getmetatable(top) ~= TextBox then return "" end
local page = top.pages and top.pages[top.pageIndex]
if not page then return "" end
return table.concat(page, "\n")
end
local function mashUntil(cond)
for _ = 1, 1200 do
if cond() then return true end
U.tap(game, "a")
U.wait(2)
end
return false
end
local function advancePages(want, shotName)
U.log(shotName, "waiting for:", want)
local ok = mashUntil(function()
local top = game.stack:top()
if getmetatable(top) ~= TextBox then return false end
-- only match once the current page has finished typing
if not (top.waiting or top.done) then return false end
return currentPageText():find(want, 1, true) ~= nil
end)
U.log(shotName, "found:", ok, "page:", currentPageText():gsub("\n", "\\n"))
U.shot(game, DIR .. "/" .. shotName .. ".png")
assert(ok, shotName .. " missing dialogue containing: " .. want)
end
local function runLeader(mapId, x, y, class, party, shots)
while game.stack:top() do game.stack:pop() end
game.save.flags = game.save.flags or {}
game.save.inventory = game.save.inventory or {}
game.save.defeatedTrainers = game.save.defeatedTrainers or {}
game.stack:push(OW, mapId, x, y, "up")
U.wait(5)
local ow = game.stack:top()
U.shot(game, DIR .. "/" .. shots.prefix .. "_0_gym.png")
ow:checkVictoryRewards(class, party)
U.wait(10)
for _, s in ipairs(shots.pages) do
advancePages(s.want, shots.prefix .. "_" .. s.name)
end
U.log(shots.prefix, "closing box:", mashUntil(function()
return game.stack:top() == ow
end))
U.shot(game, DIR .. "/" .. shots.prefix .. "_done.png")
end
game.save.player.name = game.save.player.name or "RED"
runLeader("PEWTER_GYM", 4, 3, "OPP_BROCK", 1, {
prefix = "brock",
pages = {
{ want = "BOULDERBADGE", name = "1_badge" },
{ want = "FLASH", name = "2_flash" },
{ want = "Wait!", name = "3_wait" },
{ want = "TM34", name = "4_tm34" },
{ want = "BIDE", name = "5_bide" },
},
})
assert(game.save.flags.EVENT_BEAT_BROCK, "EVENT_BEAT_BROCK")
assert(game.save.inventory.BOULDERBADGE, "BOULDERBADGE")
assert((game.save.inventory.TM_BIDE or 0) >= 1, "TM_BIDE")
runLeader("CERULEAN_GYM", 5, 5, "OPP_MISTY", 1, {
prefix = "misty",
pages = {
{ want = "CASCADEBADGE", name = "1_badge" },
{ want = "CUT", name = "2_cut" },
{ want = "TM11", name = "3_tm11" },
},
})
assert(game.save.flags.EVENT_BEAT_MISTY, "EVENT_BEAT_MISTY")
assert(game.save.inventory.CASCADEBADGE, "CASCADEBADGE")
assert((game.save.inventory.TM_BUBBLEBEAM or 0) >= 1, "TM_BUBBLEBEAM")
U.log("gym_leader_victory_test: ok")
end
+71 -8
View File
@@ -1,10 +1,11 @@
-- Driver: Pokémon Center nurse heal, welcome/choice dialogue, the
-- machine monitor + per-mon balls, the jingle flash, and the farewell.
-- Driver: Pokémon Center nurse heal + Mom home heal.
-- Logs frames between healAnim end and the fighting-fit / looking-great text.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local ChoiceBox = require("src.ui.ChoiceBox")
local TextBox = require("src.render.TextBox")
local mon = Pokemon.new(game.data, "CHARMANDER", 12)
mon.hp = 3
table.insert(game.save.party, mon)
@@ -14,7 +15,6 @@ return function(game)
U.teleport(game, "VIRIDIAN_POKECENTER", 3, 3, "up")
local ow = game.overworld
-- mash A until a condition holds
local function mashUntil(cond)
for _ = 1, 400 do
if cond() then return true end
@@ -24,6 +24,11 @@ return function(game)
return false
end
local function topIsText()
return getmetatable(game.stack:top()) == TextBox
end
-- -------- Poké Center --------
U.tap(game, "a") -- talk to the nurse
U.wait(30)
U.shot(game, DIR .. "/heal_00_welcome.png")
@@ -38,7 +43,6 @@ return function(game)
U.shot(game, DIR .. "/heal_02_ball1.png")
U.wait(28)
U.shot(game, DIR .. "/heal_03_ball2.png")
-- overlay must stay glued to the machine under survey zoom
game:zoomStep(-1); game:zoomStep(-1)
U.wait(3)
U.shot(game, DIR .. "/heal_03z_zoomed.png")
@@ -51,14 +55,73 @@ return function(game)
U.shot(game, DIR .. "/heal_05_flash_b.png")
U.wait(10)
U.shot(game, DIR .. "/heal_06_flash_c.png")
-- wait out the jingle + 32-frame beat
for _ = 1, 40 do
if not ow.healAnim then break end
U.wait(10)
local animEndFrame, fitFrame
for _ = 1, 400 do
if ow.healAnim == nil and not animEndFrame then
animEndFrame = U.frame()
if topIsText() then fitFrame = animEndFrame end
U.shot(game, DIR .. "/heal_06b_anim_end.png")
if fitFrame then break end
end
if animEndFrame and topIsText() and not fitFrame then
fitFrame = U.frame()
break
end
U.wait(1)
end
U.shot(game, DIR .. "/heal_07_fit.png")
U.log("pc anim_end_frame:", animEndFrame, "fit_frame:", fitFrame,
"gap_anim_to_text:",
(fitFrame and animEndFrame) and (fitFrame - animEndFrame) or "n/a")
U.log("farewell reached:", mashUntil(function()
return game.stack:top() == ow
end))
U.shot(game, DIR .. "/heal_08_end.png")
-- -------- Mom (Reds House) --------
require("src.script.Flags").set(game.save, "EVENT_GOT_STARTER")
for _, m in ipairs(game.save.party) do
m.hp = 1
end
-- stand below Mom (5,4) facing up
U.teleport(game, "REDS_HOUSE_1F", 5, 5, "up")
ow = game.overworld
U.tap(game, "a")
U.wait(20)
U.shot(game, DIR .. "/heal_mom_00_rest.png")
U.log("mom fade started:", mashUntil(function()
return ow.fadeOverlay ~= nil
end))
local fadeStart = U.frame()
U.shot(game, DIR .. "/heal_mom_01_fade.png")
local Music = require("src.core.Music")
local sawJingle, jingleDoneFrame, fadeGone, greatFrame = false
for _ = 1, 600 do
if Music.oneShotPlaying() then sawJingle = true end
if sawJingle and not Music.oneShotPlaying() and not jingleDoneFrame then
jingleDoneFrame = U.frame()
end
if ow.fadeOverlay == nil and fadeStart and not fadeGone
and U.frame() > fadeStart + 5 then
fadeGone = U.frame()
end
if fadeGone and topIsText() then
greatFrame = U.frame()
break
end
U.wait(1)
end
U.shot(game, DIR .. "/heal_mom_02_great.png")
U.log("mom saw_jingle:", sawJingle,
"jingle_done_frame:", jingleDoneFrame,
"fade_gone_frame:", fadeGone,
"great_frame:", greatFrame,
"gap_fade_to_text:",
(greatFrame and fadeGone) and (greatFrame - fadeGone) or "n/a")
mashUntil(function()
return game.stack:top() == ow
end)
U.shot(game, DIR .. "/heal_mom_03_end.png")
end
+162
View File
@@ -0,0 +1,162 @@
-- Driver: level-up learn with a full moveset (#173).
-- Injects the post-KO level-up queue (grew + StatBox + learnMove) inside
-- a live wild battle so MoveLearnMenu runs the same path as a real win.
--
-- SHOT_DIR=/tmp/learn173 POKEPORT_DRIVER=tests/drivers/learn_move_full_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/learn173"
os.execute("mkdir -p " .. DIR)
local Pokemon = require("src.pokemon.Pokemon")
local mon = Pokemon.new(game.data, "CHARMANDER", 14)
mon.moves = {
{ id = "SCRATCH", pp = 35 },
{ id = "GROWL", pp = 40 },
{ id = "EMBER", pp = 25 },
{ id = "SMOKESCREEN", pp = 20 },
}
table.insert(game.save.party, 1, mon)
game.save.options = game.save.options or {}
game.save.options.textSpeed = 1
U.teleport(game, "ROUTE_1", 5, 5, "down")
local ow = game.overworld
local BattleState = require("src.battle.BattleState")
local battle = BattleState.newWild(game, "RATTATA", 2)
battle.onFinish = function() end
ow:pushBattle(battle)
local function top() return game.stack:top() end
local function stackIds()
local parts = {}
for _, st in ipairs(game.stack.states or {}) do
local id = st.screenId or (st.pages and "TextBox")
or (st.onChoose and "ChoiceBox")
or (st.mon and st.stats and "StatBox")
or (st.newMoveId and "MoveLearnMenu")
or (st.phase and ("Battle:" .. tostring(st.phase)))
or "?"
if st.selecting ~= nil then
id = id .. (st.selecting and "[sel]" or "[ask]")
end
if st.pages then
id = id .. string.format("[p%d/%d%s%s]", st.pageIndex or 0, #st.pages,
st.waiting and "W" or "", st.done and "D" or "")
end
parts[#parts + 1] = id
end
return table.concat(parts, ">")
end
local function isText()
local t = top()
return t and t.pages ~= nil
end
local function isChoice()
local t = top()
return t and t.onChoose ~= nil and t.index ~= nil and not t.mon
end
local function learnMenu()
for _, st in ipairs(game.stack.states or {}) do
if st.newMoveId and st.mon and st.index then return st end
end
end
local function waitFor(cond, max)
for i = 1, max or 900 do
if cond() then return true end
U.wait(1)
end
return false
end
local function atPrompt()
local t = top()
return t and t.pages and (t.waiting or t.done)
end
-- #163: \v conts pause with ▼ mid-page; tap through them so the shot
-- lands on the full page (or the done+choice tail)
local function advanceTextPage()
waitFor(atPrompt, 300)
for _ = 1, 4 do
local t = top()
if not (t and t.pages and t.waiting and t.contAdvance) then break end
U.tap(game, "a")
U.wait(2)
waitFor(atPrompt, 300)
end
U.wait(2)
end
waitFor(function() return battle.phase == "menu" end, 400)
local StatBox = BattleState.StatBox
battle.phase = "messages"
battle.afterQueue = "menu"
battle.queue = {}
battle.current = nil
battle.nextInsert = 0
battle:say("CHARMANDER gained\n16 EXP. Points!")
battle:say("CHARMANDER grew\nto level 15!")
battle:ui(function() return StatBox.new(game, mon) end)
mon.level = 15
-- append (not uiNext): learnMove uses uiNext for mid-fn ordering
battle:ui(function()
return battle:buildScreen("MoveLearnMenu", mon, "LEER")
end)
for _ = 1, 400 do
if learnMenu() then break end
U.tap(game, "a")
U.wait(2)
end
if not learnMenu() then error("MoveLearnMenu never opened: " .. stackIds()) end
U.log("opened", stackIds())
-- page 1
advanceTextPage()
U.log("shot0", stackIds())
U.shot(game, DIR .. "/learn_0_trying.png")
U.tap(game, "a"); U.wait(2)
-- page 2
advanceTextPage()
U.log("shot1", stackIds())
U.shot(game, DIR .. "/learn_1_cant_more.png")
U.tap(game, "a"); U.wait(2)
-- page 3 types out (through its cont), then ChoiceBox overlays
advanceTextPage()
if not waitFor(isChoice, 120) then
error("ChoiceBox never appeared: " .. stackIds())
end
U.wait(4) -- let choice settle over Delete prompt
U.log("shot2", stackIds())
U.shot(game, DIR .. "/learn_2_delete_yesno.png")
U.tap(game, "a"); U.wait(4) -- YES
if not waitFor(function()
local m = learnMenu()
return m and m.selecting and top() == m
end, 120) then
error("forget list never active: " .. stackIds())
end
U.log("shot3", stackIds())
U.shot(game, DIR .. "/learn_3_which_move.png")
U.tap(game, "a"); U.wait(4) -- forget SCRATCH
local n = 4
for _ = 1, 60 do
if not isText() then break end
local t = top()
if t.done or t.waiting then
U.log("shot" .. n, stackIds())
U.shot(game, ("%s/learn_%d_after.png"):format(DIR, n))
n = n + 1
U.tap(game, "a")
U.wait(4)
else
U.wait(1)
end
end
U.log("done", DIR, "moves[1]=", mon.moves[1] and mon.moves[1].id, stackIds())
love.event.quit()
end
+112
View File
@@ -0,0 +1,112 @@
-- Driver: Mt Moon B2F fossil pick (scripts/MtMoonB2F.asm). After the
-- Super Nerd is beaten, take one fossil and confirm he walks to the other
-- and says "All right. Then this is mine!" before it vanishes.
-- Runs DOME then HELIX paths (map is reset between).
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
if #game.save.party == 0 then
table.insert(game.save.party, Pokemon.new(game.data, "PIKACHU", 20))
end
local function runPick(tag, x, y, takeName, leaveName, itemId, expectNerdX, expectNerdY, gotFlag)
game.save.flags = game.save.flags or {}
game.save.flags.EVENT_BEAT_MT_MOON_3_SUPER_NERD = true
game.save.flags.EVENT_GOT_DOME_FOSSIL = nil
game.save.flags.EVENT_GOT_HELIX_FOSSIL = nil
game.save.inventory.DOME_FOSSIL = nil
game.save.inventory.HELIX_FOSSIL = nil
game.save.objectToggles = game.save.objectToggles or {}
game.save.objectToggles.MT_MOON_B2F = nil
U.teleport(game, "MT_MOON_B2F", x, y, "up")
local ow = game.overworld
U.wait(5)
local function idle()
return game.stack:top() == ow and not ow.runner:isRunning()
and #ow.scriptMoves == 0
end
local function mash(cond, label, cap)
for _ = 1, cap or 400 do
if cond() then return true end
U.tap(game, "a")
U.wait(3)
end
U.log("TIMEOUT:", tag, label)
return false
end
local function fossilVisible(name)
for _, n in ipairs(ow.npcs) do
if n.def and n.def.name == name then return true end
end
return false
end
local function nerd()
return ow:npcByIndex(1)
end
U.shot(game, DIR .. ("/mtmoon_%s_0_before.png"):format(tag))
U.log(tag, "before", takeName .. ":", tostring(fossilVisible(takeName)),
leaveName .. ":", tostring(fossilVisible(leaveName)),
"nerd:", nerd() and (nerd().cellX .. "," .. nerd().cellY) or "nil")
U.tap(game, "a")
U.wait(24)
U.shot(game, DIR .. ("/mtmoon_%s_1_ask.png"):format(tag))
U.tap(game, "a") -- YES
mash(function()
return game.stack:top() == ow or ow.runner:isRunning()
end, "received / walk start", 200)
local sawMineText, midwalk = false, false
for _ = 1, 500 do
local n = nerd()
if not midwalk and n and (n.cellX ~= 12 or n.cellY ~= 8
or n.moving or #ow.scriptMoves > 0) then
if n.cellX ~= 12 or n.cellY ~= 8 or n.moving then
midwalk = true
U.shot(game, DIR .. ("/mtmoon_%s_2_walk.png"):format(tag))
end
end
local top = game.stack:top()
if top and top ~= ow and top.pages and top.done then
local flat = ""
for _, page in ipairs(top.pages) do
for _, line in ipairs(page) do flat = flat .. line .. " " end
end
if flat:find("this is mine", 1, true) then
sawMineText = true
U.shot(game, DIR .. ("/mtmoon_%s_3_mine.png"):format(tag))
break
end
end
U.wait(1)
end
mash(idle, "cutscene idle", 400)
U.shot(game, DIR .. ("/mtmoon_%s_4_done.png"):format(tag))
local n = nerd()
U.log(tag, "sawMineText:", tostring(sawMineText),
"midwalk:", tostring(midwalk))
U.log(tag, "after", takeName .. ":", tostring(fossilVisible(takeName)),
leaveName .. ":", tostring(fossilVisible(leaveName)))
U.log(tag, "nerd at:", n and (n.cellX .. "," .. n.cellY) or "nil",
"expect:", expectNerdX .. "," .. expectNerdY)
U.log(tag, "flag:", gotFlag, tostring(game.save.flags[gotFlag]),
"bag:", tostring(game.save.inventory[itemId]))
end
runPick("dome", 12, 7,
"MTMOONB2F_DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL", "DOME_FOSSIL",
13, 7, "EVENT_GOT_DOME_FOSSIL")
runPick("helix", 13, 7,
"MTMOONB2F_HELIX_FOSSIL", "MTMOONB2F_DOME_FOSSIL", "HELIX_FOSSIL",
12, 7, "EVENT_GOT_HELIX_FOSSIL")
U.log("DONE")
love.event.quit()
end
+65 -11
View File
@@ -1,6 +1,5 @@
-- Driver: the full Pallet intro chain. Steps north to trigger Oak,
-- follows him to the lab, takes a starter, watches the rival
-- counter-pick, tries to leave (door gate + ambush battle).
-- Driver: Pallet intro (Oak escort → starter → rival ambush) plus the
-- #158 parcel-return / rival-join / Pokédex-gift cutscene.
return function(game)
local U = dofile("tests/drivers/util.lua")
@@ -96,13 +95,68 @@ return function(game)
U.log("final:", ow.map.id, ow.player.cellX, ow.player.cellY,
"battled:", tostring(game.save.flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB),
"battleSeen:", tostring(BattleSeen))
U.log("stuck-state: runner=", tostring(ow.runner:isRunning()),
"moves=", #ow.scriptMoves, "top=", tostring(game.stack:top() == ow),
"transitioning=", tostring(ow.transitioning),
"emote=", tostring(ow.emote ~= nil))
for i, mv in ipairs(ow.scriptMoves) do
U.log((" move %d: entity=%s dir=%s remaining=%d moving=%s"):format(
i, tostring(mv.entity.def and mv.entity.def.sprite or "PLAYER"),
tostring(mv.dir), mv.remaining, tostring(mv.entity.moving)))
-- ---- #158: return Oak's Parcel → rival joins → Pokédex gift ----
local Flags = require("src.script.Flags")
Flags.set(game.save, "EVENT_GOT_OAKS_PARCEL")
game.save.inventory.OAKS_PARCEL = 1
-- desk Oak + both dexes visible; rival hidden until the script shows
-- him. Keep the starter-ball hides from the pick (Squirtle + Bulbasaur).
game.save.objectToggles = game.save.objectToggles or {}
game.save.objectToggles.OAKS_LAB = {
OAKSLAB_OAK1 = true,
OAKSLAB_OAK2 = false,
OAKSLAB_RIVAL = false,
OAKSLAB_POKEDEX1 = true,
OAKSLAB_POKEDEX2 = true,
OAKSLAB_SQUIRTLE_POKE_BALL = false,
OAKSLAB_BULBASAUR_POKE_BALL = false,
}
U.teleport(game, "OAKS_LAB", 5, 3, "up")
ow = game.overworld
U.wait(20)
U.shot(game, DIR .. "/oak_12_parcel_ready.png")
U.tap(game, "a")
U.wait(40)
U.shot(game, DIR .. "/oak_13_deliver.png")
local rivalSeen, atDeskShot, leaveShot = false, false, false
for _ = 1, 1200 do
if idle() and Flags.get(game.save, "EVENT_GOT_POKEDEX")
and not ow:npcByIndex(1) then
break
end
local rival = ow:npcByIndex(1)
local top = game.stack:top()
if rival and not rivalSeen then
rivalSeen = true
U.wait(24)
U.shot(game, DIR .. "/oak_14_rival_arrive.png")
end
if rival and rival.cellY <= 3 and not atDeskShot then
atDeskShot = true
U.shot(game, DIR .. "/oak_14b_rival_at_desk.png")
end
if rival and rival.cellY >= 6 and Flags.get(game.save, "EVENT_GOT_POKEDEX")
and not leaveShot then
leaveShot = true
U.shot(game, DIR .. "/oak_14c_rival_leave.png")
end
if top ~= ow then
U.tap(game, "a")
end
U.wait(4)
end
mashUntil(idle, "pokedex cutscene done", 600)
U.shot(game, DIR .. "/oak_15_after_pokedex.png")
local toggles = game.save.objectToggles and game.save.objectToggles.OAKS_LAB
local rivalAfter = ow:npcByIndex(1)
U.log("parcel:",
"gotDex=", tostring(Flags.get(game.save, "EVENT_GOT_POKEDEX")),
"oakGot=", tostring(Flags.get(game.save, "EVENT_OAK_GOT_PARCEL")),
"rivalSeen=", tostring(rivalSeen),
"rivalGone=", tostring(rivalAfter == nil),
"atDesk=", tostring(atDeskShot),
"dex1=", tostring(toggles and toggles.OAKSLAB_POKEDEX1),
"dex2=", tostring(toggles and toggles.OAKSLAB_POKEDEX2),
"route22=", tostring(Flags.get(game.save, "EVENT_ROUTE22_RIVAL_WANTS_BATTLE")))
end
+87
View File
@@ -0,0 +1,87 @@
-- Driver: Player's PC deposit "How many?" with >7 bag stacks (#174).
-- pokered PrintListMenuEntries shows 4 names; PrintText + quantity box
-- must not overlap the list.
-- SHOT_DIR=/tmp/pc_deposit POKEPORT_DRIVER=tests/drivers/pc_deposit_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local PlayerPC = require("src.ui.PlayerPC")
local ListMenu = require("src.ui.ListMenu")
local QuantityBox = require("src.ui.QuantityBox")
local Bag = require("src.inventory.Bag")
local pass = true
local function check(cond, msg)
if cond then U.log("PASS: " .. msg) else pass = false; U.log("FAIL: " .. msg) end
end
local function topIs(mt) return getmetatable(game.stack:top()) == mt end
U.teleport(game, "VIRIDIAN_POKECENTER", 13, 4, "up")
game.save.pcItems = {}
-- >7 distinct depositable stacks (bug reproduces only then)
local seed = {
"POTION", "SUPER_POTION", "ANTIDOTE", "AWAKENING", "BURN_HEAL",
"ICE_HEAL", "PARLYZ_HEAL", "FULL_HEAL", "REPEL", "POKE_BALL",
}
for _, id in ipairs(seed) do Bag.add(game.save, id, 5) end
check(#seed > 7, "seeded more than 7 bag stacks")
game.stack:push(PlayerPC.new(game))
U.wait(4)
U.tap(game, "down"); U.wait(1) -- DEPOSIT ITEM
U.tap(game, "a"); U.wait(4)
check(topIs(ListMenu), "DEPOSIT ITEM list opened")
local list = game.stack:top()
check(list.rows == 4, "deposit list uses 4 rows like PrintListMenuEntries")
check(list.messageBox == true, "deposit list uses messageBox footer")
check(#list.items > 7, "list has more than 7 depositable items")
U.shot(game, DIR .. "/pc_deposit_0_list.png")
-- pick first non-key stack (seed items are all quantity-prompted)
U.tap(game, "a"); U.wait(4)
check(topIs(QuantityBox), "quantity box on stack")
check(list.footer == "How many?", "How many? footer set")
U.shot(game, DIR .. "/pc_deposit_1_how_many.png")
-- layout probe: list rows stay above the text box (ty=12 → y=96)
local drawn = {}
local Font = require("src.render.Font")
local savedDraw, savedBox = Font.draw, Font.drawBox
Font.draw = function(text, x, y)
drawn[#drawn + 1] = { kind = "text", text = tostring(text), x = x, y = y }
return savedDraw(text, x, y)
end
Font.drawBox = function(tx, ty, tw, th)
drawn[#drawn + 1] = { kind = "box", tx = tx, ty = ty, tw = tw, th = th }
return savedBox(tx, ty, tw, th)
end
game.stack:draw()
Font.draw, Font.drawBox = savedDraw, savedBox
local howY, qtyBox, textBox, maxItemY
for _, d in ipairs(drawn) do
if d.kind == "text" and d.text == "How many?" then howY = d.y end
if d.kind == "box" and d.ty == 9 and d.tw == 5 then qtyBox = d end
if d.kind == "box" and d.tx == 0 and d.ty == 12 and d.tw == 20 then
textBox = d
end
if d.kind == "text" and tostring(d.text):match("^x%d") and d.y then
if not maxItemY or d.y > maxItemY then maxItemY = d.y end
end
end
check(howY == 112, "How many? sits on text-box first line (y=112)")
check(textBox ~= nil, "bottom PrintText box drawn")
check(qtyBox ~= nil, "quantity box at pokered row 9")
check(maxItemY and maxItemY <= 72, "last visible item row above qty/text (y<=72)")
if maxItemY and howY then
check(maxItemY + 8 <= howY, "item glyphs do not overlap How many?")
end
U.tap(game, "a"); U.wait(6) -- confirm x01
U.shot(game, DIR .. "/pc_deposit_2_stored.png")
check(type(list.footer) == "string" and list.footer:find("stored", 1, true),
"stored-via-PC footer after deposit")
U.log(pass and "RESULT: ALL PASS" or "RESULT: SEE FAILURES ABOVE")
love.event.quit(pass and 0 or 1)
end
+37
View File
@@ -0,0 +1,37 @@
-- Driver: Bill's PC vs player's PC top menus (#176). Both should open
-- on the left (pokered BillsPCMenu / PlayersPCMenu TextBoxBorder at 0,0).
-- SHOT_DIR=/tmp/pc_sides POKEPORT_DRIVER=tests/drivers/pc_menu_sides_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local BoxMenu = require("src.ui.BoxMenu")
local PlayerPC = require("src.ui.PlayerPC")
U.teleport(game, "VIRIDIAN_POKECENTER", 13, 4, "up")
game.save.flags = game.save.flags or {}
game.save.flags.EVENT_MET_BILL = true
game.stack:push(BoxMenu.new(game))
U.wait(5)
local bills = game.stack:top()
U.log(("bills tx=%d ty=%d tw=%d th=%d"):format(
bills.tx, bills.ty, bills.tw, bills.th))
U.shot(game, DIR .. "/pc_0_bills.png")
U.wait(8) -- let async captureScreenshot flush before pop
game.stack:pop()
U.wait(2)
game.stack:push(PlayerPC.new(game))
U.wait(5)
local player = game.stack:top()
U.log(("player tx=%d ty=%d tw=%d th=%d"):format(
player.tx, player.ty, player.tw, player.th))
U.shot(game, DIR .. "/pc_1_player.png")
U.wait(8)
local sameSide = player.tx == bills.tx and player.ty == bills.ty
U.log(sameSide and "PASS: both menus share top-left origin"
or "FAIL: menus not on the same side")
U.log("DONE")
love.event.quit()
end
+74
View File
@@ -0,0 +1,74 @@
-- Driver: Bill's PC RELEASE two mons in one list session (#171).
-- SHOT_DIR=/tmp/pc_release POKEPORT_DRIVER=tests/drivers/pc_release_two_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local Boxes = require("src.pokemon.Boxes")
local BoxMenu = require("src.ui.BoxMenu")
local ListMenu = require("src.ui.ListMenu")
local ChoiceBox = require("src.ui.ChoiceBox")
local TextBox = require("src.render.TextBox")
local pass = true
local function check(cond, msg)
if cond then U.log("PASS: " .. msg) else pass = false; U.log("FAIL: " .. msg) end
end
local function topIs(mt) return getmetatable(game.stack:top()) == mt end
local function mash(btn, cond, n)
for _ = 1, (n or 240) do
if cond() then return true end
U.tap(game, btn)
U.wait(2)
end
return false
end
U.teleport(game, "VIRIDIAN_POKECENTER", 13, 4, "up")
game.save.flags = game.save.flags or {}
game.save.flags.EVENT_MET_BILL = true
game.save.options = game.save.options or {}
game.save.options.textSpeed = 1
local box = Boxes.active(game.save)
for i = #box, 1, -1 do box[i] = nil end
box[1] = Pokemon.new(game.data, "RATTATA", 5)
box[2] = Pokemon.new(game.data, "PIDGEY", 6)
box[3] = Pokemon.new(game.data, "CATERPIE", 4)
check(#box == 3, "seeded 3 boxed mons")
game.stack:push(BoxMenu.new(game))
U.wait(4)
U.tap(game, "down"); U.wait(1)
U.tap(game, "down"); U.wait(1)
U.tap(game, "a") -- RELEASE
check(mash("a", function() return topIs(ListMenu) end, 20), "RELEASE list opened")
U.shot(game, DIR .. "/pc_release_0_list.png")
local function releaseOne(label)
check(topIs(ListMenu), label .. ": still on release list")
local before = #Boxes.active(game.save)
U.tap(game, "a") -- pick current row
check(mash("a", function() return topIs(ChoiceBox) end), label .. ": confirm choice")
U.shot(game, DIR .. "/pc_release_" .. label .. "_confirm.png")
U.tap(game, "up"); U.wait(1) -- defaultNo -> YES
U.tap(game, "a")
check(mash("a", function()
return topIs(ListMenu) or (#game.stack.states > 0 and topIs(TextBox) == false)
end), label .. ": bye text advancing")
check(mash("a", function() return topIs(ListMenu) end), label .. ": back on list")
local after = #Boxes.active(game.save)
check(after == before - 1, label .. ": box count " .. before .. " -> " .. after)
U.shot(game, DIR .. "/pc_release_" .. label .. "_list.png")
end
releaseOne("1st")
releaseOne("2nd")
check(#Boxes.active(game.save) == 1, "one mon remains after two releases")
check(topIs(ListMenu), "still in RELEASE list without re-entering")
U.shot(game, DIR .. "/pc_release_done.png")
U.log(pass and "RESULT: ALL PASS" or "RESULT: SEE FAILURES ABOVE")
love.event.quit(pass and 0 or 1)
end
+203
View File
@@ -0,0 +1,203 @@
-- Driver: #154 Route 22 rival exit direction + #168 Cerulean Bill text
-- and southbound walk-off.
--
-- SHOT_DIR=/tmp/rival_walkoff POKEPORT_SPEED=8 \
-- POKEPORT_DRIVER=tests/drivers/rival_walkoff_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/rival_walkoff"
os.execute("mkdir -p " .. DIR)
local Pokemon = require("src.pokemon.Pokemon")
local Flags = require("src.script.Flags")
local Zoom = require("src.render.Zoom")
if game.save.options then game.save.options.zoom = 0 end
Zoom.reset()
local function tank()
local mon = Pokemon.new(game.data, "MEWTWO", 100)
mon.moves = {
{ id = "PSYCHIC_M", pp = 99 },
{ id = "THUNDERBOLT", pp = 99 },
{ id = "ICE_BEAM", pp = 99 },
{ id = "RECOVER", pp = 99 },
}
return mon
end
game.save.party = { tank() }
game.save.player = game.save.player or {}
game.save.player.name = "RED"
game.save.player.rival = "BLUE"
Flags.set(game.save, "EVENT_CHOSE_SQUIRTLE")
local function idle(ow)
return game.stack:top() == ow and not ow.runner:isRunning()
and #ow.scriptMoves == 0 and not ow.transitioning
end
local function mashUntil(cond, label, cap)
for _ = 1, cap or 800 do
if cond() then return true end
if game.stack:top() ~= game.overworld then U.tap(game, "a") end
U.wait(2)
end
U.log("TIMEOUT waiting for " .. label)
return false
end
local function rivalNpc()
local ow = game.overworld
for _, npc in ipairs(ow.npcs or {}) do
if npc.def and npc.def.name and npc.def.name:find("RIVAL") then
return npc
end
end
end
-- Fight until the battle state leaves the stack. Do NOT mash the
-- post-battle script text -- the caller screenshots that.
local function winBattle(label)
local ow = game.overworld
local sawBattle = false
for frames = 1, 5000 do
local top = game.stack:top()
if top and top.phase then
sawBattle = true
if top.phase == "menu" then
top.menuIndex = 1
elseif top.phase == "moveSelect" then
top.moveIndex = 1
end
U.tap(game, "a")
if frames > 2400 and top.onFinish then
U.log("force-finishing stalled", label)
top.onFinish("win")
if game.stack:top() == top then game.stack:pop() end
end
elseif sawBattle then
-- battle popped: either a TextBox or the overworld runner
return true
elseif top ~= ow then
U.tap(game, "a") -- pre-battle dialogue
end
U.wait(2)
end
U.log("TIMEOUT winBattle", label)
return false
end
-- ---------------------------------------------------------- #154
-- Route 22 optional post-lab rival: after the fight he must walk
-- toward Viridian (right / down), not back toward the League.
Flags.set(game.save, "EVENT_GOT_POKEDEX")
Flags.clear(game.save, "EVENT_BEAT_BROCK")
Flags.clear(game.save, "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE")
Flags.clear(game.save, "EVENT_BEAT_GIOVANNI")
Flags.clear(game.save, "EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE")
U.teleport(game, "ROUTE_22", 30, 5, "left")
local ow = game.overworld
U.shot(game, DIR .. "/r22_0_before.png")
U.hold(game, "left", 20)
mashUntil(function()
return ow.runner:isRunning() or game.stack:top() ~= ow
end, "route22 ambush", 200)
U.shot(game, DIR .. "/r22_1_ambush.png")
mashUntil(function() return game.stack:top() ~= ow end, "route22 battle", 800)
winBattle("route22")
local sawExit, exitFacing, exitX, exitY = false, nil, nil, nil
for _ = 1, 600 do
if game.stack:top() ~= ow then U.tap(game, "a") end
local r = rivalNpc()
if r and game.save.flags.EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE then
if (r.facing == "right" or r.facing == "down" or r.cellX > 28)
and not sawExit then
sawExit = true
exitFacing, exitX, exitY = r.facing, r.cellX, r.cellY
U.shot(game, DIR .. "/r22_2_exit.png")
U.log("r22 exit:", exitFacing, exitX, exitY)
elseif sawExit and (r.facing == "down" or r.cellY > 5) then
U.shot(game, DIR .. "/r22_3_exit_mid.png")
U.log("r22 exit mid:", r.facing, r.cellX, r.cellY)
break
end
end
if idle(ow) and game.save.flags.EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE then
break
end
U.wait(2)
end
mashUntil(function() return idle(ow) end, "route22 done", 800)
U.shot(game, DIR .. "/r22_4_done.png")
U.log("r22 beat:", tostring(game.save.flags.EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE),
"exitFacing:", tostring(exitFacing))
-- ---------------------------------------------------------- #168
-- Nugget Bridge: Bill dialogue, then walk into Cerulean (south).
Flags.clear(game.save, "EVENT_BEAT_CERULEAN_RIVAL")
game.save.party = { tank() }
U.teleport(game, "CERULEAN_CITY", 20, 7, "up")
ow = game.overworld
U.shot(game, DIR .. "/cer_0_before.png")
U.hold(game, "up", 20)
mashUntil(function()
return ow.runner:isRunning() or game.stack:top() ~= ow
end, "cerulean ambush", 200)
U.shot(game, DIR .. "/cer_1_ambush.png")
mashUntil(function() return game.stack:top() ~= ow end, "cerulean battle", 800)
winBattle("cerulean")
local function boxText(top)
if not top or type(top.pages) ~= "table" then return "" end
local parts = {}
for _, page in ipairs(top.pages) do
if type(page) == "table" then
for _, line in ipairs(page) do parts[#parts + 1] = tostring(line) end
end
end
return table.concat(parts, " ")
end
local billShot, exitShot, lastBox, postBoxes = false, false, nil, 0
for _ = 1, 2000 do
local top = game.stack:top()
if top ~= ow then
local t = boxText(top)
if game.save.flags.EVENT_BEAT_CERULEAN_RIVAL and t ~= "" and top ~= lastBox then
lastBox = top
postBoxes = postBoxes + 1
U.log("post-battle box", postBoxes, t:sub(1, 80))
-- Bill line is the second post-flag text (after DefeatedText)
if not billShot and (t:find("BILL") or t:find("Storage")
or t:find("invented") or t:find("MANIAC") or t:find("pages")
or t:find("guess what") or postBoxes >= 2) then
billShot = true
for _ = 1, 30 do U.wait(2); U.tap(game, "a") end -- advance into Bill body
U.wait(20)
U.shot(game, DIR .. "/cer_2_bill.png")
U.log("bill text:", boxText(game.stack:top()):sub(1, 160))
end
end
U.tap(game, "a")
elseif game.save.flags.EVENT_BEAT_CERULEAN_RIVAL then
local r = rivalNpc()
if r and r.cellY and r.cellY >= 6 and r.facing == "down" and not exitShot then
exitShot = true
U.shot(game, DIR .. "/cer_3_exit.png")
U.log("cer exit:", r.facing, r.cellX, r.cellY)
end
if idle(ow) then break end
end
U.wait(2)
end
mashUntil(function() return idle(ow) end, "cerulean done", 800)
U.shot(game, DIR .. "/cer_4_done.png")
U.log("cer beat:", tostring(game.save.flags.EVENT_BEAT_CERULEAN_RIVAL),
"billShot:", tostring(billShot), "exitShot:", tostring(exitShot))
end
+5
View File
@@ -731,6 +731,11 @@ local function switchTo(battle, slot)
end
local t = replacementMenu()
if not t or t.index ~= slot then break end
press("a") -- select mon -> SWITCH / STATS / CANCEL
U.wait(8)
t = replacementMenu()
if not (t and t.submenu) then break end
-- SWITCH is first (SwitchStatsCancelText)
press("a")
U.wait(10)
ok = true
@@ -0,0 +1,60 @@
-- Driver: S.S. Anne kitchen PrintTrashText bins (#188).
-- Teleports beside each bin, interacts, screenshots the dialogue.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local TextBox = require("src.render.TextBox")
local pass = true
local function check(cond, msg)
if cond then U.log("PASS: " .. msg) else pass = false; U.log("FAIL: " .. msg) end
end
local function topIs(mt) return getmetatable(game.stack:top()) == mt end
local bins = game.data.field.hiddenExtras
and game.data.field.hiddenExtras.printTrash
and game.data.field.hiddenExtras.printTrash.SS_ANNE_KITCHEN
check(bins and #bins == 2, "SS_ANNE_KITCHEN has 2 PrintTrashText bins")
for i, bin in ipairs(bins or {}) do
-- stand west of the bin facing right (cooks occupy the column)
U.teleport(game, "SS_ANNE_KITCHEN", bin.x - 1, bin.y, "right")
local ow = game.overworld
local fx, fy = ow.player:facingCell()
U.log(("bin %d: at (%d,%d) facing %s -> (%d,%d)"):format(
i, ow.player.cellX, ow.player.cellY, ow.player.facing, fx, fy))
check(fx == bin.x and fy == bin.y, ("facing kitchen trash at (%d,%d)"):format(bin.x, bin.y))
U.tap(game, "a")
local opened = false
for _ = 1, 60 do
if topIs(TextBox) then opened = true; break end
U.wait(2)
end
check(opened, ("bin %d opened a TextBox"):format(i))
if opened then
local box = game.stack:top()
for _ = 1, 180 do
if not topIs(TextBox) then break end
if box.waiting or box.done then break end
U.wait(1)
end
U.wait(4)
local page = box.pages and box.pages[1]
local body = type(page) == "table" and table.concat(page, "\n")
or (type(page) == "string" and page or "")
U.log("dialogue:", body:gsub("\n", "\\n"):sub(1, 80))
check(body:find("only trash here", 1, true) ~= nil,
("bin %d shows trash dialogue"):format(i))
U.shot(game, DIR .. ("/ss_anne_kitchen_trash_%d.png"):format(i))
for _ = 1, 40 do
if not topIs(TextBox) then break end
U.tap(game, "a")
U.wait(2)
end
end
end
U.log(pass and "RESULT: ALL PASS" or "RESULT: SEE FAILURES ABOVE")
end
@@ -0,0 +1,49 @@
-- Driver: #189 S.S. Anne 1F door→room order. Enter each hallway cabin
-- door left→right; screenshot room contents (SHOT_DIR).
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots-189"
local doors = {}
for i, w in ipairs(game.data.maps.SS_ANNE_1F.warps) do
if w.destMap == "SS_ANNE_1F_ROOMS" then
doors[#doors + 1] = { index = i, x = w.x, y = w.y, destWarp = w.destWarp }
end
end
table.sort(doors, function(a, b) return a.x < b.x end)
U.teleport(game, "SS_ANNE_1F_ROOMS", 12, 8, "down")
U.shot(game, DIR .. "/anne189_rooms_overview.png")
U.teleport(game, "SS_ANNE_1F", 20, 10, "up")
U.shot(game, DIR .. "/anne189_hall_overview.png")
for n, d in ipairs(doors) do
U.teleport(game, "SS_ANNE_1F", d.x, d.y + 1, "up")
U.shot(game, string.format("%s/anne189_door%d_hall_x%d.png", DIR, n, d.x))
U.hold(game, "up", 24)
local ow = game.overworld
for _ = 1, 120 do
U.wait(1)
if ow.map.id == "SS_ANNE_1F_ROOMS" and not ow.transitioning
and #(ow.scriptMoves or {}) == 0 and not ow.player.moving then
break
end
end
U.wait(10)
local names = {}
local col = math.floor(ow.player.cellX / 10)
local row = math.floor(ow.player.cellY / 10)
for _, npc in ipairs(ow.npcs or {}) do
if math.floor(npc.cellX / 10) == col and math.floor(npc.cellY / 10) == row then
names[#names + 1] = npc.def.name or "?"
end
end
table.sort(names)
U.log(string.format(
"door%d hall(%d,%d)#%d destWarp=%d -> %s (%d,%d) cell=%d,%d npcs: %s",
n, d.x, d.y, d.index, d.destWarp, ow.map.id,
ow.player.cellX, ow.player.cellY, col, row, table.concat(names, ",")))
U.shot(game, string.format("%s/anne189_door%d_room.png", DIR, n))
end
end
+84
View File
@@ -0,0 +1,84 @@
-- Driver for #169: poison status icon must appear AFTER move anim +
-- "was poisoned!" text, not during the "used POISONPOWDER!" announce.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local BattleState = require("src.battle.BattleState")
local mon = Pokemon.new(game.data, "BULBASAUR", 12)
mon.moves[1] = { id = "POISONPOWDER", pp = 20 }
mon.moves[2] = { id = "TACKLE", pp = 20 }
table.insert(game.save.party, 1, mon)
U.teleport(game, "ROUTE_1", 5, 5, "down")
local ow = game.overworld
local battle = BattleState.newWild(game, "PIDGEY", 8)
battle.onFinish = function() end
-- force hit + make the foe never act (skip second half of the turn)
battle.rng = function(a, b) return a or 0 end
battle.enemyAction = function() return nil end
ow:pushBattle(battle)
local function waitFor(cond, max)
for _ = 1, max or 600 do
if cond() then return true end
U.wait(1)
end
return false
end
local function mashUntil(cond, max)
for _ = 1, max or 80 do
if cond() then return true end
U.tap(game, "a")
U.wait(4)
end
return false
end
mashUntil(function() return battle.phase == "menu" end)
U.shot(game, DIR .. "/poison_0_menu.png")
U.tap(game, "a"); U.wait(8) -- FIGHT
U.tap(game, "a"); U.wait(2) -- POISONPOWDER
-- announce: "BULBASAUR used POISONPOWDER!" — no PSN on HUD yet
waitFor(function()
return battle.current and battle.current.text
and battle.current.text:find("POISONPOWDER", 1, true)
end, 120)
U.shot(game, DIR .. "/poison_1_announce.png")
U.log(("announce shownStatus=%s mon.status=%s"):format(
tostring(battle.enemy.shownStatus), tostring(battle.enemy.mon.status)))
-- mash through announce into the move anim / poisoned text
local sawAnim, sawText = false, false
for _ = 1, 200 do
if battle.animPlaying or battle.animName == "POISONPOWDER" then
if not sawAnim then
sawAnim = true
U.shot(game, DIR .. "/poison_2_anim.png")
U.log(("anim shownStatus=%s"):format(tostring(battle.enemy.shownStatus)))
end
end
if battle.current and battle.current.text
and battle.current.text:find("poisoned", 1, true) then
if not sawText then
sawText = true
U.shot(game, DIR .. "/poison_3_text.png")
U.log(("text shownStatus=%s"):format(tostring(battle.enemy.shownStatus)))
end
end
if battle.enemy.shownStatus == "PSN" and sawText then
U.shot(game, DIR .. "/poison_4_icon.png")
U.log("HUD PSN revealed after poisoned text")
break
end
U.tap(game, "a")
U.wait(2)
end
waitFor(function() return battle.phase == "menu" end, 200)
U.shot(game, DIR .. "/poison_5_menu.png")
U.log(("final shownStatus=%s"):format(tostring(battle.enemy.shownStatus)))
end
+60
View File
@@ -0,0 +1,60 @@
-- Driver: try a TM from the battle bag; Gen 1 refuses with Oak's
-- "This isn't the time to use that!" (ItemUseTMHM -> ItemUseNotTime).
--
-- SHOT_DIR=/tmp/tm_battle_shots POKEPORT_DRIVER=tests/drivers/tm_battle_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local Pokemon = require("src.pokemon.Pokemon")
local Bag = require("src.inventory.Bag")
local TextBox = require("src.render.TextBox")
local ListMenu = require("src.ui.ListMenu")
table.insert(game.save.party, Pokemon.new(game.data, "CHARMANDER", 12))
Bag.add(game.save, "TM_TOXIC", 1)
U.teleport(game, "ROUTE_1", 5, 5, "down")
local ow = game.overworld
local BattleState = require("src.battle.BattleState")
local battle = BattleState.newWild(game, "PIDGEY", 8)
battle.onFinish = function() end
ow:pushBattle(battle)
local function mashUntil(cond, max)
for _ = 1, max or 80 do
if cond() then return true end
U.tap(game, "a")
U.wait(4)
end
return false
end
local function pageText(box)
if not box or getmetatable(box) ~= TextBox then return "" end
return table.concat(box.pages[box.pageIndex] or {}, "\n")
end
U.log("to menu:", mashUntil(function() return battle.phase == "menu" end))
U.shot(game, DIR .. "/tm_battle_0_menu.png")
-- FIGHT/PKMN / ITEM/RUN: down to ITEM, then A
U.tap(game, "down"); U.wait(4)
U.tap(game, "a"); U.wait(12)
U.shot(game, DIR .. "/tm_battle_1_bag.png")
U.log("bag open:", getmetatable(game.stack:top()) == ListMenu)
U.tap(game, "a")
mashUntil(function()
local top = game.stack:top()
return getmetatable(top) == TextBox and top.waiting
and pageText(top):find("isn't the", 1, true)
end, 40)
U.shot(game, DIR .. "/tm_battle_2_reject.png")
local top = game.stack:top()
local text = pageText(top)
U.log("reject TextBox:", getmetatable(top) == TextBox)
U.log("oak text:", text:find("isn't the", 1, true) and "ok" or text)
U.log("tm still in bag:", (game.save.inventory.TM_TOXIC or 0) >= 1)
U.log("not booted:", not text:find("Booted", 1, true))
end
+155
View File
@@ -0,0 +1,155 @@
-- Driver: NPC in-game trade cable animation (InternalClockTradeAnim).
-- Vermilion Trade House: SPEAROW -> FARFETCH'D (DUX).
--
-- SHOT_DIR=/tmp/trade_shots POKEPORT_DRIVER=tests/drivers/trade_anim_test.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/trade_shots"
local Pokemon = require("src.pokemon.Pokemon")
local TradeAnim = require("src.ui.TradeAnim")
local TextBox = require("src.render.TextBox")
local PartyMenu = require("src.ui.PartyMenu")
local ChoiceBox = require("src.ui.ChoiceBox")
local function topIs(cls)
return getmetatable(game.stack:top()) == cls
end
game.save.party = { Pokemon.new(game.data, "SPEAROW", 10) }
U.teleport(game, "VERMILION_TRADE_HOUSE", 3, 6, "up")
U.wait(5)
U.shot(game, DIR .. "/trade_00_house.png")
U.tap(game, "a")
U.wait(10)
for _ = 1, 200 do
if topIs(ChoiceBox) then break end
U.tap(game, "a")
U.wait(2)
end
U.shot(game, DIR .. "/trade_01_offer.png")
U.tap(game, "a") -- YES
U.wait(6)
for _ = 1, 60 do
if topIs(PartyMenu) then break end
U.wait(1)
end
U.log("party menu:", topIs(PartyMenu))
U.shot(game, DIR .. "/trade_02_party.png")
U.tap(game, "a")
U.wait(6)
for _ = 1, 200 do
if topIs(TradeAnim) then break end
U.tap(game, "a")
U.wait(2)
end
local anim = game.stack:top()
if getmetatable(anim) ~= TradeAnim then
U.log("FAIL: TradeAnim never appeared")
return
end
U.log("TradeAnim phase:", anim.phase)
local function ffUntil(phase, cap)
for _ = 1, cap or 3000 do
if anim.phase == phase or anim.phase == "done" then break end
if anim.waitingText or topIs(TextBox) then
U.wait(1)
else
anim:update(1 / 60)
end
end
U.wait(1)
end
-- hold mid show_player (scx settled, mon visible)
ffUntil("show_player", 1)
while anim.phase == "show_player" and (anim.sub ~= "hold" or anim.scx > 0) do
anim:update(1 / 60)
end
for _ = 1, 10 do anim:update(1 / 60) end
U.wait(1)
U.shot(game, DIR .. "/trade_03_show_player.png")
U.log("show_player sub=", anim.sub, "scx=", anim.scx)
ffUntil("open_cable", 500)
-- mid scroll-in
while anim.phase == "open_cable" and anim.scx > 40 do
anim:update(1 / 60)
end
U.wait(1)
U.shot(game, DIR .. "/trade_04_open_cable.png")
U.log("open_cable scx=", anim.scx)
ffUntil("ball_enter", 200)
while anim.phase == "ball_enter" and anim.ballX < 0x80 do
anim:update(1 / 60)
end
U.wait(1)
U.shot(game, DIR .. "/trade_05_ball_enter.png")
U.log("ball at", anim.ballX, anim.ballY)
ffUntil("transfer_lr", 400)
for _ = 1, 24 do anim:update(1 / 60) end
U.wait(1)
U.shot(game, DIR .. "/trade_06_transfer_lr.png")
ffUntil("went_to", 800)
for _ = 1, 600 do
if topIs(TextBox) then
local tb = game.stack:top()
if tb.done then break end
-- finish typewriter so both lines are visible
if tb.codes then tb.charIndex = #tb.codes end
if tb.pages and tb.pageIndex and tb.lineIndex then
local page = tb.pages[tb.pageIndex]
if page and tb.lineIndex < #page then
tb.lineIndex = #page
tb.shown = {}
for _, line in ipairs(page) do
local codes = require("src.render.Font").encode(line)
tb.shown[#tb.shown + 1] = codes
end
tb.charIndex = #(tb.shown[#tb.shown] or {})
tb.done = true
end
end
break
end
U.wait(1)
end
U.wait(2)
U.shot(game, DIR .. "/trade_07_went_to.png")
U.log("went-to:", topIs(TextBox))
ffUntil("transfer_rl", 2500)
for _ = 1, 16 do anim:update(1 / 60) end
U.wait(1)
U.shot(game, DIR .. "/trade_08_transfer_rl.png")
ffUntil("show_enemy", 800)
while anim.phase == "show_enemy" and not anim.monVisible do
anim:update(1 / 60)
end
for _ = 1, 30 do anim:update(1 / 60) end
U.wait(1)
U.shot(game, DIR .. "/trade_09_show_enemy.png")
for _ = 1, 5000 do
if game.stack:top() == game.overworld then break end
local top = game.stack:top()
if getmetatable(top) == TradeAnim and not top.waitingText then
top:update(1 / 60)
end
U.tap(game, "a")
U.wait(1)
end
U.shot(game, DIR .. "/trade_10_done.png")
local mon = game.save.party[1]
U.log("party:", mon and mon.species, "nick:", mon and mon.nickname,
"ot:", mon and mon.ot)
U.log("done")
end
+34
View File
@@ -0,0 +1,34 @@
-- Driver: trainer card with empty badge slots (gym leader faces) and a
-- partial set (face/badge mix), matching DrawBadges in pokered.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
game.save.player.name = "RED"
game.save.money = 3000
game.save.playTime = 3661
game.save.inventory = game.save.inventory or {}
local TrainerCard = require("src.ui.TrainerCard")
-- no badges: every slot should show the gym leader face
for _, id in ipairs({
"BOULDERBADGE", "CASCADEBADGE", "THUNDERBADGE", "RAINBOWBADGE",
"SOULBADGE", "MARSHBADGE", "VOLCANOBADGE", "EARTHBADGE",
}) do
game.save.inventory[id] = nil
end
game.stack:push(TrainerCard.new(game))
U.wait(5)
U.shot(game, DIR .. "/trainer_card_0_faces.png")
U.tap(game, "b"); U.wait(3)
-- boulder + cascade owned: first two slots swap to badges
game.save.inventory.BOULDERBADGE = true
game.save.inventory.CASCADEBADGE = true
game.stack:push(TrainerCard.new(game))
U.wait(5)
U.shot(game, DIR .. "/trainer_card_1_partial.png")
U.tap(game, "b"); U.wait(3)
U.log("TRAINER_CARD_DRIVER: done")
end
@@ -0,0 +1,70 @@
-- Driver: trainer sight must not engage off the GB screen (#153/#183).
-- Oracle: engine/overworld/trainer_sight.asm TrainerEngage (IMAGEINDEX=$ff)
-- + movement.asm CheckSpriteAvailability (10x9 window). 8-bit pixel
-- distance wraps at ~12/28 tiles; without the on-screen gate, Viridian
-- Forest / Victory Road trainers aggro through trees and walls.
--
-- Scene A: VIRIDIAN_FOREST (18,33), west of tree wall vs Bug Catcher
-- (30,33) LEFT range 4 — dx=12 pixel-wrap must NOT engage.
-- Scene B: same trainer at (26,33) on-screen — MUST engage + walk-up.
-- Scene C: VICTORY_ROAD_2F (0,9) vs Hiker (12,9) — dx=12 must NOT engage.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local function trainerByIndex(ow, index)
for _, npc in ipairs(ow.npcs) do
if npc.def.index == index then return npc end
end
end
-- --- A: Viridian Forest wrap through trees (dx=12, walkable west of wall) ---
U.teleport(game, "VIRIDIAN_FOREST", 18, 33, "right")
local ow = game.overworld
local t = trainerByIndex(ow, 2)
U.wait(10)
U.log("VF far: player", ow.player.cellX, ow.player.cellY,
"trainer", t and t.cellX, t and t.cellY,
"engaging", tostring(ow.engaging))
assert(t and t.cellX == 30 and t.cellY == 33, "VF Bug Catcher at (30,33)")
assert(not ow.engaging, "VF: off-screen trainer must not engage through trees")
assert(t.cellX == 30 and t.cellY == 33, "VF: trainer must not walk through trees")
U.shot(game, DIR .. "/153_vf_far_no_aggro.png")
-- --- B: real on-screen sight line (same corridor, no tree wall) ---
U.teleport(game, "VIRIDIAN_FOREST", 26, 33, "right")
ow = game.overworld
t = trainerByIndex(ow, 2)
local guard = 0
while not ow.engaging and guard < 30 do
guard = guard + 1
U.wait(1)
end
U.log("VF near: engaging", tostring(ow.engaging),
"emote", tostring(ow.emote and ow.emote.frames),
"trainer", t and t.cellX, t and t.cellY)
assert(ow.engaging, "VF: distance-4 on-screen must engage")
U.shot(game, DIR .. "/153_vf_near_engage.png")
-- EmotionBubble 60f + (dist-1)*16 walk frames
U.wait(60 + 3 * 16 + 10)
U.log("VF walkup: trainer", t.cellX, t.cellY, "player", ow.player.cellX, ow.player.cellY)
assert(t.cellY == 33 and t.cellX >= 26 and t.cellX <= 30,
"VF: walk-up stays on the open corridor")
assert(t.cellX == 27, "VF: trainer stops adjacent after distance-1 walk-up")
U.shot(game, DIR .. "/153_vf_near_walkup.png")
-- --- C: Victory Road 2F wrap (dx=12) ---
U.teleport(game, "VICTORY_ROAD_2F", 0, 9, "right")
ow = game.overworld
t = trainerByIndex(ow, 1)
U.wait(10)
U.log("VR2 far: player", ow.player.cellX, ow.player.cellY,
"trainer", t and t.cellX, t and t.cellY,
"engaging", tostring(ow.engaging))
assert(t and t.cellX == 12 and t.cellY == 9, "VR2 Hiker at (12,9)")
assert(not ow.engaging, "VR2: off-screen trainer must not engage")
assert(t.cellX == 12 and t.cellY == 9, "VR2: trainer must not walk through walls")
U.shot(game, DIR .. "/183_vr2_far_no_aggro.png")
U.log("trainer_sight_walls_test OK")
end
+9 -1
View File
@@ -33,7 +33,15 @@ end
function U.shot(game, path)
game.capturePath = path
U.wait(2) -- let the capture flush
-- love.draw consumes capturePath once per rendered frame, but fast runs
-- (POKEPORT_SPEED) step the driver many times per render; spin until the
-- capture lands so later actions can't outrun it
for _ = 1, 120 do
if not game.capturePath then break end
frame = frame + 1
coroutine.yield()
end
U.wait(1)
end
-- skip the intro movie + title into a fresh overworld game