mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-22 13:36:14 +02:00
Fix Gen 1/2 battle, menu, PC and audio parity gaps; wire luacheck into CI
CLOSES #1484, CLOSES #1486, CLOSES #1513, CLOSES #1517, CLOSES #1556, CLOSES #1564, CLOSES #1567
This commit is contained in:
@@ -83,15 +83,36 @@ return function(game)
|
||||
-- the announcement-time row performMove inserts directly as well as the
|
||||
-- ones animNext adds, which is the whole difference the fix makes.
|
||||
local function watchAnims(battle)
|
||||
local seen, mark = {}, {}
|
||||
local seen, mark, types = {}, {}, {}
|
||||
return seen, function()
|
||||
for _, row in ipairs(battle.queue) do
|
||||
if row.anim and not mark[row] then
|
||||
mark[row] = true
|
||||
seen[#seen + 1] = row.anim
|
||||
types[row.anim] = row.hit and row.hit.animType or false
|
||||
end
|
||||
end
|
||||
end, types
|
||||
end
|
||||
|
||||
-- engine/battle/animations.asm:427-437
|
||||
local function rideAndSample(battle, poll, frames, shotPath)
|
||||
local peak, shot = 0, false
|
||||
for i = 1, frames do
|
||||
poll()
|
||||
if i % 6 == 0 then U.tap(battle.game, "a") end
|
||||
local fx = battle.fx
|
||||
if fx and fx.shakeX and not battle.animPlaying then
|
||||
local dx = math.abs(fx.shakeX)
|
||||
if dx > peak then peak = dx end
|
||||
if dx > 0 and not shot then
|
||||
shot = true
|
||||
U.shot(battle.game, shotPath)
|
||||
end
|
||||
end
|
||||
U.wait(1)
|
||||
end
|
||||
return peak
|
||||
end
|
||||
|
||||
local function listOf(seen)
|
||||
@@ -136,23 +157,28 @@ return function(game)
|
||||
local battle = BattleState.newWild(game, "PIDGEY", 8)
|
||||
battle.onFinish = function() end
|
||||
ow:pushBattle(battle)
|
||||
local seen, poll = watchAnims(battle)
|
||||
local seen, poll, types = watchAnims(battle)
|
||||
check("the battle reached the menu", waitPhase(battle, "menu", 60, poll))
|
||||
check("BIDE is the move on the list", pickBide(battle, poll))
|
||||
for _ = 1, 20 do poll() U.wait(1) end
|
||||
U.shot(game, DIR .. "/bug375_store_spiral.png")
|
||||
for _ = 1, 30 do poll() U.wait(1) end
|
||||
|
||||
local peak = rideAndSample(battle, poll, 180,
|
||||
DIR .. "/bug1564_store_shake.png")
|
||||
U.shot(game, DIR .. "/bug375_store_text.png")
|
||||
|
||||
check("the storing turn queued XSTATITEM_ANIM", has(seen, "XSTATITEM_ANIM"))
|
||||
check("and not BIDE's hit animation: " .. listOf(seen), not has(seen, "BIDE"))
|
||||
check("BIDE locked in for " .. tostring(battle.player.bideTurns) .. " turns",
|
||||
battle.player.bideTurns ~= nil)
|
||||
check("the storing spiral carries wAnimationType 6 (#1564)",
|
||||
types["XSTATITEM_ANIM"] == 6)
|
||||
check(("the screen shook, peak dx = %d px (want 3)"):format(peak), peak == 3)
|
||||
|
||||
-- ride out the locked turns: the storing text needs A presses and the menu
|
||||
-- never comes back until BIDE unleashes
|
||||
local released = false
|
||||
for _ = 1, 120 do
|
||||
for _ = 1, 220 do
|
||||
U.tap(game, "a")
|
||||
for _ = 1, 6 do poll() U.wait(1) end
|
||||
if has(seen, "BIDE") then
|
||||
@@ -179,7 +205,7 @@ return function(game)
|
||||
foeBattle.enemy.mon.moves = { { id = "BIDE", pp = 10 } }
|
||||
foeBattle.enemy.curMoves = foeBattle.enemy.mon.moves
|
||||
ow:pushBattle(foeBattle)
|
||||
local foeSeen, foePoll = watchAnims(foeBattle)
|
||||
local foeSeen, foePoll, foeTypes = watchAnims(foeBattle)
|
||||
check("the foe's battle reached the menu",
|
||||
waitPhase(foeBattle, "menu", 60, foePoll))
|
||||
pickBide(foeBattle, foePoll) -- ours does not matter here, the foe's does
|
||||
@@ -192,10 +218,20 @@ return function(game)
|
||||
check("the foe's storing turn queued XSTATITEM_DUPLICATE_ANIM: "
|
||||
.. listOf(foeSeen), has(foeSeen, "XSTATITEM_DUPLICATE_ANIM"))
|
||||
|
||||
local foePeak = rideAndSample(foeBattle, foePoll, 180,
|
||||
DIR .. "/bug1564_enemy_store_shake.png")
|
||||
check("the foe's spiral carries wAnimationType 3 (#1564)",
|
||||
foeTypes["XSTATITEM_DUPLICATE_ANIM"] == 3)
|
||||
check(("the foe's shake peaked at %d px (want 6)"):format(foePeak),
|
||||
foePeak == 6)
|
||||
|
||||
U.log("The pad is yours in a battle where both sides know only BIDE, so")
|
||||
U.log("pick FIGHT then BIDE and watch turn one: the screen palette flashes")
|
||||
U.log("white and balls spiral inward, silently, and then it says storing")
|
||||
U.log("energy. No flash on the RATTATA and no BIDE thud until the turn it")
|
||||
U.log("white and balls spiral inward, silently. Then, still in silence, the")
|
||||
U.log("whole battle screen creeps sideways 1 px at a time out to 3 px and")
|
||||
U.log("back, twice, and only then does it say storing energy. On the foe's")
|
||||
U.log("turn the same creep goes out to 6 px and takes twice as long.")
|
||||
U.log("No flash on the RATTATA and no BIDE thud until the turn it")
|
||||
U.log("unleashes, where the thud and its animation come after the text and")
|
||||
U.log("before the enemy HP bar slides down. The foe's spiral looks the same.")
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
-- scripts/Route16Gate1F.asm:38 / home/overworld.asm:1224
|
||||
-- data/maps/force_bike_surf.asm:5
|
||||
-- POKEPORT_DRIVER=tests/drivers/bike_gate_wall_bug1548_test.lua \
|
||||
-- POKEPORT_IDENTITY=bug1548 POKEPORT_TOUCH=0 POKEPORT_VERSION=red love .
|
||||
-- Watch the sprite after each teleport: it must never overlap the gate
|
||||
-- building's black outline. Compare .bazinga/august21p2/media/1548-1.jpg.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local OverworldState = require("src.world.OverworldController")
|
||||
|
||||
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local ok = true
|
||||
local function check(label, pass)
|
||||
U.log(pass and "PASS" or "FAIL", label)
|
||||
if not pass then ok = false end
|
||||
return pass
|
||||
end
|
||||
|
||||
local function settle()
|
||||
for _ = 1, 12 do
|
||||
if game.stack:top() == game.overworld then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(25)
|
||||
end
|
||||
U.wait(60)
|
||||
end
|
||||
local function landed()
|
||||
local ow = game.overworld
|
||||
local p = ow.player
|
||||
return p.cellX, p.cellY, ow.map:isWalkableCell(p.cellX, p.cellY)
|
||||
end
|
||||
local function bikeless()
|
||||
game.save.inventory.BICYCLE = nil
|
||||
game.save.onBike, game.save.forcedBike = false, nil
|
||||
end
|
||||
|
||||
game.save.player.name = "PROBE"
|
||||
game.save.party = { Pokemon.new(game.data, "CHARIZARD", 50) }
|
||||
bikeless()
|
||||
|
||||
local fm = game.data.field.forcedMovement
|
||||
check("field.forcedMovement carries the Route 16 cells",
|
||||
fm and fm.tiles and fm.tiles.ROUTE_16 ~= nil)
|
||||
|
||||
for _, c in ipairs({ { "ROUTE_16", 17, 10 }, { "ROUTE_16", 17, 11 },
|
||||
{ "ROUTE_18", 33, 8 }, { "ROUTE_18", 33, 9 } }) do
|
||||
bikeless()
|
||||
U.teleport(game, c[1], c[2], c[3], "left")
|
||||
U.wait(20)
|
||||
settle()
|
||||
local x, y, walk = landed()
|
||||
U.log(("%s (%d,%d) facing left -> (%d,%d) walkable=%s")
|
||||
:format(c[1], c[2], c[3], x, y, tostring(walk)))
|
||||
check(("%s (%d,%d): the refusal leaves the player on a walkable cell")
|
||||
:format(c[1], c[2], c[3]), walk)
|
||||
end
|
||||
|
||||
bikeless()
|
||||
U.teleport(game, "ROUTE_16", 17, 10, "left")
|
||||
U.wait(20)
|
||||
settle()
|
||||
U.shot(game, SHOT_DIR .. "/bug1548_route16_after.png")
|
||||
|
||||
bikeless()
|
||||
U.teleport(game, "ROUTE_16", 16, 10, "right")
|
||||
U.wait(20)
|
||||
U.hold(game, "right", 24)
|
||||
settle()
|
||||
local bx, by = landed()
|
||||
U.log(("stepped onto ROUTE_16 (17,10) from the west -> (%d,%d)"):format(bx, by))
|
||||
check("a shove with open ground behind it still moves one cell",
|
||||
bx == 16 and by == 10)
|
||||
|
||||
bikeless()
|
||||
while game.stack:top() do game.stack:pop() end
|
||||
game.stack:push(OverworldState, "ROUTE_16", 18, 10, "left")
|
||||
game.overworld:setMap("ROUTE_16", 18, 10, "left", { via = "boot" })
|
||||
U.wait(20)
|
||||
local cx, cy, cwalk = landed()
|
||||
U.log(("boot inside the wall (18,10) -> (%d,%d) walkable=%s")
|
||||
:format(cx, cy, tostring(cwalk)))
|
||||
check("a save parked inside the gate wall is lifted out on load", cwalk)
|
||||
settle()
|
||||
local dx, dy, dwalk = landed()
|
||||
U.log(("after the refusal that follows -> (%d,%d) walkable=%s")
|
||||
:format(dx, dy, tostring(dwalk)))
|
||||
check("and the refusal it lands on cannot push it back in", dwalk)
|
||||
U.shot(game, SHOT_DIR .. "/bug1548_repaired.png")
|
||||
|
||||
game.save.inventory.BICYCLE = 1
|
||||
game.save.onBike, game.save.forcedBike = false, nil
|
||||
U.teleport(game, "ROUTE_16", 17, 10, "left")
|
||||
U.wait(30)
|
||||
local ex, ey, ewalk = landed()
|
||||
check("with a BICYCLE the forced tile mounts instead of shoving",
|
||||
game.save.onBike == true and ex == 17 and ey == 10 and ewalk)
|
||||
|
||||
U.log(ok and "all clear" or "a check failed")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
-- Driver: #1517's other half, the error handler that hid the crash.
|
||||
--
|
||||
-- boot.lua's deferErrhand picks `love.errorhandler or love.errhand`, so a
|
||||
-- love.errorhandler that returns nil ends love.run's `while func do` loop and
|
||||
-- the process leaves to the OS with no error screen and nothing on stdout.
|
||||
-- LOVE 11.5 pre-populates love.errhand only, so main.lua capturing
|
||||
-- love.errorhandler captured nil and every Lua error in every shipped build
|
||||
-- exited silently. Measured on the installed 11.5:
|
||||
-- love.errorhandler type: nil / love.errhand type: function
|
||||
--
|
||||
-- Nothing here can be a unit test: the deliverable is whether a human sees
|
||||
-- LOVE's blue error screen or an app that vanishes.
|
||||
--
|
||||
-- POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/errorhandler_probe_bug1517_test.lua love .
|
||||
--
|
||||
-- Pre-fix: the window closes on the first drawn frame after the probe arms,
|
||||
-- exit 1, no output. Post-fix: the blue screen reads "bug1517: error-handler
|
||||
-- probe" plus the lua-error.log hint, and stays up until it is dismissed.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
|
||||
U.log("[1517] love.errorhandler at boot was: "
|
||||
.. (rawget(love, "errorhandler") and "a function" or "nil"))
|
||||
U.log("[1517] love.errhand is: " .. type(rawget(love, "errhand")))
|
||||
|
||||
U.wait(30)
|
||||
|
||||
-- Raise from a draw callback, not from this coroutine: main.lua resumes the
|
||||
-- driver under coroutine.resume and prints its errors itself, which is the
|
||||
-- one path that does NOT reach love.errorhandler. StateStack:draw only
|
||||
-- calls the states it is actually holding, so the probe goes on the live
|
||||
-- top state rather than on game.overworld, which is not on the stack while
|
||||
-- the title screen or the load menu is up.
|
||||
local target = game.stack:top()
|
||||
U.log("[1517] arming the probe on " .. tostring(target and target.screenId))
|
||||
local realDraw = target.draw
|
||||
target.draw = function(self, ...)
|
||||
if realDraw then realDraw(self, ...) end
|
||||
error("bug1517: error-handler probe")
|
||||
end
|
||||
|
||||
U.log("[1517] armed; the next drawn frame raises from love.draw")
|
||||
U.log("[1517] you should now see LOVE's blue error screen, not a closed app")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
@@ -70,15 +70,18 @@ return function(game)
|
||||
U.tap(game, "right")
|
||||
shot("03-tmhm") -- TMs carry ×NN, the two HMs carry none
|
||||
|
||||
-- SELECT arms the row instead of registering it (#1427): the hollow ▷ marks
|
||||
-- TM_HEADBUTT while "Where should this be moved to?" holds the box.
|
||||
-- SELECT armed the row here for #1427; since #1567 it is filtered out of the
|
||||
-- TM/HM pocket entirely (engine/items/tmhm.asm:207), so these taps must leave
|
||||
-- the list exactly as shot 03 has it: no hollow ▷, no "Where should this be
|
||||
-- moved to?", no reorder.
|
||||
U.tap(game, "down")
|
||||
U.tap(game, "select")
|
||||
shot("04-tmhm-armed")
|
||||
shot("04-tmhm-select-ignored")
|
||||
U.tap(game, "up")
|
||||
shot("05-tmhm-destination")
|
||||
shot("05-tmhm-still-numbered")
|
||||
U.tap(game, "a")
|
||||
shot("06-tmhm-moved") -- HEADBUTT is now the first TM row
|
||||
U.tap(game, "b")
|
||||
shot("06-tmhm-unchanged")
|
||||
|
||||
U.log("[driver] bag order:", table.concat(Bag.order(save), " "))
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
-- Assertion driver: a KEY ITEM and an HM through the player's item PC.
|
||||
--
|
||||
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1486_test.lua \
|
||||
-- perl -e 'alarm 300; exec @ARGV' \
|
||||
-- python3 -c "import pty; pty.spawn(['love','.'])"
|
||||
--
|
||||
-- Issue #1486. What the unit tier cannot show: the two screens a player
|
||||
-- actually looks at. .DepositItem (engine/events/pokecenter_pc.asm:504) reads
|
||||
-- CANT_TOSS only to force x1 and skip .AskQuantity, and PlaceMenuItemQuantity
|
||||
-- (engine/menus/menu_2.asm:18) draws no ×NN under such a row.
|
||||
--
|
||||
-- Shots land in /tmp/gold-bug1486:
|
||||
--
|
||||
-- 01-key-items-pocket.png the DEPOSIT chooser on the KEY ITEMS pocket
|
||||
-- 02-deposited.png "Deposited 1 BICYCLE(S)." -- before the fix this
|
||||
-- is the unchanged pocket with no message at all
|
||||
-- 03-pc-list.png the PC list: POTION with its ×03, BICYCLE and
|
||||
-- HM01 with no ×NN at all
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1486"
|
||||
local fails = 0
|
||||
|
||||
local function ok(cond, msg)
|
||||
if cond then print("[1486] ok " .. msg)
|
||||
else fails = fails + 1 print("[1486] FAIL " .. msg) end
|
||||
return cond
|
||||
end
|
||||
|
||||
local function tap(button, frames)
|
||||
game.input.pressQueue[#game.input.pressQueue + 1] = button
|
||||
game.input.state[button] = true
|
||||
U.wait(2)
|
||||
game.input.state[button] = false
|
||||
U.wait(frames or 4)
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
local w = game.world
|
||||
assert(w and w.map, "gold world did not boot")
|
||||
local save = game.save
|
||||
|
||||
local Bag = require("src.inventory.Bag")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
save.party = { Mon.new(game.data, "CYNDAQUIL", 10) }
|
||||
save.inventory = {}
|
||||
save.pcItems = { POTION = 3 }
|
||||
assert(Bag.add(save, "BICYCLE", 1, game.data), "BICYCLE into the bag")
|
||||
assert(Bag.add(save, "HM_CUT", 1, game.data), "HM01 into the bag")
|
||||
|
||||
local function topId()
|
||||
local top = game.stack:top()
|
||||
return top and top.screenId or nil
|
||||
end
|
||||
|
||||
assert(w:setMap("CHERRYGROVE_POKECENTER_1F", 4, 4, "up"),
|
||||
"setMap CHERRYGROVE_POKECENTER_1F failed")
|
||||
U.wait(5)
|
||||
local pcX, pcY
|
||||
for cy = 0, w.map.heightCells - 1 do
|
||||
for cx = 0, w.map.widthCells - 1 do
|
||||
if w.map:cellCollision(cx, cy) == 0x93 then pcX, pcY = cx, cy end
|
||||
end
|
||||
end
|
||||
assert(pcX, "no COLL_PC tile in the Pokecenter")
|
||||
assert(w:setMap("CHERRYGROVE_POKECENTER_1F", pcX, pcY + 1, "up"),
|
||||
"setMap onto the PC tile failed")
|
||||
U.wait(5)
|
||||
|
||||
tap("a", 8) -- PCScript
|
||||
tap("a", 4) -- the turn-on line
|
||||
tap("down", 4)
|
||||
tap("a", 4) -- <PLAYER>'s PC
|
||||
tap("a", 4)
|
||||
tap("a", 6) -- both PokecenterPlayersPCText pages
|
||||
ok(topId() == "Gen2ItemPcMenu",
|
||||
"<PLAYER>'s PC opens the item PC (top: " .. tostring(topId()) .. ")")
|
||||
|
||||
-- DEPOSIT ITEM -> the PACK chooser -> the KEY ITEMS pocket.
|
||||
tap("down", 4)
|
||||
tap("a", 6)
|
||||
tap("right", 4) -- ITEM -> BALL
|
||||
tap("right", 4) -- BALL -> KEY ITEMS
|
||||
U.shot(game, out .. "/01-key-items-pocket.png")
|
||||
tap("a", 6) -- the BICYCLE row: x1, no prompt
|
||||
ok(save.pcItems.BICYCLE == 1,
|
||||
"the BICYCLE landed in the PC (" .. tostring(save.pcItems.BICYCLE) .. ")")
|
||||
ok(save.inventory.BICYCLE == nil,
|
||||
"and left the bag (" .. tostring(save.inventory.BICYCLE) .. ")")
|
||||
U.shot(game, out .. "/02-deposited.png")
|
||||
tap("a", 4) -- the Deposited line
|
||||
|
||||
-- The same, one pocket over: an HM.
|
||||
tap("right", 4) -- KEY ITEMS -> TM/HM
|
||||
tap("a", 6)
|
||||
ok(save.pcItems.HM_CUT == 1,
|
||||
"HM01 landed in the PC (" .. tostring(save.pcItems.HM_CUT) .. ")")
|
||||
ok(save.inventory.HM_CUT == nil,
|
||||
"and left the bag (" .. tostring(save.inventory.HM_CUT) .. ")")
|
||||
tap("a", 4)
|
||||
tap("b", 6) -- close the PACK
|
||||
|
||||
-- WITHDRAW ITEM: the list is where PlaceMenuItemQuantity shows.
|
||||
tap("up", 4)
|
||||
tap("a", 6)
|
||||
U.shot(game, out .. "/03-pc-list.png")
|
||||
tap("a", 6) -- the first row, x1 or the selector
|
||||
print(("[1486] %d failures"):format(fails))
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,142 @@
|
||||
-- #1556, the two battle jingles: _NewDexDataText's sound_slot_machine_start
|
||||
-- (data/text/common_3.asm:285) on a first catch, and _LearnedMoveText's
|
||||
-- sound_dex_fanfare_50_79 (data/text/common_3.asm:119) on a level-up move.
|
||||
--
|
||||
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1556_battle.lua \
|
||||
-- perl -e 'alarm 420; exec @ARGV' \
|
||||
-- python3 -c "import pty; pty.spawn(['love','.'])"
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1556-battle (default)
|
||||
--
|
||||
-- Both ride the message queue's `sfx` + `waitSfx` keys, which the "Gotcha!"
|
||||
-- line and the "grew to level" line already used -- the two lines next door
|
||||
-- to them did not. So the ear is comparing neighbours: the catch jingle must
|
||||
-- be followed by a SECOND, different one over the #DEX line, and the level-up
|
||||
-- fanfare by a second over "learned".
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local Sound = require("src.core.Sound")
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1556-battle"
|
||||
|
||||
local heard = {}
|
||||
local realPlay = Sound.play
|
||||
Sound.play = function(data, name)
|
||||
heard[#heard + 1] = name
|
||||
return realPlay(data, name)
|
||||
end
|
||||
local function reset() heard = {} end
|
||||
local function report(label, want)
|
||||
U.log(label, #heard > 0 and table.concat(heard, ", ") or "(silence)")
|
||||
U.log(" want:", want)
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
local world = game.world
|
||||
assert(world and world.map, "gold world did not boot")
|
||||
local save, data = game.save, game.data
|
||||
|
||||
local function battleScreen()
|
||||
local top = game.stack:top()
|
||||
return (top and top.battle) and top or nil
|
||||
end
|
||||
|
||||
local function tapUntil(predicate, tries, btn)
|
||||
for _ = 1, tries or 400 do
|
||||
if predicate() then return true end
|
||||
U.tap(game, btn or "a")
|
||||
U.wait(2)
|
||||
end
|
||||
return predicate()
|
||||
end
|
||||
|
||||
-- ---- 1. a first catch: "Gotcha!" then the #DEX line ---------------------
|
||||
save.party = { Mon.new(data, "CYNDAQUIL", 20) }
|
||||
save.inventory = { MASTER_BALL = 1 }
|
||||
save.pokedexReceived = true
|
||||
save.pokedex = { seen = {}, caught = {} }
|
||||
|
||||
local wild = Mon.new(data, "PIDGEY", 3)
|
||||
assert(wild, "the cache carries no PIDGEY")
|
||||
assert(world:startBattle({ wild = wild }), "the wild battle refused to start")
|
||||
assert(tapUntil(function()
|
||||
local screen = battleScreen()
|
||||
return screen ~= nil and screen.phase == "menu"
|
||||
end), "the battle never reached BattleMenu")
|
||||
local screen = battleScreen()
|
||||
|
||||
-- BattleMenuHeader's 2x2 grid: 1 FIGHT / 2 PkMn, 3 PACK / 4 RUN.
|
||||
if screen.menuIndex % 2 == 0 then U.tap(game, "left") U.wait(3) end
|
||||
if screen.menuIndex <= 2 then U.tap(game, "down") U.wait(3) end
|
||||
U.tap(game, "a")
|
||||
U.wait(6)
|
||||
local pack = game.stack:top()
|
||||
assert(pack and pack.rows, "the battle PACK did not open")
|
||||
local ballRow
|
||||
for index, row in ipairs(pack.rows) do
|
||||
if row.id == "MASTER_BALL" then ballRow = index end
|
||||
end
|
||||
assert(ballRow, "the battle PACK does not show the MASTER BALL")
|
||||
for _ = 2, ballRow do U.tap(game, "down") U.wait(2) end
|
||||
reset()
|
||||
U.tap(game, "a")
|
||||
U.wait(180) -- the throw, the wobbles, "Gotcha!"
|
||||
report("01 catch:",
|
||||
"Sfx_CaughtMon, then Sfx_SlotMachineStart over the #DEX line")
|
||||
U.shot(game, out .. "/01-caught.png")
|
||||
for _ = 1, 6 do U.tap(game, "a") U.wait(20) end
|
||||
U.shot(game, out .. "/02-dex-line.png")
|
||||
for _ = 1, 30 do
|
||||
if not battleScreen() then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
end
|
||||
|
||||
-- ---- 2. a level-up that teaches a move ---------------------------------
|
||||
local SPECIES = "CYNDAQUIL"
|
||||
local def = data.pokemon and data.pokemon[SPECIES]
|
||||
local target
|
||||
for _, entry in ipairs((def and def.levelMoves) or {}) do
|
||||
if entry.level and entry.level > 6 and not target then target = entry.level end
|
||||
end
|
||||
if not target then
|
||||
U.log("SKIP 02 -- no level-up move row for " .. SPECIES)
|
||||
else
|
||||
local hero = Mon.new(data, SPECIES, target - 1)
|
||||
local growth = Mon.growthFor(data, def.growthRate)
|
||||
hero.experience = Mon.experienceForLevel(growth, target) - 1
|
||||
-- Only three moves, so LearnMove takes the free slot rather than ForgetMove
|
||||
-- (engine/pokemon/learn.asm:23-29).
|
||||
while hero.moves and #hero.moves > 3 do table.remove(hero.moves) end
|
||||
save.party = { hero }
|
||||
save.inventory = {}
|
||||
U.log(("levelling %s %d -> %d for its %s row")
|
||||
:format(SPECIES, hero.level, target, tostring(target)))
|
||||
|
||||
local prey = Mon.new(data, "PIDGEY", 2)
|
||||
prey.hp = 1
|
||||
assert(world:startBattle({ wild = prey }), "the second battle refused")
|
||||
assert(tapUntil(function()
|
||||
local s = battleScreen()
|
||||
return s ~= nil and s.phase == "menu"
|
||||
end), "the second battle never reached BattleMenu")
|
||||
screen = battleScreen()
|
||||
if screen.menuIndex % 2 == 0 then U.tap(game, "left") U.wait(3) end
|
||||
if screen.menuIndex > 2 then U.tap(game, "up") U.wait(3) end
|
||||
reset()
|
||||
U.tap(game, "a") -- FIGHT
|
||||
U.wait(6)
|
||||
U.tap(game, "a") -- the first move
|
||||
U.wait(240)
|
||||
report("02 level-up move:",
|
||||
"Sfx_DexFanfare5079 twice: \"grew to level\" and \"learned\"")
|
||||
U.shot(game, out .. "/03-level-up.png")
|
||||
for _ = 1, 8 do U.tap(game, "a") U.wait(30) end
|
||||
U.shot(game, out .. "/04-learned.png")
|
||||
end
|
||||
|
||||
Sound.play = realPlay
|
||||
U.log("done -- the controls are yours")
|
||||
end
|
||||
@@ -0,0 +1,196 @@
|
||||
-- #1556: Bill's PC -- the deposit / withdraw / release cry, and the six
|
||||
-- SFX_WRONG refusals (engine/pokemon/bills_pc.asm).
|
||||
--
|
||||
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1556_billspc.lua \
|
||||
-- perl -e 'alarm 420; exec @ARGV' \
|
||||
-- python3 -c "import pty; pty.spawn(['love','.'])"
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1556-billspc (default)
|
||||
--
|
||||
-- Eight moments, each seeded, driven through the real screen and screenshotted.
|
||||
-- The log names what sounded; the ear is the point. DepositPokemon and
|
||||
-- TryWithdrawPokemon both `call PlayMonCry` right after RemoveMonFromPartyOrBox
|
||||
-- (:1762 and :1817), and PlayMonCry itself bails on an EGG
|
||||
-- (home/pokemon.asm:113), so a boxed egg must move in silence.
|
||||
--
|
||||
-- The run ends with the PC screen open so a human takes the controls there.
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Boxes = require("src.core.gen2.Boxes")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local Sound = require("src.core.Sound")
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1556-billspc"
|
||||
|
||||
local heard = {}
|
||||
local realPlay, realCry = Sound.play, Sound.playCry
|
||||
Sound.play = function(data, name)
|
||||
heard[#heard + 1] = name
|
||||
return realPlay(data, name)
|
||||
end
|
||||
Sound.playCry = function(data, species, clip)
|
||||
heard[#heard + 1] = "cry:" .. tostring(species)
|
||||
return realCry(data, species, clip)
|
||||
end
|
||||
|
||||
local function reset() heard = {} end
|
||||
local function report(label, want)
|
||||
U.log(label, #heard > 0 and table.concat(heard, ", ") or "(silence)")
|
||||
U.log(" want:", want)
|
||||
end
|
||||
|
||||
local function tap(button, frames)
|
||||
game.input.pressQueue[#game.input.pressQueue + 1] = button
|
||||
game.input.state[button] = true
|
||||
U.wait(2)
|
||||
game.input.state[button] = false
|
||||
U.wait(frames or 6)
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
local w = game.world
|
||||
assert(w and w.map, "gold world did not boot")
|
||||
local save = game.save
|
||||
local data = game.data
|
||||
|
||||
local function mon(species, level)
|
||||
return Mon.new(data, species, level)
|
||||
end
|
||||
|
||||
local function openBox(mode)
|
||||
Screens.push(game, "Gen2BoxMenu", {
|
||||
save = save, mode = mode,
|
||||
onClose = function() game.stack:pop() end,
|
||||
})
|
||||
U.wait(8)
|
||||
return game.stack:top()
|
||||
end
|
||||
|
||||
local function close()
|
||||
while game.stack:top() and game.stack:top().submenuRows do
|
||||
game.stack:pop()
|
||||
end
|
||||
U.wait(4)
|
||||
end
|
||||
|
||||
local function fillBox(index, n, species)
|
||||
local box = Boxes.box(save, index)
|
||||
for i = #box, 1, -1 do box[i] = nil end
|
||||
for _ = 1, n do box[#box + 1] = mon(species or "RATTATA", 5) end
|
||||
end
|
||||
|
||||
local function setParty(n, species)
|
||||
save.party = {}
|
||||
for _ = 1, n do save.party[#save.party + 1] = mon(species or "CYNDAQUIL", 10) end
|
||||
end
|
||||
|
||||
save.currentBox = 1
|
||||
|
||||
-- ---- 1. WITHDRAW a mon: its cry ------------------------------------------
|
||||
setParty(1)
|
||||
fillBox(1, 1, "PIDGEY")
|
||||
local menu = openBox("withdraw")
|
||||
reset()
|
||||
tap("a") -- the submenu
|
||||
tap("a", 12) -- WITHDRAW
|
||||
report("01 withdraw:", "cry:PIDGEY")
|
||||
U.shot(game, out .. "/01-withdraw-cry.png")
|
||||
tap("a") -- clear anything on screen
|
||||
close()
|
||||
|
||||
-- ---- 2. DEPOSIT it back: the cry again -----------------------------------
|
||||
setParty(3)
|
||||
save.party[#save.party + 1] = mon("PIDGEY", 5)
|
||||
fillBox(1, 0)
|
||||
menu = openBox("deposit")
|
||||
for _ = 1, 3 do tap("down") end
|
||||
reset()
|
||||
tap("a")
|
||||
tap("a", 12) -- DEPOSIT
|
||||
report("02 deposit:", "cry:PIDGEY")
|
||||
U.shot(game, out .. "/02-deposit-cry.png")
|
||||
tap("a")
|
||||
close()
|
||||
|
||||
-- ---- 3. WITHDRAW into a full party: SFX_WRONG (:1845) --------------------
|
||||
setParty(Boxes.PARTY_SIZE)
|
||||
fillBox(1, 1, "PIDGEY")
|
||||
menu = openBox("withdraw")
|
||||
reset()
|
||||
tap("a")
|
||||
tap("a", 12)
|
||||
report("03 withdraw, party full:", "Sfx_Wrong + \"You can't take any more\"")
|
||||
U.shot(game, out .. "/03-party-full.png")
|
||||
tap("a")
|
||||
close()
|
||||
|
||||
-- ---- 4. DEPOSIT into a full box: SFX_WRONG (:1790) ----------------------
|
||||
setParty(3)
|
||||
fillBox(1, Boxes.MONS_PER_BOX)
|
||||
menu = openBox("deposit")
|
||||
reset()
|
||||
tap("a")
|
||||
tap("a", 12)
|
||||
report("04 deposit, box full:", "Sfx_Wrong + \"The BOX is full.\"")
|
||||
U.shot(game, out .. "/04-box-full.png")
|
||||
tap("a")
|
||||
close()
|
||||
|
||||
-- ---- 5. DEPOSIT the last healthy mon: SFX_WRONG -------------------------
|
||||
setParty(2)
|
||||
save.party[2].hp = 0
|
||||
fillBox(1, 0)
|
||||
menu = openBox("deposit")
|
||||
reset()
|
||||
tap("a")
|
||||
tap("a", 12)
|
||||
report("05 deposit, last healthy:", "Sfx_Wrong + \"can't deposit the last\"")
|
||||
U.shot(game, out .. "/05-last-mon.png")
|
||||
tap("a")
|
||||
close()
|
||||
|
||||
-- ---- 6. RELEASE a boxed mon: its cry (:1866) ----------------------------
|
||||
setParty(3)
|
||||
fillBox(1, 1, "SENTRET")
|
||||
menu = openBox("withdraw")
|
||||
reset()
|
||||
tap("select", 8) -- RELEASE asks first
|
||||
tap("up") -- the YES/NO box defaults to NO
|
||||
tap("a", 14)
|
||||
report("06 release:", "cry:SENTRET before \"was released.\"")
|
||||
U.shot(game, out .. "/06-release-cry.png")
|
||||
tap("a")
|
||||
close()
|
||||
|
||||
-- ---- 7. RELEASE an EGG: SFX_WRONG, no cry (:1625) -----------------------
|
||||
setParty(3)
|
||||
fillBox(1, 0)
|
||||
local egg = mon("TOGEPI", 5)
|
||||
egg.isEgg = true
|
||||
Boxes.box(save, 1)[1] = egg
|
||||
menu = openBox("withdraw")
|
||||
reset()
|
||||
tap("select", 10)
|
||||
report("07 release an EGG:", "Sfx_Wrong, and NO cry:TOGEPI")
|
||||
U.shot(game, out .. "/07-release-egg.png")
|
||||
tap("a")
|
||||
close()
|
||||
|
||||
-- ---- 8. MOVE into a full box: SFX_WRONG (:1567) -------------------------
|
||||
setParty(3)
|
||||
fillBox(1, 1, "HOOTHOOT")
|
||||
fillBox(2, Boxes.MONS_PER_BOX)
|
||||
menu = openBox("move")
|
||||
reset()
|
||||
tap("a") -- the MOVE/STATS/CANCEL submenu
|
||||
tap("a", 8) -- MOVE -> the insert cursor
|
||||
tap("right", 8) -- box 2, which is full
|
||||
tap("a", 10)
|
||||
report("08 move into a full box:", "Sfx_Wrong + \"There's no room!\"")
|
||||
U.shot(game, out .. "/08-no-room.png")
|
||||
|
||||
Sound.play, Sound.playCry = realPlay, realCry
|
||||
U.log("done -- the PC is open; the controls are yours")
|
||||
end
|
||||
@@ -0,0 +1,203 @@
|
||||
-- #1556: the bump sound, which the Gen 2 port never wired.
|
||||
--
|
||||
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1556_bump.lua \
|
||||
-- perl -e 'alarm 420; exec @ARGV' \
|
||||
-- python3 -c "import pty; pty.spawn(['love','.'])"
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1556-bump (default)
|
||||
--
|
||||
-- Nothing here can be asserted by a unit test: the deliverable is the sound.
|
||||
-- What the log gives a reader is the RATE, which is the half that is easy to
|
||||
-- get wrong -- .BumpSound is `call CheckSFX / ret c`
|
||||
-- (engine/overworld/player_movement.asm:771), a busy-channel test rather than
|
||||
-- a frame counter, so a held direction re-rings only once the previous Sfx_Bump
|
||||
-- has stopped. A per-frame count means Sound.sfxBusy() is not seeing it.
|
||||
--
|
||||
-- Five moments, in the cart's own terms:
|
||||
-- 1. a wall .CheckLandPerms carry -> .bump (:264-265)
|
||||
-- 2. an NPC .CheckNPC = 0 -> .bump (:267-269)
|
||||
-- 3. a ledge hop .TryJump's carry returns ABOVE .NotMoving (:80-81),
|
||||
-- so the hop is Sfx_JumpOverLedge and NO bump
|
||||
-- 4. a map connection the step is taken; silence
|
||||
-- 5. a map edge with no connection behind it: the border block is a wall
|
||||
--
|
||||
-- The run ends in the overworld so a human takes the controls where it stops.
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Permissions = require("src.world.gen2.Permissions")
|
||||
local Sound = require("src.core.Sound")
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1556-bump"
|
||||
|
||||
local heard = {}
|
||||
local realPlay = Sound.play
|
||||
Sound.play = function(data, name)
|
||||
heard[#heard + 1] = name
|
||||
return realPlay(data, name)
|
||||
end
|
||||
|
||||
local function reset() heard = {} end
|
||||
|
||||
local function report(label)
|
||||
local counts, order = {}, {}
|
||||
for _, name in ipairs(heard) do
|
||||
if not counts[name] then order[#order + 1] = name end
|
||||
counts[name] = (counts[name] or 0) + 1
|
||||
end
|
||||
local parts = {}
|
||||
for _, name in ipairs(order) do
|
||||
parts[#parts + 1] = ("%s x%d"):format(name, counts[name])
|
||||
end
|
||||
U.log(label, #parts > 0 and table.concat(parts, ", ") or "(silence)")
|
||||
return counts
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
local w = game.world
|
||||
assert(w and w.map, "gold world did not boot")
|
||||
|
||||
local function walkable(map, x, y)
|
||||
return map:inBounds(x, y) and map:isWalkable(x, y)
|
||||
end
|
||||
|
||||
local DELTA = { up = { 0, -1 }, down = { 0, 1 },
|
||||
left = { -1, 0 }, right = { 1, 0 } }
|
||||
|
||||
-- A standing cell whose neighbour in `dir` is a wall.
|
||||
local function findWall(map)
|
||||
for y = 0, map.heightCells - 1 do
|
||||
for x = 0, map.widthCells - 1 do
|
||||
if walkable(map, x, y) then
|
||||
for dir, d in pairs(DELTA) do
|
||||
local nx, ny = x + d[1], y + d[2]
|
||||
if map:inBounds(nx, ny) and not map:isWalkable(nx, ny) then
|
||||
return x, y, dir
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- 1. a wall -----------------------------------------------------------
|
||||
assert(w:setMap("PLAYERS_HOUSE_1F", 4, 4, "down"), "setMap PLAYERS_HOUSE_1F")
|
||||
U.wait(5)
|
||||
local wx, wy, wdir = findWall(w.map)
|
||||
if wx then
|
||||
assert(w:setMap("PLAYERS_HOUSE_1F", wx, wy, wdir))
|
||||
U.wait(8)
|
||||
reset()
|
||||
U.hold(game, wdir, 90)
|
||||
U.wait(5)
|
||||
local counts = report("01 wall (" .. wdir .. ", 90 frames held):")
|
||||
U.log(" Sfx_Bump over 90 held frames:", counts.Sfx_Bump or 0,
|
||||
"-- a per-frame count here means the CheckSFX gate is not working")
|
||||
U.shot(game, out .. "/01-wall.png")
|
||||
else
|
||||
U.log("SKIP 01 wall -- no wall cell found in PLAYERS_HOUSE_1F")
|
||||
end
|
||||
|
||||
-- ---- 2. an NPC -----------------------------------------------------------
|
||||
local npc, npcDir
|
||||
for _, e in ipairs(w.entities or {}) do
|
||||
if e ~= w.player and e.cellX and not e.bigObject then
|
||||
for dir, d in pairs(DELTA) do
|
||||
local sx, sy = e.cellX - d[1], e.cellY - d[2]
|
||||
if walkable(w.map, sx, sy) then npc, npcDir = { sx, sy }, dir break end
|
||||
end
|
||||
end
|
||||
if npc then break end
|
||||
end
|
||||
if npc then
|
||||
assert(w:setMap("PLAYERS_HOUSE_1F", npc[1], npc[2], npcDir))
|
||||
U.wait(8)
|
||||
reset()
|
||||
U.hold(game, npcDir, 45)
|
||||
U.wait(5)
|
||||
report("02 NPC (" .. npcDir .. "):")
|
||||
U.shot(game, out .. "/02-npc.png")
|
||||
else
|
||||
U.log("SKIP 02 NPC -- no reachable NPC in PLAYERS_HOUSE_1F")
|
||||
end
|
||||
|
||||
-- ---- 3. a ledge ----------------------------------------------------------
|
||||
local ROUTES = { "ROUTE_29", "ROUTE_30", "ROUTE_31", "ROUTE_32",
|
||||
"ROUTE_33", "ROUTE_34", "ROUTE_35", "ROUTE_36" }
|
||||
local hopped = false
|
||||
for _, mapId in ipairs(ROUTES) do
|
||||
if w:setMap(mapId, 5, 5, "down") then
|
||||
U.wait(5)
|
||||
local map = w.map
|
||||
for y = 0, map.heightCells - 1 do
|
||||
for x = 0, map.widthCells - 1 do
|
||||
local facings = Permissions.ledgeFacings(map:cellCollision(x, y))
|
||||
if facings then
|
||||
for dir, on in pairs(facings) do
|
||||
local d = on and DELTA[dir]
|
||||
if d and walkable(map, x + d[1] * 2, y + d[2] * 2) then
|
||||
assert(w:setMap(mapId, x, y, dir))
|
||||
U.wait(8)
|
||||
reset()
|
||||
U.hold(game, dir, 40)
|
||||
U.wait(20)
|
||||
local counts = report(
|
||||
("03 ledge hop (%s %s):"):format(mapId, dir))
|
||||
U.log(" want Sfx_JumpOverLedge and NO Sfx_Bump; bumps:",
|
||||
counts.Sfx_Bump or 0)
|
||||
U.shot(game, out .. "/03-ledge.png")
|
||||
hopped = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if hopped then break end
|
||||
end
|
||||
if hopped then break end
|
||||
end
|
||||
end
|
||||
if hopped then break end
|
||||
end
|
||||
if not hopped then U.log("SKIP 03 ledge -- no hoppable ledge found") end
|
||||
|
||||
-- ---- 4 and 5. map edges --------------------------------------------------
|
||||
assert(w:setMap("NEW_BARK_TOWN", 5, 5, "down"), "setMap NEW_BARK_TOWN")
|
||||
U.wait(5)
|
||||
local DIR_CONN = { up = "north", down = "south", left = "west",
|
||||
right = "east" }
|
||||
local map = w.map
|
||||
local function edgeCell(dir)
|
||||
local d = DELTA[dir]
|
||||
for y = 0, map.heightCells - 1 do
|
||||
for x = 0, map.widthCells - 1 do
|
||||
if walkable(map, x, y)
|
||||
and not map:inBounds(x + d[1], y + d[2]) then
|
||||
return x, y
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, dir in ipairs({ "up", "down", "left", "right" }) do
|
||||
local conn = map:connection(DIR_CONN[dir])
|
||||
local ex, ey = edgeCell(dir)
|
||||
if ex then
|
||||
assert(w:setMap("NEW_BARK_TOWN", ex, ey, dir))
|
||||
U.wait(8)
|
||||
reset()
|
||||
U.hold(game, dir, 40)
|
||||
U.wait(20)
|
||||
local counts = report(("%s edge %s (connection: %s):")
|
||||
:format(conn and "04" or "05", dir,
|
||||
tostring(conn and conn.mapId or "none")))
|
||||
U.log(" want", conn and "silence" or "Sfx_Bump", "-- bumps:",
|
||||
counts.Sfx_Bump or 0)
|
||||
U.shot(game, out .. ("/0%s-edge-%s.png"):format(conn and "4" or "5", dir))
|
||||
assert(w:setMap("NEW_BARK_TOWN", ex, ey, dir))
|
||||
U.wait(5)
|
||||
end
|
||||
end
|
||||
|
||||
Sound.play = realPlay
|
||||
assert(w:setMap("NEW_BARK_TOWN", 5, 5, "down"))
|
||||
U.log("done -- the controls are yours; walk into anything")
|
||||
end
|
||||
@@ -0,0 +1,207 @@
|
||||
-- #1556: the field jingles that ride a TX_SOUND at the end of a string --
|
||||
-- RARE CANDY's level-up, "X learned MOVE!", the trophy -- plus the TM/HM
|
||||
-- refusals, which are `call PlaySFX` next to the message instead.
|
||||
--
|
||||
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1556_fanfare.lua \
|
||||
-- perl -e 'alarm 420; exec @ARGV' \
|
||||
-- python3 -c "import pty; pty.spawn(['love','.'])"
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1556-fanfare (default)
|
||||
--
|
||||
-- The one deliberate SILENCE is moment 4: TeachTMHM's `.compatible` arm is
|
||||
-- `callfar KnowsMove / jr c, .nope` (engine/items/tmhm.asm:139-140) with no
|
||||
-- PlaySFX at all, so "X already knows MOVE!" must stay mute. Only the
|
||||
-- INCOMPATIBLE arm rings (:131).
|
||||
--
|
||||
-- The box is supposed to HOLD for the fanfare before it takes a button:
|
||||
-- TextCommand_SOUND is PlaySFX then WaitSFX (home/text.asm), which
|
||||
-- TextBox.soundOpts models with auto.wait.
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local PackMenu = require("src.ui.gen2.PackMenu")
|
||||
local Sound = require("src.core.Sound")
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1556-fanfare"
|
||||
|
||||
local heard = {}
|
||||
local realPlay = Sound.play
|
||||
Sound.play = function(data, name)
|
||||
heard[#heard + 1] = name
|
||||
return realPlay(data, name)
|
||||
end
|
||||
local function reset() heard = {} end
|
||||
local function report(label, want)
|
||||
U.log(label, #heard > 0 and table.concat(heard, ", ") or "(silence)")
|
||||
U.log(" want:", want)
|
||||
end
|
||||
|
||||
local function tap(button, frames)
|
||||
game.input.pressQueue[#game.input.pressQueue + 1] = button
|
||||
game.input.state[button] = true
|
||||
U.wait(2)
|
||||
game.input.state[button] = false
|
||||
U.wait(frames or 6)
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
local w = game.world
|
||||
assert(w and w.map, "gold world did not boot")
|
||||
local save, data = game.save, game.data
|
||||
|
||||
local SPECIES = "CYNDAQUIL"
|
||||
local def = data.pokemon and data.pokemon[SPECIES]
|
||||
assert(def, "the cache carries no " .. SPECIES)
|
||||
|
||||
-- Which TMs this species may and may not learn, straight off its BASE_TMHM.
|
||||
local canLearn, cannotLearn = nil, nil
|
||||
local allowed = {}
|
||||
for _, moveId in ipairs(def.tmhm or {}) do allowed[moveId] = true end
|
||||
for itemId, item in pairs(data.items or {}) do
|
||||
if item.teaches and tostring(itemId):sub(1, 3) == "TM_" then
|
||||
if allowed[item.teaches] then
|
||||
canLearn = canLearn or itemId
|
||||
else
|
||||
cannotLearn = cannotLearn or itemId
|
||||
end
|
||||
end
|
||||
end
|
||||
U.log("TM that fits:", tostring(canLearn),
|
||||
"-- TM that does not:", tostring(cannotLearn))
|
||||
|
||||
local function seed()
|
||||
local mon = Mon.new(data, SPECIES, 12)
|
||||
save.party = { mon }
|
||||
return mon
|
||||
end
|
||||
|
||||
local function openPack()
|
||||
local pack = PackMenu.new(game, {
|
||||
save = save, world = w, onClose = function() end })
|
||||
game.stack:push(pack)
|
||||
U.wait(6)
|
||||
return pack
|
||||
end
|
||||
|
||||
local field = game.overworld or game.stack:top()
|
||||
local function popToField()
|
||||
for _ = 1, 12 do
|
||||
if game.stack:top() == field or not game.stack:top() then break end
|
||||
game.stack:pop()
|
||||
U.wait(2)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- 1. RARE CANDY: "X grew to level N!" + the level-up fanfare ---------
|
||||
local mon = seed()
|
||||
save.inventory = { RARE_CANDY = 2 }
|
||||
reset()
|
||||
game:usePartyItem("RARE_CANDY")
|
||||
U.wait(8)
|
||||
tap("a", 30) -- the party pick
|
||||
report("01 rare candy:", "Sfx_DexFanfare5079 under \"grew to level 13!\"")
|
||||
U.shot(game, out .. "/01-rare-candy.png")
|
||||
tap("a", 10)
|
||||
popToField()
|
||||
|
||||
-- ---- 2. a TM the mon CAN learn: "X learned MOVE!" + the fanfare ---------
|
||||
if canLearn then
|
||||
mon = seed()
|
||||
save.inventory = { [canLearn] = 1 }
|
||||
local pack = openPack()
|
||||
reset()
|
||||
pack:openTeachParty({ id = canLearn })
|
||||
U.wait(10)
|
||||
tap("a", 30) -- the party pick
|
||||
report("02 TM " .. canLearn .. ":",
|
||||
"Sfx_DexFanfare5079 under \"learned ...!\"")
|
||||
U.shot(game, out .. "/02-tm-learned.png")
|
||||
tap("a", 10)
|
||||
popToField()
|
||||
else
|
||||
U.log("SKIP 02 -- no TM in this cache that " .. SPECIES .. " can learn")
|
||||
end
|
||||
|
||||
-- ---- 3. a TM the mon CANNOT learn: SFX_WRONG (tmhm.asm:131) ------------
|
||||
if cannotLearn then
|
||||
mon = seed()
|
||||
save.inventory = { [cannotLearn] = 1 }
|
||||
local pack = openPack()
|
||||
reset()
|
||||
pack:openTeachParty({ id = cannotLearn })
|
||||
U.wait(10)
|
||||
tap("a", 20)
|
||||
report("03 TM " .. cannotLearn .. ":",
|
||||
"Sfx_Wrong under \"can't learn ...!\"")
|
||||
U.shot(game, out .. "/03-tm-refused.png")
|
||||
tap("a", 10)
|
||||
popToField()
|
||||
else
|
||||
U.log("SKIP 03 -- every TM in this cache fits " .. SPECIES)
|
||||
end
|
||||
|
||||
-- ---- 4. a TM the mon ALREADY KNOWS: SILENCE ----------------------------
|
||||
if canLearn then
|
||||
mon = seed()
|
||||
local moveId = data.items[canLearn].teaches
|
||||
mon.moves = mon.moves or {}
|
||||
mon.moves[1] = { id = moveId, pp = 10, maxPp = 10 }
|
||||
save.inventory = { [canLearn] = 1 }
|
||||
local pack = openPack()
|
||||
reset()
|
||||
pack:openTeachParty({ id = canLearn })
|
||||
U.wait(10)
|
||||
tap("a", 20)
|
||||
report("04 TM already known:", "SILENCE -- KnowsMove has no PlaySFX")
|
||||
U.shot(game, out .. "/04-tm-already-known.png")
|
||||
tap("a", 10)
|
||||
popToField()
|
||||
end
|
||||
|
||||
-- ---- 5. a TM on an EGG: SFX_WRONG (tmhm.asm:104-111), a regression -----
|
||||
if canLearn then
|
||||
mon = seed()
|
||||
mon.isEgg = true
|
||||
save.inventory = { [canLearn] = 1 }
|
||||
local pack = openPack()
|
||||
reset()
|
||||
pack:openTeachParty({ id = canLearn })
|
||||
U.wait(10)
|
||||
tap("a", 20)
|
||||
report("05 TM on an EGG:", "Sfx_Wrong, and the list stays up")
|
||||
U.shot(game, out .. "/05-tm-egg.png")
|
||||
tap("b", 6)
|
||||
popToField()
|
||||
end
|
||||
|
||||
-- ---- 6. the trophy box: _SentTrophyHomeText's fanfare ------------------
|
||||
if data.items and data.items.NORMAL_BOX then
|
||||
seed()
|
||||
save.inventory = { NORMAL_BOX = 1 }
|
||||
save.decorations = save.decorations or {}
|
||||
local pack = openPack()
|
||||
pack:rebuild()
|
||||
local row
|
||||
for index, entry in ipairs(pack.rows) do
|
||||
if entry.id == "NORMAL_BOX" then row = index end
|
||||
end
|
||||
if row then
|
||||
pack.index = row
|
||||
reset()
|
||||
pack:useSelected()
|
||||
U.wait(20)
|
||||
report("06 trophy box:", "Sfx_DexFanfare5079 with the trophy line")
|
||||
U.shot(game, out .. "/06-trophy.png")
|
||||
tap("a", 6)
|
||||
else
|
||||
U.log("SKIP 06 -- the NORMAL BOX is not in the ITEM pocket here")
|
||||
end
|
||||
popToField()
|
||||
else
|
||||
U.log("SKIP 06 -- this cache has no NORMAL_BOX")
|
||||
end
|
||||
|
||||
Sound.play = realPlay
|
||||
U.log("done -- the controls are yours")
|
||||
end
|
||||
@@ -0,0 +1,105 @@
|
||||
-- #1567: the TM/HM pocket's row order.
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1567_test.lua love .
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1567 (default)
|
||||
--
|
||||
-- Nothing here can be asserted: the bug IS the order rows land in.
|
||||
-- TMHM_DisplayPocketItems walks the fixed 57-byte wTMsHMs array from 1 to 57
|
||||
-- and prints every non-zero slot (engine/items/tmhm.asm:341), so the cart's
|
||||
-- pocket is always TM01..TM50 then HM01..HM07 no matter what the player picked
|
||||
-- up first. The port drew it from the acquisition-ordered bag list instead.
|
||||
--
|
||||
-- The seed is deliberately scrambled -- gold_bug1425_test.lua seeds its TMs
|
||||
-- already in numeric order, which is why its screenshots never showed this.
|
||||
--
|
||||
-- The run ends with the PACK still open on the TM pocket, so a human takes the
|
||||
-- controls exactly where the screenshots stop.
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Bag = require("src.inventory.Bag")
|
||||
local PackMenu = require("src.ui.gen2.PackMenu")
|
||||
|
||||
-- Picked up back to front: TM/HM numbers 57, 50, 51, 5, 1, 53. TM_ROAR is the
|
||||
-- one on the far side of the ITEM_C3 hole (id $c4, number 5), so it also pins
|
||||
-- that the number and not the item id is what sorts.
|
||||
local SEED = {
|
||||
{ "HM_WATERFALL", 1 },
|
||||
{ "TM_NIGHTMARE", 2 },
|
||||
{ "HM_CUT", 1 },
|
||||
{ "TM_ROAR", 3 },
|
||||
{ "TM_DYNAMICPUNCH", 12 },
|
||||
{ "HM_SURF", 1 },
|
||||
}
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1567"
|
||||
|
||||
local function shot(name)
|
||||
U.wait(3)
|
||||
U.shot(game, ("%s/%s.png"):format(out, name))
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
assert(game.world and game.world.map, "gold world did not boot")
|
||||
|
||||
local save = game.save
|
||||
save.inventory = {}
|
||||
save.bagOrder = {}
|
||||
for _, entry in ipairs(SEED) do
|
||||
local id, count = entry[1], entry[2]
|
||||
if not (game.data.items and game.data.items[id]) then
|
||||
U.log("[driver] SKIP", id, "-- not in this cache")
|
||||
else
|
||||
save.inventory[id] = count
|
||||
table.insert(Bag.order(save), id)
|
||||
end
|
||||
end
|
||||
-- A POTION and a SUPER POTION so the ITEM pocket can be compared against the
|
||||
-- TM pocket at the end: SELECT must still arm a row over there.
|
||||
for _, entry in ipairs({ { "POTION", 5 }, { "SUPER_POTION", 2 } }) do
|
||||
if game.data.items and game.data.items[entry[1]] then
|
||||
save.inventory[entry[1]] = entry[2]
|
||||
table.insert(Bag.order(save), entry[1])
|
||||
end
|
||||
end
|
||||
|
||||
U.log("[driver] pickup order:", table.concat(Bag.order(save), " "))
|
||||
|
||||
local pack = PackMenu.new(game, { save = save, world = game.world,
|
||||
onClose = function() end })
|
||||
game.stack:push(pack)
|
||||
|
||||
shot("00-items") -- POTION over SUPER POTION, pickup order
|
||||
U.tap(game, "right")
|
||||
U.tap(game, "right")
|
||||
U.tap(game, "right")
|
||||
-- TM01 DYNAMICPUNCH ×12, TM05 ROAR ×03, TM50 NIGHTMARE ×02, then HM01 CUT,
|
||||
-- HM03 SURF, HM07 WATERFALL with no ×NN at all (engine/items/tmhm.asm:390).
|
||||
-- Before the fix this read HM07, TM50, HM01, TM05, TM01, HM03.
|
||||
shot("01-tmhm-numbered")
|
||||
|
||||
local order = {}
|
||||
for i = 1, #pack.rows do order[i] = pack.rows[i].id end
|
||||
U.log("[driver] TM/HM rows:", table.concat(order, " "))
|
||||
|
||||
-- engine/items/tmhm.asm:207 filters SELECT out of this pocket: no hollow ▷,
|
||||
-- no "Where should this be moved to?".
|
||||
U.tap(game, "select")
|
||||
shot("02-tmhm-select-ignored")
|
||||
U.tap(game, "down")
|
||||
U.tap(game, "select")
|
||||
shot("03-tmhm-select-ignored-row2")
|
||||
|
||||
-- The ITEM pocket, where SELECT still arms (engine/items/pack.asm:1290).
|
||||
U.tap(game, "right")
|
||||
U.tap(game, "select")
|
||||
shot("04-items-select-arms")
|
||||
U.tap(game, "b")
|
||||
|
||||
-- Back to the TM pocket for the human.
|
||||
U.tap(game, "right")
|
||||
U.tap(game, "right")
|
||||
U.tap(game, "right")
|
||||
U.log("[driver] shots in " .. out .. " -- the PACK is yours")
|
||||
end
|
||||
@@ -0,0 +1,197 @@
|
||||
-- #1513: exp credit has to be per THIS appearance of the enemy's active mon.
|
||||
-- ResetBattleParticipants falls through into AddBattleParticipant
|
||||
-- (engine/battle/core.asm:3033 and :3037), and AI_Switch farcalls it right
|
||||
-- after EnemySwitch (engine/battle/ai/items.asm:697), so the moment the rival
|
||||
-- rotates, the only mon still credited is the one the player has on the field.
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 POKEPORT_IDENTITY=gold-dev \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_exp_participants_bug1513.lua love .
|
||||
--
|
||||
-- What to look for, in order:
|
||||
-- 00 CYNDAQUIL is out against JOEY's GEODUDE.
|
||||
-- 01 TOTODILE has been switched in through the real party menu, so BOTH
|
||||
-- party mons have now "met" GEODUDE.
|
||||
-- 02 "JOEY withdrew GEODUDE!" -- the rotation the ticket is about.
|
||||
-- 03 the exp lines for the KO of the mon that came IN (PIDGEY). Only
|
||||
-- TOTODILE may be named here. A "CYNDAQUIL gained N EXP. Points!" line
|
||||
-- in this shot is the bug.
|
||||
-- 04 the party screen: CYNDAQUIL's level and EXP bar are pixel-identical to
|
||||
-- shot 01's, TOTODILE's have moved.
|
||||
-- 05 GEODUDE comes back out and is KO'd. CYNDAQUIL still gains nothing --
|
||||
-- that half is correct cart behavior (ram/wram.asm:775-776, "All bits
|
||||
-- cleared if the enemy faints") and must NOT be "fixed".
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
|
||||
local function battleScreen(game)
|
||||
for _ = 1, 900 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then return top end
|
||||
U.wait(1)
|
||||
end
|
||||
error("battle screen never came up")
|
||||
end
|
||||
|
||||
local function runToPhase(game, screen, phase, frames)
|
||||
for _ = 1, (frames or 600) do
|
||||
if screen.phase == phase then return true end
|
||||
if screen.battle.over then return false end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Page the queue out, shooting the first box that carries `needle`.
|
||||
local function pageUntilMenu(game, screen, out, needles)
|
||||
local shot = {}
|
||||
for _ = 1, 1200 do
|
||||
local message = screen.message
|
||||
if message then
|
||||
for needle, path in pairs(needles) do
|
||||
if not shot[needle] and message:find(needle, 1, true) then
|
||||
U.shot(game, out .. "/" .. path)
|
||||
shot[needle] = message
|
||||
end
|
||||
end
|
||||
end
|
||||
if screen.phase == "menu" or screen.phase == "done"
|
||||
or screen.battle.over then
|
||||
break
|
||||
end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
return shot
|
||||
end
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1513"
|
||||
U.wait(45)
|
||||
local world = game.world
|
||||
assert(world and world.map, "gold world did not boot")
|
||||
local failures = {}
|
||||
local function check(ok, what)
|
||||
print((ok and "[ok] " or "[FAIL] ") .. what)
|
||||
if not ok then failures[#failures + 1] = what end
|
||||
end
|
||||
|
||||
local lead = Mon.new(game.data, "CYNDAQUIL", 20)
|
||||
lead.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||
local bench = Mon.new(game.data, "TOTODILE", 20)
|
||||
bench.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||
game.save.party = { lead, bench }
|
||||
|
||||
local foe1 = Mon.new(game.data, "GEODUDE", 8)
|
||||
foe1.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||
local foe2 = Mon.new(game.data, "PIDGEY", 8)
|
||||
foe2.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
||||
-- attributes[6] is the low byte of TRNATTR_AI_ITEM_SWITCH: OFTEN
|
||||
-- (src/battle/gen2/Ai.lua:1307). The real Rival1 class is SWITCH_SOMETIMES
|
||||
-- (pokegold:data/trainers/attributes.asm), the same path at a lower roll.
|
||||
assert(world:startBattle({ trainer = { class = "YOUNGSTER", name = "JOEY",
|
||||
party = { foe1, foe2 }, attributes = { 0, 0, 0, 0, 0, 0x01, 0 } } }),
|
||||
"trainer startBattle failed")
|
||||
local screen = battleScreen(game)
|
||||
assert(runToPhase(game, screen, "menu", 400), "never reached the menu")
|
||||
U.shot(game, out .. "/00-lead-out.png")
|
||||
|
||||
------------------------------------------------- the player's own switch
|
||||
-- Through the real party menu, so the human sees the same path they used.
|
||||
screen:chooseMenu("party")
|
||||
for _ = 1, 240 do
|
||||
if screen.battle.player == bench then break end
|
||||
local top = game.stack:top()
|
||||
if top ~= screen then
|
||||
U.tap(game, "down")
|
||||
U.wait(3)
|
||||
U.tap(game, "a")
|
||||
U.wait(6)
|
||||
U.tap(game, "a")
|
||||
U.wait(6)
|
||||
else
|
||||
U.wait(2)
|
||||
end
|
||||
end
|
||||
check(screen.battle.player == bench, "TOTODILE was switched in")
|
||||
check(screen.battle.participants[1] and screen.battle.participants[2],
|
||||
"both party mons are credited against GEODUDE")
|
||||
pageUntilMenu(game, screen, out, {})
|
||||
U.shot(game, out .. "/01-switched.png")
|
||||
print(("[driver] before the rotation: cyndaquil %d totodile %d")
|
||||
:format(lead.experience, bench.experience))
|
||||
local leadExp = lead.experience
|
||||
local benchExp = bench.experience
|
||||
|
||||
------------------------------------------------------- the AI's rotation
|
||||
-- Perish Song at one turn left is CheckAbleToSwitch's maximum score, the
|
||||
-- cheapest way to make the AI commit to the rotation on demand.
|
||||
screen.battle:volatile(screen.battle.enemy).perish = 1
|
||||
screen:submit({ kind = "move", move = "TACKLE" })
|
||||
local seen = pageUntilMenu(game, screen, out, {
|
||||
["withdrew"] = "02-rival-withdrew.png",
|
||||
["gained"] = "03-exp-lines.png",
|
||||
})
|
||||
check(seen["withdrew"] ~= nil, "JOEY withdrew GEODUDE")
|
||||
check(screen.battle.enemy == foe2, "and sent PIDGEY out")
|
||||
check(screen.battle.participants[1] == nil,
|
||||
"CYNDAQUIL lost its credit when the rival rotated")
|
||||
check(screen.battle.participants[2] == true,
|
||||
"and TOTODILE, the mon on the field, kept it")
|
||||
|
||||
------------------------------------------------------------ the KO payout
|
||||
if not screen.battle.over and (foe2.hp or 0) > 0 then
|
||||
if screen.phase ~= "menu" then
|
||||
runToPhase(game, screen, "menu", 400)
|
||||
end
|
||||
foe2.hp = 1
|
||||
screen:submit({ kind = "move", move = "TACKLE" })
|
||||
local paid = pageUntilMenu(game, screen, out, {
|
||||
["gained"] = "03-exp-lines.png",
|
||||
})
|
||||
if paid["gained"] then print("[driver] exp box: " .. paid["gained"]) end
|
||||
end
|
||||
print(("[driver] after the PIDGEY KO: cyndaquil %d totodile %d")
|
||||
:format(lead.experience, bench.experience))
|
||||
check(lead.experience == leadExp,
|
||||
"CYNDAQUIL earned NOTHING from the mon it never faced")
|
||||
check(bench.experience > benchExp, "TOTODILE was paid for it")
|
||||
|
||||
if screen.phase == "menu" then
|
||||
screen:chooseMenu("party")
|
||||
U.wait(20)
|
||||
U.shot(game, out .. "/04-party-exp.png")
|
||||
U.tap(game, "b")
|
||||
U.wait(10)
|
||||
if game.stack:top() ~= screen then
|
||||
U.tap(game, "b")
|
||||
U.wait(10)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------- GEODUDE comes back and faints
|
||||
-- Correct cart behavior: no residual credit for a previous appearance.
|
||||
leadExp = lead.experience
|
||||
benchExp = bench.experience
|
||||
if not screen.battle.over then
|
||||
runToPhase(game, screen, "menu", 600)
|
||||
if screen.phase == "menu" and screen.battle.enemy == foe1 then
|
||||
foe1.hp = 1
|
||||
screen:submit({ kind = "move", move = "TACKLE" })
|
||||
pageUntilMenu(game, screen, out, { ["gained"] = "05-geodude-ko.png" })
|
||||
print(("[driver] after the GEODUDE KO: cyndaquil %d totodile %d")
|
||||
:format(lead.experience, bench.experience))
|
||||
check(lead.experience == leadExp,
|
||||
"CYNDAQUIL still earns nothing when GEODUDE comes back (correct)")
|
||||
check(bench.experience > benchExp, "and TOTODILE takes the whole share")
|
||||
else
|
||||
print("[driver] GEODUDE never came back out; second half skipped")
|
||||
end
|
||||
end
|
||||
|
||||
if #failures > 0 then
|
||||
error(#failures .. " exp participant checks failed")
|
||||
end
|
||||
print("[driver] #1513 fixed; shots in " .. out)
|
||||
end
|
||||
@@ -221,6 +221,13 @@ return function(game)
|
||||
stored[i] = mon(species, 10 + i)
|
||||
end
|
||||
Boxes.rename(save, 2, "GRASS")
|
||||
-- $5d for a held item, $5c for MAIL (engine/pokemon/bills_pc.asm:1079-1094).
|
||||
-- A boxed mon can never hold mail, so the letter goes on a party mon.
|
||||
stored[1].item = "BERRY"
|
||||
save.party[2].item = "FLOWER_MAIL"
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
Mail.set(save, 2, Mail.entry("FLOWER_MAIL", "HI THERE!",
|
||||
save.player.name or "GOLD", save.player.id or 0, save.party[2].species))
|
||||
|
||||
local pc = PcMenu.new(game, { save = save })
|
||||
show("20-pc-menu", pc)
|
||||
@@ -228,12 +235,22 @@ return function(game)
|
||||
pc.pickIndex = 2
|
||||
show("21-pc-changebox", pc)
|
||||
|
||||
show("22-pc-withdraw", BoxMenu.new(game, {
|
||||
save = save, mode = "withdraw",
|
||||
}))
|
||||
show("23-pc-deposit", BoxMenu.new(game, {
|
||||
save = save, mode = "deposit",
|
||||
}))
|
||||
-- Browsing paints the 7x7 block in BillsPCOrangePalette and only .PrepSubmenu
|
||||
-- swaps the mon's colours in (engine/pokemon/bills_pc.asm:305-309, :356-369,
|
||||
-- engine/gfx/cgb_layouts.asm:284-289); the icon is BG palette 0 either way.
|
||||
local wd = BoxMenu.new(game, { save = save, mode = "withdraw" })
|
||||
show("22-pc-withdraw", wd)
|
||||
wd.index = wd:total() -- CANCEL
|
||||
wd:ensureVisible()
|
||||
show("22b-pc-withdraw-cancel", wd)
|
||||
wd.index = 1
|
||||
wd:ensureVisible()
|
||||
wd.phase = "submenu"
|
||||
show("22c-pc-withdraw-submenu", wd)
|
||||
local dep = BoxMenu.new(game, { save = save, mode = "deposit" })
|
||||
show("23-pc-deposit", dep)
|
||||
dep.index = 2 -- the mon holding FLOWER MAIL
|
||||
show("23b-pc-deposit-mail", dep)
|
||||
|
||||
-- The two clock screens NEW GAME and Mom open (timeset.asm InitClock and
|
||||
-- SetDayOfWeek), each at its picker rather than at its opening page.
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
-- Driver: #1517, a Gen 2 stat block in a Gen 1 party.
|
||||
--
|
||||
-- Gen 1's party_struct carries one Special word (macros/ram.asm:28-37) and
|
||||
-- PrintStatsBox reads four fixed cells, wLoadedMonAttack/Defense/Speed/Special
|
||||
-- (engine/pokemon/status_screen.asm:255-296). Gen 2 splits that word into
|
||||
-- SpclAtk/SpclDef (pokegold macros/ram.asm:29-42), so a mod that wrote a Gen 2
|
||||
-- block over a Yellow party leaves the port with no `special` to print.
|
||||
--
|
||||
-- cp -R ~/Library/Application\ Support/LOVE/pokemon-love2d/yellow \
|
||||
-- ~/Library/Application\ Support/LOVE/bug1517/yellow
|
||||
-- POKEPORT_VERSION=yellow POKEPORT_GAME=yellow POKEPORT_IDENTITY=bug1517 \
|
||||
-- POKEPORT_TOUCH=0 POKEPORT_SPEED=8 SHOT_DIR=/tmp/bug1517 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/summary_stats_bug1517_test.lua love .
|
||||
--
|
||||
-- The copy is not optional: a fresh POKEPORT_IDENTITY has no cache and the
|
||||
-- boot hangs with no output.
|
||||
--
|
||||
-- Pre-fix the LOVE window vanishes the moment STATS is chosen, with no error
|
||||
-- screen and nothing on stdout (that silence is the other half of #1517).
|
||||
-- Post-fix the status screen opens and SPECIAL reads the CalcStats value
|
||||
-- logged below. The run ends on the open screen, so a human takes the
|
||||
-- controls at exactly the frame the reporter's build died on.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/bug1517"
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Stats = require("src.pokemon.Stats")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local SummaryMenu = require("src.ui.SummaryMenu")
|
||||
|
||||
local function top() return game.stack:top() end
|
||||
|
||||
U.newGame(game)
|
||||
U.wait(20)
|
||||
|
||||
-- BUTTERFREE because the reporter's party led with one, and CONFUSION is a
|
||||
-- special move, so the same block would have crashed Damage.applyStage.
|
||||
local species = game.data.pokemon.BUTTERFREE and "BUTTERFREE" or "PIDGEY"
|
||||
local def = game.data.pokemon[species]
|
||||
local mon = Pokemon.new(game.data, species, 21)
|
||||
local want = Stats.calc(def, 21, mon.dvs, mon.statExp)
|
||||
|
||||
-- the shape src/battle/gen2/Mon.lua Mon.stats produces: no `special`
|
||||
mon.stats = {
|
||||
hp = want.hp, attack = want.attack, defense = want.defense,
|
||||
speed = want.speed, specialAttack = want.special, specialDefense = want.special,
|
||||
}
|
||||
mon.hp = mon.stats.hp
|
||||
game.save.party = { mon }
|
||||
U.log(("[1517] fixture: %s :L21, stats.special = %s, CalcStats says %d")
|
||||
:format(species, tostring(mon.stats.special), want.special))
|
||||
|
||||
SaveData.validate(game.save, game.data)
|
||||
local after = game.save.party[1].stats
|
||||
U.log("[1517] after SaveData.validate, special = " .. tostring(after.special)
|
||||
.. ", specialAttack = " .. tostring(after.specialAttack))
|
||||
U.log("[1517] the repair layer is SaveData.validate -> scrubKnownMon -> "
|
||||
.. "Stats.ensure; a nil above means it did not run")
|
||||
|
||||
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
|
||||
U.wait(10)
|
||||
|
||||
local function cursorTo(menu, field, wanted)
|
||||
for _ = 1, 40 do
|
||||
if not menu or menu[field] == wanted then return menu and menu[field] == wanted end
|
||||
U.tap(game, menu[field] < wanted and "down" or "up")
|
||||
U.wait(3)
|
||||
end
|
||||
return menu[field] == wanted
|
||||
end
|
||||
|
||||
U.tap(game, "start")
|
||||
U.wait(10)
|
||||
local menu = top()
|
||||
if not (menu and menu.screenId == "StartMenu") then
|
||||
U.log("[1517] FAIL no start menu; top = " .. tostring(menu and menu.screenId))
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
local row
|
||||
for i, it in ipairs(menu.items or {}) do
|
||||
if it.label == "POKéMON" then row = i break end
|
||||
end
|
||||
if not (row and cursorTo(menu, "index", row)) then
|
||||
U.log("[1517] FAIL could not reach the POKéMON row")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
|
||||
local party = top()
|
||||
if not (party and party.screenId == "PartyMenu") then
|
||||
U.log("[1517] FAIL no party menu; top = " .. tostring(party and party.screenId))
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
U.shot(game, DIR .. "/01-party-list.png")
|
||||
U.tap(game, "a")
|
||||
U.wait(8)
|
||||
local subRow
|
||||
for i, it in ipairs(party.subItems or {}) do
|
||||
if it.action == "stats" then subRow = i break end
|
||||
end
|
||||
if not (subRow and cursorTo(party, "subIndex", subRow)) then
|
||||
U.log("[1517] FAIL could not reach the STATS row")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
U.shot(game, DIR .. "/02-stats-selected.png")
|
||||
|
||||
U.log("[1517] opening STATS -- pre-fix the app dies here, silently")
|
||||
U.tap(game, "a")
|
||||
U.wait(30)
|
||||
|
||||
local screen = top()
|
||||
local opened = getmetatable(screen) == SummaryMenu or screen.screenId == "SummaryMenu"
|
||||
U.log("[1517] SURVIVED, top = " .. tostring(screen and screen.screenId))
|
||||
U.shot(game, DIR .. "/03-status-screen.png")
|
||||
U.log(("[1517] %s: SPECIAL on screen must read %d")
|
||||
:format(opened and "read it off the frame" or "WRONG SCREEN", want.special))
|
||||
U.log("[1517] shots in " .. DIR .. " -- the status screen is yours")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
Reference in New Issue
Block a user