mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 12:15:31 +02:00
fixing bugs
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user