mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 03:35:56 +02:00
CLOSES #1396, CLOSES #1398, CLOSES #1400, CLOSES #1401, CLOSES #1406, CLOSES #1407, CLOSES #1411, CLOSES #1413, CLOSES #1415, CLOSES #1416, CLOSES #1417, CLOSES #1419, CLOSES #1421, CLOSES #1422, CLOSES #1423, CLOSES #1424, CLOSES #1425, CLOSES #1427, CLOSES #1428, CLOSES #1429, CLOSES #1431, CLOSES #1432, CLOSES #1433, CLOSES #1435, CLOSES #1437, CLOSES #1440, CLOSES #1441, CLOSES #1442, CLOSES #1443, CLOSES #1447, CLOSES #1449, CLOSES #1456, CLOSES #1464, CLOSES #1465, CLOSES #1468, CLOSES #1469, CLOSES #1470
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
-- Driver: Fly / Teleport must take Pikachu with it (#1400). _LeaveMapAnim
|
||||
-- drops the companion sprite before the bird flaps (home/pikachu.asm:1) and
|
||||
-- EnterMapAnim only puts it back once the swoop or the spin has landed, so
|
||||
-- Pikachu is invisible for the whole sequence and never stands on the landing
|
||||
-- cell ahead of the player.
|
||||
--
|
||||
-- POKEPORT_DRIVER=tests/drivers/fly_pikachu_bug1400_test.lua \
|
||||
-- POKEPORT_VERSION=yellow POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots love .
|
||||
--
|
||||
-- Never add POKEPORT_SPEED; the run needs an imported Yellow cache.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local PF = require("src.world.PikachuFollower")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
local function check(label, ok)
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
return ok
|
||||
end
|
||||
|
||||
check("running as Yellow (needs POKEPORT_VERSION=yellow)",
|
||||
GameVersion.isYellow())
|
||||
|
||||
game.save.flags = game.save.flags or {}
|
||||
game.save.flags.EVENT_GOT_STARTER = true
|
||||
game.save.flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
|
||||
-- DisablePikachuOverworldSpriteDrawing is what keeps it in the ball
|
||||
-- (pokeyellow scripts/OaksLab.asm); out of the ball is what follows (#1009)
|
||||
game.save.pikachuInBall = false
|
||||
game.save.party = { Pokemon.new(game.data, "PIKACHU", 20) }
|
||||
game.save.onBike = false
|
||||
|
||||
local function drawn()
|
||||
local ow = game.overworld
|
||||
local npc = ow and PF.current(ow)
|
||||
if not npc then return false end
|
||||
for _, e in ipairs(ow.entities or {}) do
|
||||
if e == npc then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
U.teleport(game, "ROUTE_17", 4, 10, "down")
|
||||
U.wait(30)
|
||||
local ow = game.stack:top()
|
||||
check("Pikachu is out and drawn before the flight", drawn())
|
||||
U.shot(game, DIR .. "/bug1400_0_before.png")
|
||||
|
||||
ow:flyTo("PALLET_TOWN")
|
||||
U.wait(12) -- mid in-place flap
|
||||
check("hidden during the departure flap", not drawn())
|
||||
U.shot(game, DIR .. "/bug1400_1_flap.png")
|
||||
|
||||
U.wait(60) -- the bird's path out
|
||||
check("still hidden while the bird flies off", not drawn())
|
||||
|
||||
local guard = 0
|
||||
while ow.map.id == "ROUTE_17" and guard < 900 do
|
||||
guard = guard + 1
|
||||
coroutine.yield()
|
||||
end
|
||||
guard = 0
|
||||
while not ow.flyArrive and guard < 900 do
|
||||
guard = guard + 1
|
||||
coroutine.yield()
|
||||
end
|
||||
U.wait(12) -- mid swoop, the frame the old bug showed Pikachu already landed
|
||||
check("hidden through the arrival swoop", not drawn())
|
||||
U.shot(game, DIR .. "/bug1400_2_arrive.png")
|
||||
|
||||
guard = 0
|
||||
while ow.flyArrive and guard < 900 do
|
||||
guard = guard + 1
|
||||
coroutine.yield()
|
||||
end
|
||||
U.wait(10)
|
||||
check("back out once the bird has landed", drawn())
|
||||
do
|
||||
local npc = PF.current(ow)
|
||||
local p = ow.player
|
||||
check("and it comes back on the player's own cell",
|
||||
npc and p and npc.cellX == p.cellX and npc.cellY == p.cellY)
|
||||
end
|
||||
U.shot(game, DIR .. "/bug1400_3_landed.png")
|
||||
|
||||
-- Dig / Teleport / Escape Rope take the same _LeaveMapAnim path
|
||||
game.save.lastHeal = { map = "VIRIDIAN_CITY", x = 23, y = 26 }
|
||||
ow:beginTeleportOut()
|
||||
U.wait(20)
|
||||
check("hidden during the teleport-out spin", not drawn())
|
||||
U.shot(game, DIR .. "/bug1400_4_spin_out.png")
|
||||
|
||||
guard = 0
|
||||
while (ow.teleportOut or ow.player.spinning or ow.transitioning)
|
||||
and guard < 900 do
|
||||
guard = guard + 1
|
||||
coroutine.yield()
|
||||
end
|
||||
U.wait(10)
|
||||
check("back out once the arrival spin has landed", drawn())
|
||||
U.shot(game, DIR .. "/bug1400_5_spin_in.png")
|
||||
|
||||
U.log("Watch the shots in order: Pikachu stands beside the player before")
|
||||
U.log("the flight, is nowhere on screen for the flap, the fade and the")
|
||||
U.log("swoop, and only reappears under him once the bird sets him down.")
|
||||
U.log("A Pikachu standing on the landing cell during the swoop is the bug.")
|
||||
U.log("The pad is yours -- fly around and watch the departures.")
|
||||
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
-- #1401: $d9/$da battlergfx loaded the wrong row count
|
||||
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1401_test.lua love .
|
||||
|
||||
local U = require("tests.drivers.util")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
|
||||
-- data/moves/animations.asm:1840 Growl, :3605 Icy Wind
|
||||
local CASES = {
|
||||
{ move = "GROWL", cmd = "battlergfx_2row", rows = 1, head = 7, feet = 6 },
|
||||
{ move = "ICY_WIND", cmd = "battlergfx_1row", rows = 2, head = 14, feet = 12 },
|
||||
}
|
||||
|
||||
local function sheet(runner, gfx)
|
||||
for _, entry in ipairs(runner.loaded or {}) do
|
||||
if entry.gfx == gfx then return entry end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function liftedStrip(runner)
|
||||
local structs = runner.objects and runner.objects.structs or {}
|
||||
for slot = 1, #structs do
|
||||
local id = structs[slot].objectId
|
||||
if type(id) == "string" and id:match("PLAYERHEAD") then return id end
|
||||
if type(id) == "string" and id:match("ENEMYFEET") then return id end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return function(game)
|
||||
U.wait(45)
|
||||
local world = game.world
|
||||
assert(world and world.map, "gold world did not boot")
|
||||
|
||||
game.save.party = { Mon.new(game.data, "CYNDAQUIL", 30) }
|
||||
assert(world:startBattle({ wild = Mon.new(game.data, "PIDGEY", 30) }),
|
||||
"startBattle failed")
|
||||
|
||||
local battle
|
||||
for _ = 1, 600 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then battle = top break end
|
||||
U.wait(1)
|
||||
end
|
||||
assert(battle and battle.battle, "battle screen is not on the stack")
|
||||
assert(battle.anims and battle.anims.scripts,
|
||||
"battle_anims.lua has no scripts -- re-import Gold")
|
||||
|
||||
local failed = false
|
||||
for _, case in ipairs(CASES) do
|
||||
battle.anim = nil
|
||||
if not battle:animForMove(case.move, "player") then
|
||||
U.log("FAIL no animation for", case.move)
|
||||
failed = true
|
||||
else
|
||||
local head, feet
|
||||
for _ = 1, 400 do
|
||||
local runner = battle.anim
|
||||
if not runner then break end
|
||||
head = head or sheet(runner, "BATTLE_ANIM_GFX_PLAYERHEAD")
|
||||
feet = feet or sheet(runner, "BATTLE_ANIM_GFX_ENEMYFEET")
|
||||
U.wait(1)
|
||||
end
|
||||
if not (head and feet) then
|
||||
U.log("FAIL", case.move, "never loaded the battler sheets")
|
||||
failed = true
|
||||
else
|
||||
U.log(("%-9s %-16s rows=%d head=%d feet=%d (want rows=%d head=%d feet=%d)")
|
||||
:format(case.move, case.cmd, head.rows, head.tiles, feet.tiles,
|
||||
case.rows, case.head, case.feet))
|
||||
if head.rows ~= case.rows or head.tiles ~= case.head
|
||||
or feet.tiles ~= case.feet then
|
||||
U.log("FAIL", case.move, "loaded the wrong row count")
|
||||
failed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
U.log(failed and "FAIL #1401" or "PASS #1401 battlergfx rows follow the jumptable")
|
||||
|
||||
U.log("look: Growl lifts ONE row of the Cyndaquil's own tiles, not two")
|
||||
battle.anim = nil
|
||||
battle:animForMove("GROWL", "player")
|
||||
for _ = 1, 400 do
|
||||
if not battle.anim then break end
|
||||
local strip = liftedStrip(battle.anim)
|
||||
if strip then
|
||||
U.log("parked on", strip)
|
||||
break
|
||||
end
|
||||
U.wait(1)
|
||||
end
|
||||
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,161 @@
|
||||
-- #1421: the Gold battle HUD ran ahead of its own animations.
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1421_test.lua love .
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1421 (default)
|
||||
--
|
||||
-- Two symptoms, one seam, and neither can be asserted -- both are "what is on
|
||||
-- screen while an animation runs":
|
||||
--
|
||||
-- caught a wild mon that is NOT in the dex is caught with a MASTER BALL.
|
||||
-- The caught mark ($5d at (1,1)) must be absent for every frame of
|
||||
-- the throw and every line after it, because DrawEnemyHUDBorder --
|
||||
-- the only thing that paints it -- is never called again during a
|
||||
-- capture (engine/battle/trainer_huds.asm:134-151). The control
|
||||
-- run right after it re-enters a battle with the same species now
|
||||
-- in the dex, where the mark IS there from the first frame.
|
||||
--
|
||||
-- status THUNDER WAVE on the enemy. PAR may not be on the HUD until the
|
||||
-- animation and the "is paralyzed!" line are done with, which is
|
||||
-- where UpdateBattleHuds runs (home/battle.asm:150).
|
||||
--
|
||||
-- Every frame of both animations is logged as `live` (the engine's byte, a
|
||||
-- whole turn ahead) against `hud` (what the HUD is allowed to print), so the
|
||||
-- lag is a column of text as well as a strip of pictures. The run ends in a
|
||||
-- third wild battle with THUNDER WAVE under the cursor: press A, A, A and
|
||||
-- watch the tag land with its own line.
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
|
||||
local OUT = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1421"
|
||||
local WILD = "PIDGEY"
|
||||
|
||||
local hostVisible = love.visible
|
||||
love.visible = function(v) if hostVisible then pcall(hostVisible, v) end end
|
||||
|
||||
local function tap(game, button, frames)
|
||||
game.input.pressQueue[#game.input.pressQueue + 1] = button
|
||||
game.input.state[button] = true
|
||||
U.wait(2)
|
||||
game.input.state[button] = false
|
||||
U.wait(frames or 6)
|
||||
end
|
||||
|
||||
local function openBattle(game, opts)
|
||||
assert(game.world:startBattle(opts), "startBattle failed")
|
||||
for _ = 1, 900 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then return top end
|
||||
U.wait(1)
|
||||
end
|
||||
error("battle screen never came up")
|
||||
end
|
||||
|
||||
local function toMenu(game, screen)
|
||||
for _ = 1, 400 do
|
||||
if screen.phase == "menu" then return true end
|
||||
if screen.battle.over then return false end
|
||||
tap(game, "a", 2)
|
||||
end
|
||||
return screen.phase == "menu"
|
||||
end
|
||||
|
||||
local function giveMoves(game, mon, moves)
|
||||
mon.moves = {}
|
||||
for i, id in ipairs(moves) do
|
||||
local def = assert(game.data.moves[id], id .. " is not in moves.lua")
|
||||
mon.moves[i] = { id = id, pp = def.pp, maxPp = def.pp }
|
||||
end
|
||||
return mon
|
||||
end
|
||||
|
||||
-- Shoot and log while the screen is busy: `probe` returns the two values the
|
||||
-- run is about, and the caller gets them for every frame.
|
||||
local function watch(game, screen, prefix, label, probe, limit)
|
||||
local frames = 0
|
||||
while frames < (limit or 240) do
|
||||
if frames % 3 == 0 then
|
||||
U.shot(game, ("%s-%03d.png"):format(prefix, frames))
|
||||
local live, hud = probe()
|
||||
U.log(("[driver] %s f%03d live=%s hud=%s")
|
||||
:format(label, frames, tostring(live), tostring(hud)))
|
||||
end
|
||||
if not screen.anim and screen.phase ~= "resolving" then break end
|
||||
frames = frames + 1
|
||||
U.wait(1)
|
||||
end
|
||||
return frames
|
||||
end
|
||||
|
||||
return function(game)
|
||||
U.wait(45)
|
||||
assert(game.world and game.world.map, "gold world did not boot")
|
||||
|
||||
local save = game.save
|
||||
save.pokedex = save.pokedex or { seen = {}, caught = {} }
|
||||
save.pokedex.caught[WILD] = nil
|
||||
save.inventory = { MASTER_BALL = 5, POKE_BALL = 10 }
|
||||
save.boxes = nil
|
||||
save.party = { giveMoves(game, Mon.new(game.data, "CYNDAQUIL", 30),
|
||||
{ "THUNDER_WAVE", "TACKLE" }) }
|
||||
|
||||
------------------------------------------------------------------ caught
|
||||
local screen = openBattle(game, { wild = Mon.new(game.data, WILD, 5) })
|
||||
assert(toMenu(game, screen), "never reached the battle menu")
|
||||
U.shot(game, OUT .. "/caught-00-menu.png")
|
||||
U.log(("[driver] caught: dex before = %s, hud latch = %s")
|
||||
:format(tostring(save.pokedex.caught[WILD]), tostring(screen.caughtMark)))
|
||||
|
||||
screen:useItem("MASTER_BALL")
|
||||
watch(game, screen, OUT .. "/caught-01-throw", "caught",
|
||||
function()
|
||||
return save.pokedex.caught[WILD] and true or false, screen.caughtMark
|
||||
end, 200)
|
||||
|
||||
-- The lines that follow the throw: "Gotcha!", the dex entry, the nickname
|
||||
-- prompt. The mark may not appear on any of them either.
|
||||
for i = 0, 7 do
|
||||
U.shot(game, ("%s/caught-02-after-%d.png"):format(OUT, i))
|
||||
if screen.phase == "ask-nickname" then tap(game, "b", 4)
|
||||
else tap(game, "a", 4) end
|
||||
if screen.phase == "done" or not game.stack:top() then break end
|
||||
end
|
||||
U.log(("[driver] caught: dex after = %s, hud latch = %s")
|
||||
:format(tostring(save.pokedex.caught[WILD]), tostring(screen.caughtMark)))
|
||||
|
||||
for _ = 1, 300 do
|
||||
if game.world and not (game.stack:top() or {}).battle then break end
|
||||
tap(game, "a", 2)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------ control
|
||||
-- Same species, now in the dex: the mark IS on the HUD from the first frame
|
||||
-- the enemy HUD is drawn, because DrawEnemyHUDBorder runs at battle start.
|
||||
save.party = { giveMoves(game, Mon.new(game.data, "CYNDAQUIL", 30),
|
||||
{ "THUNDER_WAVE", "TACKLE" }) }
|
||||
screen = openBattle(game, { wild = Mon.new(game.data, WILD, 5) })
|
||||
assert(toMenu(game, screen), "never reached the battle menu")
|
||||
U.shot(game, OUT .. "/control-00-mark.png")
|
||||
U.log(("[driver] control: dex = %s, hud latch = %s")
|
||||
:format(tostring(save.pokedex.caught[WILD]), tostring(screen.caughtMark)))
|
||||
|
||||
------------------------------------------------------------------ status
|
||||
screen:submit({ kind = "move", move = "THUNDER_WAVE" })
|
||||
watch(game, screen, OUT .. "/status-01-anim", "status",
|
||||
function()
|
||||
local enemy = screen.battle.enemy
|
||||
return enemy and enemy.status, screen:hudStatus(enemy, "enemy")
|
||||
end, 240)
|
||||
U.shot(game, OUT .. "/status-02-line.png")
|
||||
for i = 0, 4 do
|
||||
tap(game, "a", 4)
|
||||
U.shot(game, ("%s/status-03-after-%d.png"):format(OUT, i))
|
||||
end
|
||||
U.log(("[driver] status: live=%s hud=%s"):format(
|
||||
tostring(screen.battle.enemy and screen.battle.enemy.status),
|
||||
tostring(screen:hudStatus(screen.battle.enemy, "enemy"))))
|
||||
|
||||
U.log("[driver] shots in " .. OUT
|
||||
.. " -- the battle is yours: FIGHT, THUNDER WAVE, watch the tag land")
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
-- #1425 (and #1424): the PACK's ×NN column.
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1425_test.lua love .
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1425 (default)
|
||||
--
|
||||
-- Nothing here can be asserted: the bug IS the column a digit lands in.
|
||||
-- PlaceMenuItemQuantity's `lb bc, 1, 2` (engine/menus/menu_2.asm:24) is a
|
||||
-- two-digit field with the leading digit blanked, so a ×5 and a ×50 must have
|
||||
-- their ones digit in the SAME column; the port printed "×5" hard against the
|
||||
-- cross. Each pocket is seeded with a one-digit and a two-digit count stacked
|
||||
-- next to each other so the two rows can be read off against one another, and
|
||||
-- the TM pocket is here because its rows used to print no count at all.
|
||||
--
|
||||
-- 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")
|
||||
|
||||
-- One-digit and two-digit counts side by side in every pocket that shows one.
|
||||
local SEED = {
|
||||
{ "POTION", 5 },
|
||||
{ "SUPER_POTION", 50 },
|
||||
{ "ANTIDOTE", 1 },
|
||||
{ "FULL_HEAL", 99 },
|
||||
{ "POKE_BALL", 7 },
|
||||
{ "GREAT_BALL", 12 },
|
||||
{ "TM_DYNAMICPUNCH", 1 },
|
||||
{ "TM_HEADBUTT", 3 },
|
||||
{ "TM_THUNDER", 24 },
|
||||
{ "HM_CUT", 1 },
|
||||
{ "HM_SURF", 1 },
|
||||
}
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1425"
|
||||
|
||||
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
|
||||
|
||||
local pack = PackMenu.new(game, { save = save, world = game.world,
|
||||
onClose = function() end })
|
||||
game.stack:push(pack)
|
||||
|
||||
shot("00-items") -- ×5 over ×50: the ones digits line up
|
||||
U.tap(game, "right")
|
||||
shot("01-balls") -- ×7 over ×12
|
||||
U.tap(game, "right")
|
||||
shot("02-key-items") -- no counts at all here
|
||||
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.
|
||||
U.tap(game, "down")
|
||||
U.tap(game, "select")
|
||||
shot("04-tmhm-armed")
|
||||
U.tap(game, "up")
|
||||
shot("05-tmhm-destination")
|
||||
U.tap(game, "a")
|
||||
shot("06-tmhm-moved") -- HEADBUTT is now the first TM row
|
||||
|
||||
U.log("[driver] bag order:", table.concat(Bag.order(save), " "))
|
||||
|
||||
-- Back to the ITEM pocket and into a TOSS, whose own box prints the count
|
||||
-- with leading zeros (×01, not × 1).
|
||||
U.tap(game, "left")
|
||||
U.tap(game, "left")
|
||||
U.tap(game, "left")
|
||||
U.tap(game, "a")
|
||||
U.tap(game, "down")
|
||||
U.tap(game, "down")
|
||||
U.tap(game, "a")
|
||||
shot("07-toss-quantity")
|
||||
|
||||
U.log("[driver] shots in " .. out .. " -- the PACK is yours")
|
||||
end
|
||||
@@ -0,0 +1,174 @@
|
||||
-- #1428: the Gold battle HUD never drew the party ball rows.
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1428_test.lua love .
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1428 (default)
|
||||
--
|
||||
-- BattleStart_TrainerHuds (engine/battle/trainer_huds.asm:1-9) runs from
|
||||
-- inside BattleStartMessage, so the rows are on screen UNDER the opening line
|
||||
-- and nowhere else: the player's six always, the opponent's only outside a
|
||||
-- wild battle. EnemySwitch_TrainerHud (:11-15) brings the opponent's row
|
||||
-- back for the "will you switch?" prompt after one of its mons drops.
|
||||
--
|
||||
-- The party is seeded so all four staged tiles are on screen at once
|
||||
-- (StageBallTilesData, :47-99): healthy, statused, fainted, and the empty
|
||||
-- slots past the party count.
|
||||
--
|
||||
-- trainer the intro line, both rows, then the shift prompt after the first
|
||||
-- enemy mon faints -- the opponent's row again, one ball darkened
|
||||
-- wild the same intro with only the player's row, per the `dec a / ret z`
|
||||
--
|
||||
-- Nothing here is assertable: the fix IS twelve sprites. The run ends on the
|
||||
-- trainer battle's own menu with a human holding the controls.
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local Trainers = require("src.world.gen2.Trainers")
|
||||
|
||||
local OUT = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1428"
|
||||
|
||||
local hostVisible = love.visible
|
||||
love.visible = function(v) if hostVisible then pcall(hostVisible, v) end end
|
||||
|
||||
local function tap(game, button, frames)
|
||||
game.input.pressQueue[#game.input.pressQueue + 1] = button
|
||||
game.input.state[button] = true
|
||||
U.wait(2)
|
||||
game.input.state[button] = false
|
||||
U.wait(frames or 6)
|
||||
end
|
||||
|
||||
local function openBattle(game, opts)
|
||||
assert(game.world:startBattle(opts), "startBattle failed")
|
||||
for _ = 1, 900 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then return top end
|
||||
U.wait(1)
|
||||
end
|
||||
error("battle screen never came up")
|
||||
end
|
||||
|
||||
-- Four healthy, one poisoned, one fainted, and one slot left empty: every
|
||||
-- tile StageBallTilesData can stage, in one row. The lead is handed a
|
||||
-- damaging move outright, because slot 1 of its level-up set is LEER.
|
||||
local function seedParty(game)
|
||||
local party = {}
|
||||
for i = 1, 5 do
|
||||
party[i] = Mon.new(game.data, "CYNDAQUIL", 20 + i)
|
||||
end
|
||||
party[1].moves = {}
|
||||
for i, id in ipairs({ "EMBER", "TACKLE" }) do
|
||||
local def = assert(game.data.moves[id], id .. " is not in moves.lua")
|
||||
party[1].moves[i] = { id = id, pp = def.pp, maxPp = def.pp }
|
||||
end
|
||||
party[4].status = "poison"
|
||||
party[5].hp = 0
|
||||
return party
|
||||
end
|
||||
|
||||
local function rowState(screen)
|
||||
local rows = screen.ballRows or {}
|
||||
return ("player=%s enemy=%s balls=%s"):format(
|
||||
tostring(rows.player), tostring(rows.enemy),
|
||||
tostring(screen.hud and screen.hud:image("balls") ~= nil))
|
||||
end
|
||||
|
||||
return function(game)
|
||||
U.wait(45)
|
||||
assert(game.world and game.world.map, "gold world did not boot")
|
||||
|
||||
local save = game.save
|
||||
save.inventory = {}
|
||||
save.party = seedParty(game)
|
||||
|
||||
--------------------------------------------------------------- trainer
|
||||
local entry = game.world:trainerParty(36, 1) -- BUG_CATCHER member 1
|
||||
assert(entry, "no BUG_CATCHER member 1 in trainers.lua")
|
||||
entry.party = Trainers.party(game.data, entry)
|
||||
local screen = openBattle(game, { trainer = entry })
|
||||
|
||||
-- The opening line: both rows, both borders, and no names or bars yet.
|
||||
-- Not before the intro slide has settled, or the shots catch the pics
|
||||
-- mid-transform instead of the HUD.
|
||||
U.wait(150)
|
||||
for i = 0, 6 do
|
||||
U.shot(game, ("%s/trainer-00-intro-%d.png"):format(OUT, i))
|
||||
U.log("[driver] intro " .. i .. ": " .. rowState(screen))
|
||||
U.wait(6)
|
||||
end
|
||||
U.log(("[driver] OT party = %d"):format(#(screen.battle.enemyParty or {})))
|
||||
|
||||
-- Page through the pic slide and the send-out: the rows go with the OAM the
|
||||
-- send-out animation takes over.
|
||||
for i = 0, 7 do
|
||||
tap(game, "a", 5)
|
||||
U.shot(game, ("%s/trainer-01-sendout-%d.png"):format(OUT, i))
|
||||
end
|
||||
for _ = 1, 400 do
|
||||
if screen.phase == "menu" then break end
|
||||
tap(game, "a", 2)
|
||||
end
|
||||
U.shot(game, OUT .. "/trainer-02-menu.png")
|
||||
U.log("[driver] menu: " .. rowState(screen))
|
||||
|
||||
--------------------------------------------------------------- shift prompt
|
||||
-- Drop the enemy's lead so HandleEnemySwitch offers the switch: its row is
|
||||
-- redrawn for the prompt, with the fainted ball darkened.
|
||||
if #(screen.battle.enemyParty or {}) > 1 then
|
||||
for _ = 1, 400 do
|
||||
if screen.phase == "ask-shift" or screen.phase == "shift-intro" then
|
||||
break
|
||||
end
|
||||
if screen.battle.over then break end
|
||||
if screen.phase == "menu" then
|
||||
local enemy = screen.battle.enemy
|
||||
if enemy then enemy.hp = 1 end
|
||||
screen:chooseMenu("fight")
|
||||
U.wait(2)
|
||||
screen:chooseMove(1)
|
||||
U.wait(4)
|
||||
else
|
||||
tap(game, "a", 6)
|
||||
end
|
||||
end
|
||||
U.log("[driver] shift prompt: phase=" .. tostring(screen.phase))
|
||||
for i = 0, 5 do
|
||||
U.shot(game, ("%s/trainer-03-shift-%d.png"):format(OUT, i))
|
||||
U.log("[driver] shift " .. i .. ": phase=" .. tostring(screen.phase)
|
||||
.. " " .. rowState(screen))
|
||||
U.wait(6)
|
||||
end
|
||||
-- NO: the enemy sends its next mon and the row goes with the animation.
|
||||
tap(game, "down", 4)
|
||||
tap(game, "a", 4)
|
||||
for i = 0, 5 do
|
||||
U.shot(game, ("%s/trainer-04-after-shift-%d.png"):format(OUT, i))
|
||||
tap(game, "a", 5)
|
||||
end
|
||||
else
|
||||
U.log("[driver] this trainer has one mon; no shift prompt to show")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------- wild
|
||||
-- ShowPlayerMonsRemaining runs for a wild battle too; the `dec a / ret z`
|
||||
-- right after it is what keeps the OT row off.
|
||||
for _ = 1, 600 do
|
||||
if not (game.stack:top() or {}).battle then break end
|
||||
tap(game, "a", 2)
|
||||
end
|
||||
save.party = seedParty(game)
|
||||
screen = openBattle(game, { wild = Mon.new(game.data, "PIDGEY", 5) })
|
||||
U.wait(150)
|
||||
for i = 0, 6 do
|
||||
U.shot(game, ("%s/wild-00-intro-%d.png"):format(OUT, i))
|
||||
U.log("[driver] wild " .. i .. ": " .. rowState(screen))
|
||||
U.wait(6)
|
||||
end
|
||||
for _ = 1, 400 do
|
||||
if screen.phase == "menu" then break end
|
||||
tap(game, "a", 2)
|
||||
end
|
||||
U.shot(game, OUT .. "/wild-01-menu.png")
|
||||
|
||||
U.log("[driver] shots in " .. OUT .. " -- the battle is yours")
|
||||
end
|
||||
@@ -0,0 +1,142 @@
|
||||
-- Driver: the shiny sparkle on a SENT OUT mon (#1431).
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1431_test.lua love .
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1431 (default)
|
||||
--
|
||||
-- SendOutPlayerMon calls BattleCheckPlayerShininess and replays
|
||||
-- ANIM_SEND_OUT_MON with wBattleAnimParam 1 -- the script's `.Shiny` arm --
|
||||
-- between the plain animation and the cry (engine/battle/core.asm:3820-3837);
|
||||
-- ShowSetEnemyMonAndSendOutAnimation does the same for every enemy send-out
|
||||
-- (:3364-3383). Only the wild-intro path (BattleStartMessage, :8701-8715) had
|
||||
-- been ported, so a shiny lead-off, a shiny switch-in and a shiny enemy
|
||||
-- replacement all came out silent.
|
||||
--
|
||||
-- Three send-outs to watch, none of them the wild intro: the player's lead-off,
|
||||
-- the player's own switch, and the opponent's replacement after a faint.
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
|
||||
-- constants/battle_constants.asm: the DV pair BATTLETYPE_FORCESHINY forces.
|
||||
local SHINY_DVS = { attack = 14, defense = 10, speed = 10, special = 10 }
|
||||
|
||||
local OUT = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1431"
|
||||
|
||||
local function tap(game, button, frames)
|
||||
game.input.pressQueue[#game.input.pressQueue + 1] = button
|
||||
game.input.state[button] = true
|
||||
U.wait(2)
|
||||
game.input.state[button] = false
|
||||
U.wait(frames or 6)
|
||||
end
|
||||
|
||||
local function openBattle(game, opts)
|
||||
assert(game.world:startBattle(opts), "startBattle failed")
|
||||
for _ = 1, 900 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then return top end
|
||||
U.wait(1)
|
||||
end
|
||||
error("battle screen never came up")
|
||||
end
|
||||
|
||||
-- Stop on the frame the `.Shiny` arm is actually running: animId plus the
|
||||
-- wBattleAnimParam the arm branches on (data/moves/animations.asm:414-417).
|
||||
local function shinyArm(screen)
|
||||
local anim = screen.anim
|
||||
return anim ~= nil and anim.animId == "ANIM_SEND_OUT_MON"
|
||||
and anim.param == 1
|
||||
end
|
||||
|
||||
local function watch(game, screen, label, frames)
|
||||
for _ = 1, (frames or 400) do
|
||||
if shinyArm(screen) then
|
||||
U.log("[driver] " .. label .. ": .Shiny arm running")
|
||||
U.shot(game, ("%s/%s.png"):format(OUT, label))
|
||||
return true
|
||||
end
|
||||
U.wait(1)
|
||||
end
|
||||
U.log("[driver] " .. label .. ": NO shiny arm seen")
|
||||
return false
|
||||
end
|
||||
|
||||
return function(game)
|
||||
U.wait(45)
|
||||
assert(game.world and game.world.map, "gold world did not boot")
|
||||
|
||||
-- A shiny only reads as one in colour, and the sparkle rides the same anim.
|
||||
GbcPalette.setMode("gbc")
|
||||
|
||||
local save = game.save
|
||||
save.inventory = {}
|
||||
local lead = Mon.new(game.data, "CYNDAQUIL", 40, { dvs = SHINY_DVS })
|
||||
local bench = Mon.new(game.data, "TOTODILE", 40, { dvs = SHINY_DVS })
|
||||
assert(lead.shiny and bench.shiny, "the FORCESHINY DVs did not take")
|
||||
local move = assert(game.data.moves.EMBER, "no EMBER in moves.lua")
|
||||
lead.moves = { { id = "EMBER", pp = move.pp, maxPp = move.pp } }
|
||||
save.party = { lead, bench }
|
||||
|
||||
local entry = game.world:trainerParty(36, 1) -- BUG_CATCHER member 1
|
||||
assert(entry, "no BUG_CATCHER member 1 in trainers.lua")
|
||||
local Trainers = require("src.world.gen2.Trainers")
|
||||
entry.party = Trainers.party(game.data, entry)
|
||||
for _, mon in ipairs(entry.party) do mon.shiny = true end
|
||||
local screen = openBattle(game, { trainer = entry })
|
||||
|
||||
------------------------------------------------------------ enemy lead
|
||||
-- ShowSetEnemyMonAndSendOutAnimation, out of the opening sequence.
|
||||
watch(game, screen, "00-enemy-sendout", 900)
|
||||
|
||||
----------------------------------------------------------- player lead
|
||||
-- SendOutPlayerMon behind the "Go!" line: this is the one the report is
|
||||
-- about, and it never sparkled before the fix.
|
||||
local sawPlayer = watch(game, screen, "01-player-sendout", 900)
|
||||
|
||||
for _ = 1, 600 do
|
||||
if screen.phase == "menu" then break end
|
||||
tap(game, "a", 2)
|
||||
end
|
||||
|
||||
---------------------------------------------------------- player switch
|
||||
-- The same routine again, this time as a voluntary mid-battle switch.
|
||||
local sawSwitch = false
|
||||
if screen.phase == "menu" and (screen.battle.party or {})[2] then
|
||||
screen:submit({ kind = "switch", index = 2 })
|
||||
sawSwitch = watch(game, screen, "02-player-switch", 900)
|
||||
for _ = 1, 600 do
|
||||
if screen.phase == "menu" or screen.battle.over then break end
|
||||
tap(game, "a", 2)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------ enemy replacement
|
||||
local sawReplace = false
|
||||
if #(screen.battle.enemyParty or {}) > 1 then
|
||||
for _ = 1, 600 do
|
||||
if screen.battle.over then break end
|
||||
if screen.phase == "menu" then
|
||||
local enemy = screen.battle.enemy
|
||||
if enemy then enemy.hp = 1 end
|
||||
screen:chooseMenu("fight")
|
||||
U.wait(2)
|
||||
screen:chooseMove(1)
|
||||
break
|
||||
end
|
||||
tap(game, "a", 2)
|
||||
end
|
||||
sawReplace = watch(game, screen, "03-enemy-replacement", 900)
|
||||
end
|
||||
|
||||
U.log(("[driver] player lead-off %s, player switch %s, enemy replacement %s")
|
||||
:format(tostring(sawPlayer), tostring(sawSwitch), tostring(sawReplace)))
|
||||
U.log("[driver] shots in " .. OUT)
|
||||
U.log("[driver] every send-out of a shiny must flash, sparkle and chime")
|
||||
U.log("[driver] BEFORE its cry, not only the wild mon at battle start.")
|
||||
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,72 @@
|
||||
-- #1441: the Magnet Train ride drew a flat white screen.
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_bug1441_test.lua love .
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1441 (default)
|
||||
--
|
||||
-- Nothing here is assertable: the bug IS what the screen shows. The ride
|
||||
-- bakes its background out of data.gen2Field.magnetTrain, TILESET_TRAIN_
|
||||
-- STATION's sheet and the TOWN palettes, and every one of those was read
|
||||
-- under its Gen 1 key, so all four came back nil and GbcPalette's fallback
|
||||
-- filled the panel with DMG_SHADES[1].
|
||||
--
|
||||
-- The run boots into the Goldenrod station, prints whether the four tables
|
||||
-- are actually there, shoots the Saffron-bound ride from JUMPTABLE_INIT
|
||||
-- through to the arrival, then plays the Goldenrod-bound one back at 1x with
|
||||
-- no screenshots in the way -- a human watching the window sees the whole
|
||||
-- animation, which is the only place this bug ever showed.
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local STATION = "GOLDENROD_MAGNET_TRAIN_STATION"
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1441"
|
||||
|
||||
U.wait(45)
|
||||
local world = game.world
|
||||
assert(world and world.map, "gold world did not boot")
|
||||
world:setMap(STATION, 11, 8, "up")
|
||||
U.wait(10)
|
||||
|
||||
local data = game.data or {}
|
||||
local field = data.gen2Field and data.gen2Field.magnetTrain
|
||||
U.log("gen2Field.magnetTrain:", field and #field.bgTiles or "MISSING")
|
||||
U.log("gen2Tilesets.TILESET_TRAIN_STATION:",
|
||||
data.gen2Tilesets and data.gen2Tilesets.TILESET_TRAIN_STATION
|
||||
and data.gen2Tilesets.TILESET_TRAIN_STATION.image or "MISSING")
|
||||
U.log("gen2Palettes:", data.gen2Palettes and "ok" or "MISSING")
|
||||
U.log("gen2Sprites.SPRITE_CHRIS:",
|
||||
data.gen2Sprites and data.gen2Sprites.SPRITE_CHRIS and "ok" or "MISSING")
|
||||
|
||||
local done = false
|
||||
world:magnetTrain(true, function() done = true end)
|
||||
U.wait(2)
|
||||
|
||||
local ride = game.stack:top()
|
||||
U.log("ride background:", ride and ride.background
|
||||
and (ride:background() and "baked" or "nil canvas") or "no screen")
|
||||
|
||||
-- SFX_TRAIN_ARRIVED lands a good while in; shoot across the whole ride so
|
||||
-- the departure, the three scrolling bands and the stop are all on disk.
|
||||
for index = 0, 11 do
|
||||
U.shot(game, ("%s/%02d-ride.png"):format(out, index))
|
||||
U.wait(20)
|
||||
if done then break end
|
||||
end
|
||||
for _ = 1, 600 do
|
||||
if done then break end
|
||||
U.wait(5)
|
||||
end
|
||||
U.log("ride finished at frame", U.frame())
|
||||
U.log("shots in " .. out)
|
||||
|
||||
-- The return leg, played out in full with nothing else on screen: watch the
|
||||
-- window, not the PNGs. The ride reads no input, so it ends on its own.
|
||||
local back = false
|
||||
world:magnetTrain(false, function() back = true end)
|
||||
for _ = 1, 900 do
|
||||
if back then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(60)
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Eyeball driver (#1442): the radio card's tuning knob. PokegearRadio_Init
|
||||
-- spawns SPRITE_ANIM_OBJ_RADIO_TUNING_KNOB on tile $08 and AnimateTuningKnob
|
||||
-- writes wRadioTuningKnob into its XOFFSET, so a red needle stands in the dial
|
||||
-- box at screen x = 72 + knob and steps with every UP/DOWN. Before the fix the
|
||||
-- card drew the dial art and nothing in it.
|
||||
--
|
||||
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold \
|
||||
-- POKEPORT_SHOTS=/tmp/radioknob \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_radio_knob_bug1442.lua \
|
||||
-- perl -e 'alarm 300; exec @ARGV' \
|
||||
-- python3 -c "import pty; pty.spawn(['love','.'])"
|
||||
--
|
||||
-- The run parks on the radio card with the knob on 08.5, so UP and DOWN move
|
||||
-- the needle by hand.
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local SHOTS = os.getenv("POKEPORT_SHOTS") or "/tmp/radioknob"
|
||||
|
||||
return function(game)
|
||||
U.wait(45)
|
||||
local world = game.world
|
||||
assert(world and world.map, "gold world did not boot")
|
||||
|
||||
-- Goldenrod, where the card is handed out, and the two engine flags the
|
||||
-- START menu and the strip read: ENGINE_POKEGEAR and ENGINE_RADIO_CARD.
|
||||
assert(world:setMap("GOLDENROD_CITY", 12, 20, "down"),
|
||||
"setMap failed for GOLDENROD_CITY")
|
||||
world:setEngineFlag(4, true) -- ENGINE_POKEGEAR
|
||||
world:setEngineFlag(0, true) -- ENGINE_RADIO_CARD
|
||||
U.wait(5)
|
||||
|
||||
game:openStartMenuItem("pokegear")
|
||||
U.wait(5)
|
||||
local gear = game.stack:top()
|
||||
assert(gear and gear.cards, "the POKeGEAR did not open")
|
||||
gear.mode = "card"
|
||||
for index, card in ipairs(gear.cards) do
|
||||
if card.id == "radio" then gear.cardIndex = index end
|
||||
end
|
||||
assert(gear:card().id == "radio", "the RADIO card is missing from the strip")
|
||||
U.wait(5)
|
||||
|
||||
-- 04.5, the bottom of the dial: the needle sits at the left of the box.
|
||||
U.shot(game, SHOTS .. "/01-radio-04.5.png")
|
||||
U.log("knob", tostring(gear:currentStation().knob), "at 04.5")
|
||||
|
||||
U.tap(game, "up") U.wait(4)
|
||||
U.tap(game, "up") U.wait(4)
|
||||
U.shot(game, SHOTS .. "/02-radio-08.5.png")
|
||||
U.log("knob", tostring(gear:currentStation().knob), "at 08.5")
|
||||
|
||||
U.log("compare 01 and 02: a red needle stands in the dial box and has",
|
||||
"moved right; UP/DOWN now walk it by hand")
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
-- Driver: the starter Pikachu's battle entrance (#1429).
|
||||
--
|
||||
-- POKEPORT_DRIVER=tests/drivers/pikachu_entrance_bug1429_test.lua \
|
||||
-- POKEPORT_VERSION=yellow POKEPORT_IDENTITY=bug1429 POKEPORT_TOUCH=0 \
|
||||
-- SHOT_DIR=/tmp/shots love . (never under POKEPORT_SPEED: audio-timed)
|
||||
--
|
||||
-- SendOutMon branches on IsThisPartyMonStarterPikachu (pokeyellow
|
||||
-- engine/battle/core.asm:1798-1819): the starter never gets POOF_ANIM or
|
||||
-- AnimateSendingOutMon. It walks in instead --
|
||||
-- StarterPikachuBattleEntranceAnimation (engine/battle/pikachu_entrance_anim.asm)
|
||||
-- paints the back pic one column at a time from hlcoord 0,5, eight columns two
|
||||
-- frames apart, and only then does PlayPikachuSoundClip voice it.
|
||||
--
|
||||
-- The run halts in the middle of the walk-in, then again on the finished pic.
|
||||
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 GameVersion = require("src.core.GameVersion")
|
||||
local PF = require("src.world.PikachuFollower")
|
||||
|
||||
local function check(label, ok)
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
return ok
|
||||
end
|
||||
|
||||
check("running as Yellow (needs POKEPORT_VERSION=yellow)",
|
||||
GameVersion.isYellow())
|
||||
|
||||
game.save.player.name = "bryan"
|
||||
local pika = Pokemon.new(game.data, "PIKACHU", 20)
|
||||
BattleState.stampOT(game.save, pika)
|
||||
game.save.party = { pika, Pokemon.new(game.data, "CHARMANDER", 20) }
|
||||
check("the lead reads as the starter Pikachu",
|
||||
PF.isStarterPikachu(game.save, pika))
|
||||
|
||||
U.teleport(game, "ROUTE_1", 5, 20, "down")
|
||||
U.wait(20)
|
||||
local ow = game.overworld
|
||||
check("overworld is up to push the battle from", ow ~= nil)
|
||||
|
||||
local battle = BattleState.newWild(game, "PIDGEY", 5)
|
||||
battle.onFinish = function() end
|
||||
ow:pushBattle(battle)
|
||||
|
||||
local function tapUntil(cond, taps, gap)
|
||||
for _ = 1, (taps or 90) do
|
||||
if cond() then return true end
|
||||
U.tap(game, "a")
|
||||
for _ = 1, (gap or 4) do
|
||||
if cond() then return true end
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
return cond()
|
||||
end
|
||||
|
||||
-- Poll every frame: the slide is 16 frames long and the ball poof, if the
|
||||
-- bug were back, would never set this slot at all.
|
||||
local sliding = tapUntil(function()
|
||||
return battle.picOff ~= nil and battle.picOff.playerMon ~= nil
|
||||
end, 120, 2)
|
||||
check("the send-out started the entrance slide, not the ball poof", sliding)
|
||||
check("no grow-in is running alongside it", battle.growIn == nil)
|
||||
|
||||
if sliding then
|
||||
U.log("slide x =", tostring(battle.picOff.playerMon.x))
|
||||
check("mid-slide screenshot reached disk",
|
||||
U.shot(game, DIR .. "/bug1429_pikachu_sliding.png"))
|
||||
U.log("captured", DIR .. "/bug1429_pikachu_sliding.png")
|
||||
for _ = 1, 40 do
|
||||
if battle:picOffset("playerMon") == 0 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
check("the pic landed on its own column", battle:picOffset("playerMon") == 0)
|
||||
check("landed screenshot reached disk",
|
||||
U.shot(game, DIR .. "/bug1429_pikachu_landed.png"))
|
||||
U.log("captured", DIR .. "/bug1429_pikachu_landed.png")
|
||||
end
|
||||
|
||||
U.log("Pikachu should have walked in from the LEFT edge of the screen,")
|
||||
U.log("column by column, with no ball, no poof and no grow-out-of-the-ball,")
|
||||
U.log("and voiced its PCM clip only once it was fully drawn (#1429).")
|
||||
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Driver: where the "Will PLAYER change POKéMON?" YES/NO box sits (#1398).
|
||||
--
|
||||
-- POKEPORT_DRIVER=tests/drivers/shift_prompt_box_bug1398_test.lua \
|
||||
-- POKEPORT_IDENTITY=bug1398 POKEPORT_TOUCH=0 POKEPORT_VERSION=red \
|
||||
-- SHOT_DIR=/tmp/shots love .
|
||||
--
|
||||
-- EnemySendOutFirstMon (engine/battle/core.asm:1378-1384) does NOT go through
|
||||
-- InitYesNoTextBoxParameters for this one prompt: it inlines its own
|
||||
-- TWO_OPTION_MENU at hlcoord 0,7, i.e. hard against the LEFT edge, while every
|
||||
-- other YES/NO in the game sits at hlcoord 14,7 on the right. The port only
|
||||
-- had the shared right-hand box, so the SHIFT offer came up on the wrong side.
|
||||
--
|
||||
-- The run stops with the prompt on screen and the controls in a human's hands.
|
||||
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 Theme = require("src.ui.Theme")
|
||||
|
||||
local function check(label, ok)
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
return ok
|
||||
end
|
||||
|
||||
-- roster 1 is RATTATA 11 / EKANS 11: two mons, so the KO of the first one
|
||||
-- reaches EnemySendOutFirstMon with a reserve to announce.
|
||||
local OPP, ROSTER = "OPP_YOUNGSTER", 1
|
||||
|
||||
check("trainer class " .. OPP .. " is in the data",
|
||||
game.data.trainers ~= nil and game.data.trainers[OPP] ~= nil)
|
||||
check("Theme carries the left-edge box",
|
||||
Theme.trainerSwitchBox ~= nil and Theme.trainerSwitchBox.tx == 0
|
||||
and Theme.trainerSwitchBox.ty == 7)
|
||||
|
||||
-- SHIFT is what makes the prompt appear at all (the BIT_BATTLE_SHIFT test at
|
||||
-- core.asm:1375-1377 skips it under SET).
|
||||
game.save.options = game.save.options or {}
|
||||
game.save.options.battleStyle = "shift"
|
||||
game.save.player.name = "bryan"
|
||||
game.save.party = {
|
||||
Pokemon.new(game.data, "MEWTWO", 70),
|
||||
Pokemon.new(game.data, "CHARIZARD", 50),
|
||||
}
|
||||
|
||||
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
|
||||
U.wait(20)
|
||||
local ow = game.overworld
|
||||
check("overworld is up to push the battle from", ow ~= nil)
|
||||
|
||||
local ok, battle = pcall(BattleState.newTrainer, game, OPP, ROSTER)
|
||||
check("trainer battle constructed", ok and battle ~= nil)
|
||||
if not ok then
|
||||
U.log("could not start", OPP, "->", tostring(battle))
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
check("the foe has a reserve to send out", #battle.enemyParty >= 2)
|
||||
battle.onFinish = function() end
|
||||
ow:pushBattle(battle)
|
||||
|
||||
local function tapUntil(cond, taps, gap)
|
||||
for _ = 1, (taps or 60) do
|
||||
if cond() then return true end
|
||||
U.tap(game, "a")
|
||||
for _ = 1, (gap or 6) do
|
||||
if cond() then return true end
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
return cond()
|
||||
end
|
||||
|
||||
if not check("reached the FIGHT/PKMN/ITEM/RUN menu",
|
||||
tapUntil(function() return battle.phase == "menu" end, 200)) then
|
||||
U.log("phase is", tostring(battle.phase))
|
||||
end
|
||||
|
||||
-- FIGHT, first move: L70 MEWTWO one-shots the L11 lead.
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
U.tap(game, "a")
|
||||
U.wait(10)
|
||||
|
||||
-- Stop the moment the YES/NO goes up over the still-visible prompt page.
|
||||
local function choiceBox()
|
||||
local top = game.stack:top()
|
||||
return (top and top.tx and top.labels and top.labels[1] == "YES") and top
|
||||
or nil
|
||||
end
|
||||
local reached = tapUntil(function() return choiceBox() ~= nil end, 200, 4)
|
||||
if not check("the SHIFT prompt opened its YES/NO box", reached) then
|
||||
U.log("phase", tostring(battle.phase),
|
||||
"enemy hp", tostring(battle.enemy and battle.enemy.mon.hp),
|
||||
"top", tostring(game.stack:top()))
|
||||
end
|
||||
|
||||
local box = choiceBox()
|
||||
if box then
|
||||
check("the box is at hlcoord 0,7 (core.asm:1378-1384)",
|
||||
box.tx == 0 and box.ty == 7)
|
||||
U.log("box tx=" .. tostring(box.tx) .. " ty=" .. tostring(box.ty))
|
||||
check("prompt screenshot reached disk",
|
||||
U.shot(game, DIR .. "/bug1398_shift_prompt.png"))
|
||||
U.log("captured", DIR .. "/bug1398_shift_prompt.png")
|
||||
end
|
||||
|
||||
U.log("The foe's lead is down and the SHIFT offer is on screen.")
|
||||
U.log("YES/NO must sit against the LEFT edge, over the text box's left half,")
|
||||
U.log("not on the right where every other YES/NO in the game lives (#1398).")
|
||||
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Driver: nobody is caught mid-stride while a text box is up (#1435).
|
||||
-- UpdatePlayerSprite and UpdateNPCSprite both jump to their standing frame
|
||||
-- while BIT_FONT_LOADED is set (engine/overworld/movement.asm:57, :139), so a
|
||||
-- box that opens on a walk frame freezes a standing sprite, never a leg out.
|
||||
--
|
||||
-- POKEPORT_DRIVER=tests/drivers/talk_pose_bug1435_test.lua \
|
||||
-- POKEPORT_IDENTITY=bug1435 POKEPORT_TOUCH=0 POKEPORT_VERSION=red \
|
||||
-- SHOT_DIR=/tmp/shots love .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
local function check(label, ok)
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
return ok
|
||||
end
|
||||
|
||||
-- Mom stands at (5,4) in Red's house (pokered
|
||||
-- data/maps/objects/RedsHouse1F.asm); park the player beside her and push
|
||||
-- into her so the walk-in-place cycle is running when the box opens.
|
||||
U.teleport(game, "REDS_HOUSE_1F", 6, 4, "down")
|
||||
U.wait(20)
|
||||
local ow = game.overworld
|
||||
local p = ow.player
|
||||
|
||||
local walking = false
|
||||
for _ = 1, 90 do
|
||||
table.insert(game.input.pressQueue, "left")
|
||||
game.input.state.left = true
|
||||
coroutine.yield()
|
||||
if p:walkPhase() == 1 then walking = true break end
|
||||
end
|
||||
game.input.state.left = false
|
||||
check("bumping into MOM shows the walk frame", walking)
|
||||
U.shot(game, DIR .. "/bug1435_1_midstride.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
U.wait(4)
|
||||
check("talking to MOM opened a box over the overworld",
|
||||
game.stack:top() ~= ow)
|
||||
check("the walk clock is still mid-cycle underneath",
|
||||
(p.bumpFrames or 0) > 0 or p.moving)
|
||||
check("but the player is drawn standing", p:walkPhase() == 0)
|
||||
U.shot(game, DIR .. "/bug1435_2_talking.png")
|
||||
|
||||
-- The same gate for an NPC caught mid-step: Pallet Town's wanderers walk
|
||||
-- on their own, so wait for one and open a box while it is between cells.
|
||||
U.teleport(game, "PALLET_TOWN", 8, 8, "down")
|
||||
U.wait(30)
|
||||
ow = game.overworld
|
||||
local mover
|
||||
for _ = 1, 900 do
|
||||
for _, npc in ipairs(ow.npcs) do
|
||||
if npc.moving and npc:walkPhase() == 1 then mover = npc break end
|
||||
end
|
||||
if mover then break end
|
||||
coroutine.yield()
|
||||
end
|
||||
if check("caught a Pallet Town NPC mid-step", mover ~= nil) then
|
||||
game.stack:push(TextBox.new(game, "TESTING THE POSE."))
|
||||
U.wait(4)
|
||||
check("the NPC is still mid-step underneath", mover.moving)
|
||||
check("but it is drawn standing too", mover:walkPhase() == 0)
|
||||
U.shot(game, DIR .. "/bug1435_3_npc.png")
|
||||
end
|
||||
|
||||
U.log("Look at bug1435_2_talking.png and bug1435_3_npc.png: with the box")
|
||||
U.log("up, both sprites stand square on their feet. A leg out, or the")
|
||||
U.log("split-stride frame held for the whole conversation, is the bug.")
|
||||
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
@@ -0,0 +1,176 @@
|
||||
-- Manual check for #1453: the SGB zone scissors must stay contiguous in
|
||||
-- framebuffer pixels on a fractional-DPI surface (Android), where LOVE 11
|
||||
-- truncates the scissor to whole units before scaling it. Zones are pokered
|
||||
-- data/sgb/sgb_packets.asm BlkPacket_Titlescreen (rows 0-7 / 8-9 / 10-17).
|
||||
-- POKEPORT_DRIVER=tests/drivers/title_seam_bug1453_test.lua POKEPORT_TOUCH=0 SHOT_DIR=/tmp/shots love .
|
||||
-- PROBE_DPI picks the density to emulate (default 2.625, the reporter's).
|
||||
-- Do not set POKEPORT_SPEED: fast-forward desynchronizes the title music.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local SHOT_DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local DPI = tonumber(os.getenv("PROBE_DPI") or "") or 2.625
|
||||
|
||||
local function check(label, ok)
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
return ok
|
||||
end
|
||||
|
||||
-- past the copyright splash / attract movie (engine/movie/splash.asm)
|
||||
local title
|
||||
for _ = 1, 120 do
|
||||
local top = game.stack:top()
|
||||
if top and top.screenId == "TitleState" and top.sgbPalettes then
|
||||
title = top
|
||||
break
|
||||
end
|
||||
U.tap(game, "start")
|
||||
U.wait(9)
|
||||
end
|
||||
check("the title screen is on top", title ~= nil)
|
||||
if not (title and title.sgbPalettes) then
|
||||
U.log("No title state to look at; nothing below can run.")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
|
||||
local opts = game.save.options
|
||||
local mode = opts and opts.colors or PaletteFX.mode
|
||||
if PaletteFX.mode ~= "gbc" then
|
||||
U.log("COLORS is", tostring(mode) .. "; switching the view to SGB for the check")
|
||||
PaletteFX.setMode("gbc")
|
||||
U.wait(20)
|
||||
end
|
||||
local zones = PaletteFX.ensureZones(title:sgbPalettes(game))
|
||||
check("the title builds three SGB zones", zones ~= nil and #zones == 3)
|
||||
|
||||
-- A canvas carries its own dpiscale, so the emulated Android surface goes
|
||||
-- through the same LOVE scissor path the phone does, on this desktop.
|
||||
local probe = love.graphics.newCanvas(math.floor(1920 / DPI),
|
||||
math.floor(1080 / DPI),
|
||||
{ dpiscale = DPI })
|
||||
local PW, PH = probe:getPixelWidth(), probe:getPixelHeight()
|
||||
local Sp = math.max(1, math.floor(math.min(PW / 160, PH / 144)))
|
||||
local Ux, Uy = Sp / DPI, Sp / DPI
|
||||
local uox = math.floor((PW - 160 * Sp) / 2) / DPI
|
||||
local uoy = math.floor((PH - 144 * Sp) / 2) / DPI
|
||||
local uvpw, uvph = 160 * Ux, 144 * Uy
|
||||
U.log(("emulating %dx%d px at dpi %.4f, fit scale %d"):format(PW, PH, DPI, Sp))
|
||||
|
||||
-- what the pre-fix Renderer handed LOVE: fractional units with a half
|
||||
-- framebuffer pixel of bias, which LOVE 11 truncates away
|
||||
local function scissorOld(x, y, w, h)
|
||||
local x2, y2 = math.min(x + w, uox + uvpw), math.min(y + h, uoy + uvph)
|
||||
x, y = math.max(x, uox), math.max(y, uoy)
|
||||
if x2 <= x or y2 <= y then return false end
|
||||
local px1, py1 = math.floor(x * DPI), math.floor(y * DPI)
|
||||
local px2, py2 = math.ceil(x2 * DPI), math.ceil(y2 * DPI)
|
||||
love.graphics.setScissor((px1 + 0.5) / DPI, (py1 + 0.5) / DPI,
|
||||
(px2 - px1 + 0.5) / DPI, (py2 - py1 + 0.5) / DPI)
|
||||
return true
|
||||
end
|
||||
|
||||
local function renderProbe(old)
|
||||
love.graphics.setCanvas(probe)
|
||||
love.graphics.clear(0, 0, 0, 1)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
if old then
|
||||
local shader = PaletteFX.shader()
|
||||
love.graphics.setShader(shader)
|
||||
for _, z in ipairs(zones) do
|
||||
PaletteFX.sendColors(shader, z.colors)
|
||||
if scissorOld(uox + z.x * Ux, uoy + z.y * Uy, z.w * Ux, z.h * Uy) then
|
||||
love.graphics.draw(Renderer.canvas, uox, uoy, 0, Ux, Uy)
|
||||
end
|
||||
end
|
||||
love.graphics.setScissor()
|
||||
love.graphics.setShader()
|
||||
else
|
||||
Renderer:blitCanvas(Renderer.canvas, Ux, Uy, zones, Ux, Uy,
|
||||
uox, uoy, uox, uoy, uvpw, uvph, DPI, DPI)
|
||||
end
|
||||
love.graphics.setCanvas()
|
||||
return probe:newImageData()
|
||||
end
|
||||
|
||||
local top = math.floor(uoy * DPI)
|
||||
local bottom = math.floor((uoy + uvph) * DPI)
|
||||
local col = math.floor(uox * DPI) + 2
|
||||
local function blackRows(data)
|
||||
local rows = {}
|
||||
for py = top, math.min(bottom - 1, data:getHeight() - 1) do
|
||||
local r, g, b = data:getPixel(col, py)
|
||||
if r < 0.02 and g < 0.02 and b < 0.02 then rows[#rows + 1] = py end
|
||||
end
|
||||
return rows
|
||||
end
|
||||
|
||||
local dataNew = renderProbe(false)
|
||||
local dataOld = renderProbe(true)
|
||||
local rowsNew, rowsOld = blackRows(dataNew), blackRows(dataOld)
|
||||
U.log(("picture rows %d-%d, sampling the background column at x=%d")
|
||||
:format(top, bottom - 1, col))
|
||||
U.log("pre-fix math left", #rowsOld, "black rows:",
|
||||
#rowsOld > 0 and table.concat(rowsOld, ",") or "none")
|
||||
check("no letterbox row shows through the zone boundaries", #rowsNew == 0)
|
||||
check("the emulated surface reproduces the seam without the fix", #rowsOld > 0)
|
||||
|
||||
local ZOOM, CW, CH = 4, 220, 28
|
||||
local cy = math.max(top, math.floor((uoy + 64 * Uy) * DPI) - math.floor(CH / 2))
|
||||
local cx = math.floor(uox * DPI)
|
||||
local function zoomOf(data)
|
||||
local out = love.image.newImageData(CW * ZOOM, CH * ZOOM)
|
||||
out:mapPixel(function(x, y)
|
||||
return data:getPixel(cx + math.floor(x / ZOOM), cy + math.floor(y / ZOOM))
|
||||
end)
|
||||
return out
|
||||
end
|
||||
local function writePng(imageData, path)
|
||||
local dir = path:match("^(.*)[/\\][^/\\]+$")
|
||||
if dir and dir ~= "" then os.execute('mkdir -p "' .. dir .. '" 2>/dev/null') end
|
||||
local f = io.open(path, "wb")
|
||||
if not f then return false end
|
||||
f:write(imageData:encode("png"):getString())
|
||||
f:close()
|
||||
return true
|
||||
end
|
||||
|
||||
local zoomOld, zoomNew = zoomOf(dataOld), zoomOf(dataNew)
|
||||
check("wrote " .. SHOT_DIR .. "/bug1453_before.png",
|
||||
writePng(zoomOld, SHOT_DIR .. "/bug1453_before.png"))
|
||||
check("wrote " .. SHOT_DIR .. "/bug1453_after.png",
|
||||
writePng(zoomNew, SHOT_DIR .. "/bug1453_after.png"))
|
||||
|
||||
local imgOld = love.graphics.newImage(zoomOld)
|
||||
local imgNew = love.graphics.newImage(zoomNew)
|
||||
local baseDraw = love.draw
|
||||
love.draw = function()
|
||||
baseDraw()
|
||||
local w, h = love.graphics.getDimensions()
|
||||
local s = math.min((w - 24) / imgOld:getWidth(),
|
||||
(h * 0.5 - 48) / (imgOld:getHeight() * 2))
|
||||
local bw = imgOld:getWidth() * s + 16
|
||||
local bh = imgOld:getHeight() * 2 * s + 56
|
||||
local bx, by = (w - bw) / 2, h - bh - 8
|
||||
love.graphics.setColor(0, 0, 0, 0.75)
|
||||
love.graphics.rectangle("fill", bx, by, bw, bh)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.print("BEFORE (dpi " .. DPI .. ")", bx + 8, by + 4)
|
||||
love.graphics.draw(imgOld, bx + 8, by + 20, 0, s, s)
|
||||
love.graphics.print("AFTER", bx + 8, by + 24 + imgOld:getHeight() * s)
|
||||
love.graphics.draw(imgNew, bx + 8, by + 40 + imgOld:getHeight() * s, 0, s, s)
|
||||
end
|
||||
|
||||
PaletteFX.setMode(mode)
|
||||
U.wait(10)
|
||||
U.shot(game, SHOT_DIR .. "/bug1453_title.png")
|
||||
U.log("captured", SHOT_DIR .. "/bug1453_title.png")
|
||||
U.log("The title is live and the pad is yours; the strip along the bottom is")
|
||||
U.log("the row-64 zone boundary of an emulated " .. DPI .. "x Android surface,")
|
||||
U.log("magnified " .. ZOOM .. "x: BEFORE carries the black hairline across the")
|
||||
U.log("picture, AFTER is unbroken off-white. PROBE_DPI=2.75 tries another one.")
|
||||
|
||||
while true do
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,72 @@
|
||||
-- Driver: the TOWN MAP player marker wears the OBJ palette (#1437).
|
||||
-- LoadPlayerSpriteGraphics hands the town map the same walking sheet the
|
||||
-- overworld draws (engine/items/town_map.asm:342), so the marker has to go
|
||||
-- through the OBJ ramp and the shade-0 keying every other sprite gets --
|
||||
-- green on Red, pink on Blue, never a raw white block on the BG ramp.
|
||||
--
|
||||
-- POKEPORT_DRIVER=tests/drivers/town_map_marker_bug1437_test.lua \
|
||||
-- POKEPORT_IDENTITY=bug1437 POKEPORT_TOUCH=0 POKEPORT_VERSION=red \
|
||||
-- SHOT_DIR=/tmp/shots love .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local SpriteRenderer = require("src.render.SpriteRenderer")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
local function check(label, ok)
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
return ok
|
||||
end
|
||||
|
||||
game.save.player.name = "bryan"
|
||||
game.save.visited = {
|
||||
PALLET_TOWN = true, VIRIDIAN_CITY = true, PEWTER_CITY = true,
|
||||
CERULEAN_CITY = true, CELADON_CITY = true,
|
||||
}
|
||||
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
|
||||
U.wait(10)
|
||||
|
||||
local function openMap(mode, tag)
|
||||
game.save.options = game.save.options or {}
|
||||
game.save.options.colors = mode
|
||||
PaletteFX.setMode(mode)
|
||||
U.wait(4)
|
||||
Screens.push(game, "TownMap")
|
||||
U.wait(20)
|
||||
local top = game.stack:top()
|
||||
check(tag .. ": the TOWN MAP is up", top and top.playerQuad ~= nil)
|
||||
-- the marker must be a baked OBJ image, not a fresh newImage of the sheet
|
||||
local sprites = game.data.sprites or {}
|
||||
local red = sprites.SPRITE_RED
|
||||
local colors, group
|
||||
if PaletteFX.usesSpriteObp() then
|
||||
colors, group = PaletteFX.ogObj()
|
||||
else
|
||||
colors, group = PaletteFX.dmgObj()
|
||||
end
|
||||
local want = red and SpriteRenderer.obpImage(red.image, colors, group)
|
||||
check(tag .. ": the marker is the OBP-baked sheet",
|
||||
top and want ~= nil and top.playerSheet == want)
|
||||
-- hold the shot on a frame the blink is showing the marker
|
||||
for _ = 1, 40 do
|
||||
if top.blink < 16 then break end
|
||||
coroutine.yield()
|
||||
end
|
||||
U.shot(game, DIR .. "/bug1437_" .. tag .. ".png")
|
||||
U.tap(game, "b")
|
||||
U.wait(10)
|
||||
end
|
||||
|
||||
openMap("ogred", "ogred")
|
||||
openMap("gbc", "gbc")
|
||||
openMap("og", "og")
|
||||
|
||||
U.log("Open the shots: in OG RED the marker over PALLET TOWN is the green")
|
||||
U.log("boot-ROM trainer, in the other modes it is coloured by the map zone")
|
||||
U.log("like any overworld sprite. A white box, or a red/black marker on")
|
||||
U.log("the BG ramp, is the bug. The pad is yours -- the TOWN MAP is one")
|
||||
U.log("B away and the ITEM bag reopens it.")
|
||||
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
@@ -47,12 +47,32 @@ do
|
||||
T.eq(feet.tile, (0x80 - 6 * 2) - 49, "2ROW base")
|
||||
end
|
||||
|
||||
-- engine/battle_anims/anim_commands.asm:317
|
||||
do
|
||||
local runner = AnimRunner.new({})
|
||||
runner:start(nil)
|
||||
AnimRunner.COMMANDS.battlergfx_1row(runner)
|
||||
local head = findLoaded(runner, "BATTLE_ANIM_GFX_PLAYERHEAD")
|
||||
local feet = findLoaded(runner, "BATTLE_ANIM_GFX_ENEMYFEET")
|
||||
T.eq(head and head.battler, "enemy", "the script command routes the same way")
|
||||
T.eq(head.rows, 2, "$da's macro says 1row, its jumptable slot says _2Row")
|
||||
T.eq(head.tiles, 14, "two enemy rows")
|
||||
T.eq(head.tile, (0x80 - 6 * 2 - 7 * 2) - 49, "at the _2Row base")
|
||||
T.eq(feet.tiles, 12, "two player rows")
|
||||
T.eq(feet.tile, (0x80 - 6 * 2) - 49, "at the _2Row base")
|
||||
end
|
||||
|
||||
do
|
||||
local runner = AnimRunner.new({})
|
||||
runner:start(nil)
|
||||
AnimRunner.COMMANDS.battlergfx_2row(runner)
|
||||
local head = findLoaded(runner, "BATTLE_ANIM_GFX_PLAYERHEAD")
|
||||
local feet = findLoaded(runner, "BATTLE_ANIM_GFX_ENEMYFEET")
|
||||
T.eq(head.rows, 1, "$d9's macro says 2row, its jumptable slot says _1Row")
|
||||
T.eq(head.tiles, 7, "one enemy row")
|
||||
T.eq(head.tile, (0x80 - 6 - 7) - 49, "at the _1Row base")
|
||||
T.eq(feet.tiles, 6, "one player row")
|
||||
T.eq(feet.tile, (0x80 - 6) - 49, "at the _1Row base")
|
||||
end
|
||||
|
||||
T.finish("gen2 battler gfx row attribution bug 1231")
|
||||
|
||||
@@ -151,8 +151,8 @@ do
|
||||
|
||||
local fresh = { mods = {} }
|
||||
SaveData.setModEnabled(fresh, "b", true, "gold")
|
||||
eq(fresh.modsByVersion.gold.b, nil,
|
||||
"no shared flag reads as enabled, so agreeing with it stores nothing")
|
||||
eq(fresh.modsByVersion.gold.b, true,
|
||||
"with no shared flag the answer is stored, not dropped against an assumed default")
|
||||
end
|
||||
|
||||
do
|
||||
|
||||
@@ -357,8 +357,8 @@ local options = OptionsMenu.new(optionsGame, {
|
||||
options = Save.defaultOptions(),
|
||||
})
|
||||
-- The cart's seven rows, then the port's: CONTROLS, audio, speed, display,
|
||||
-- video mode, the mobile-gated touch three (buildRows) and CANCEL.
|
||||
check("twenty-one rows", #OptionsMenu.ROWS, 21)
|
||||
-- video mode, the mobile-gated touch three (buildRows), MAX FPS and CANCEL.
|
||||
check("twenty-two rows", #OptionsMenu.ROWS, 22)
|
||||
check("the cart's rows come first", OptionsMenu.ROWS[7].key, "frame")
|
||||
check("then the rebind screen", OptionsMenu.ROWS[8].id, "controls")
|
||||
check("then the port's audio group", OptionsMenu.ROWS[9].key, "musicVol")
|
||||
|
||||
Reference in New Issue
Block a user