mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-22 13:36:14 +02:00
Fix transition state pop, Gen 2 Bide/Sleep Talk/exp bugs, pp repair, TM pocket capacity and Bill's PC arrows
CLOSES #1663, CLOSES #1664, CLOSES #1665, CLOSES #1666, CLOSES #1667, CLOSES #1668, CLOSES #1670, CLOSES #1672
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
-- BIDE on the real Gold battle screen: the PP counter in the move list, and
|
||||
-- the lock that keeps a storing mon on the move it started (#1664, #1665).
|
||||
--
|
||||
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/gold_bide_lock_bug1664_1665.lua \
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bide love .
|
||||
--
|
||||
-- data/moves/effects.asm:795-800 puts `storeenergy` ahead of `doturn`, and
|
||||
-- BattleCommand_DoTurn masks SUBSTATUS_BIDE out of the PP spend
|
||||
-- (engine/battle/effect_commands.asm:977-979), so the whole three-turn Bide
|
||||
-- costs the one PP the opening turn paid. The shots are the point: the
|
||||
-- number beside BIDE in the move list must read the same on 01, 02 and 03.
|
||||
--
|
||||
-- The lock is ParsePlayerAction's own arm (engine/battle/core.asm:569-576),
|
||||
-- which sits INSIDE the FIGHT branch and skips MoveSelectionScreen only -- so
|
||||
-- the 2x2 menu still opens, a switch is still legal, and using an item runs
|
||||
-- .reset_bide (:627-629) and CANCELS the store. Block 2 submits TACKLE in
|
||||
-- the middle of a Bide on purpose: the engine has to answer with the Bide.
|
||||
--
|
||||
-- NOT covered here, and deliberately: src/ui/gen2/BattleState.lua still draws
|
||||
-- the move list on a storing turn. The cart jumps past it. That half is a
|
||||
-- screen change and is called out in the fix report rather than faked.
|
||||
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 drain(game, screen, frames)
|
||||
for _ = 1, (frames or 400) do
|
||||
if screen.phase == "menu" and #screen.queue == 0 and not screen.anim then
|
||||
return true
|
||||
end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function giveMoves(mon, game, 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
|
||||
|
||||
local function newWild(game, species, level, moves)
|
||||
local mon = Mon.new(game.data, species, level)
|
||||
if moves then giveMoves(mon, game, moves) end
|
||||
return mon
|
||||
end
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bide"
|
||||
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 function pp(mon, slot) return mon.moves[slot].pp end
|
||||
|
||||
-- ------------------------------------------------------------------ 1
|
||||
-- One Bide, start to finish, with the move list open between turns. The
|
||||
-- foe is a SPLASH-only RATTATA so nothing interrupts the store.
|
||||
local player = Mon.new(game.data, "SNORLAX", 40)
|
||||
giveMoves(player, game, { "BIDE", "TACKLE" })
|
||||
game.save.party = { player }
|
||||
game.save.inventory = { POTION = 5 }
|
||||
local foe = newWild(game, "RATTATA", 20, { "TACKLE" })
|
||||
assert(world:startBattle({ wild = foe }), "startBattle failed")
|
||||
local screen = battleScreen(game)
|
||||
drain(game, screen, 200)
|
||||
local opening = pp(player, 1)
|
||||
print(("[driver] BIDE opens at %d PP"):format(opening))
|
||||
|
||||
screen:submit({ kind = "move", move = "BIDE" })
|
||||
drain(game, screen, 400)
|
||||
U.shot(game, out .. "/01-selected.png")
|
||||
local afterSelect = pp(player, 1)
|
||||
print(("[driver] after the SELECTION turn: %d PP"):format(afterSelect))
|
||||
check(afterSelect == opening - 1,
|
||||
"the opening turn pays its one PP through doturn")
|
||||
check(screen.battle:forcedMove(player) == "BIDE",
|
||||
"and ParsePlayerAction's bide arm holds the FIGHT choice")
|
||||
|
||||
-- The middle of the store, with TACKLE deliberately submitted: the cart
|
||||
-- never re-reads the move list, so wCurPlayerMove is still the BIDE.
|
||||
local tackleBefore = pp(player, 2)
|
||||
screen:submit({ kind = "move", move = "TACKLE" })
|
||||
drain(game, screen, 400)
|
||||
U.shot(game, out .. "/02-storing.png")
|
||||
print(("[driver] after a STORING turn: BIDE %d PP, TACKLE %d PP")
|
||||
:format(pp(player, 1), pp(player, 2)))
|
||||
check(pp(player, 1) == afterSelect, "a storing turn spends no PP at all")
|
||||
check(pp(player, 2) == tackleBefore,
|
||||
"and the move the menu offered was never run")
|
||||
|
||||
-- The release. Whatever the roll, the store is two or three turns
|
||||
-- (UnleashEnergy's `BattleRandom / and 1 / inc a / inc a`,
|
||||
-- move_effects/bide.asm:88-92), so keep pressing FIGHT until it lets go.
|
||||
local foeBefore = foe.hp
|
||||
for _ = 1, 3 do
|
||||
if not screen.battle:forcedMove(player) then break end
|
||||
screen:submit({ kind = "move", move = "BIDE" })
|
||||
drain(game, screen, 400)
|
||||
end
|
||||
U.shot(game, out .. "/03-unleashed.png")
|
||||
print(("[driver] after the RELEASE: %d PP, foe %d -> %d HP")
|
||||
:format(pp(player, 1), foeBefore, foe.hp))
|
||||
check(pp(player, 1) == afterSelect,
|
||||
"the whole Bide cost exactly one PP, the way the cart charges it")
|
||||
check(screen.battle:forcedMove(player) == nil, "and the lock is released")
|
||||
for _ = 1, 400 do
|
||||
if not game.stack:top() or not game.stack:top().battle then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
U.wait(60)
|
||||
|
||||
-- ------------------------------------------------------------------ 2
|
||||
-- .reset_bide: a nonzero wBattlePlayerAction that is not
|
||||
-- BATTLEPLAYERACTION_SWITCH clears SUBSTATUS_BIDE (core.asm:572-573,
|
||||
-- :627-629), so opening the PACK mid-store throws the stored damage away.
|
||||
local bider = Mon.new(game.data, "SNORLAX", 40)
|
||||
giveMoves(bider, game, { "BIDE", "TACKLE" })
|
||||
bider.hp = math.max(1, bider.hp - 30)
|
||||
game.save.party = { bider }
|
||||
game.save.inventory = { POTION = 5 }
|
||||
local foe2 = newWild(game, "RATTATA", 20, { "TACKLE" })
|
||||
assert(world:startBattle({ wild = foe2 }), "startBattle failed")
|
||||
screen = battleScreen(game)
|
||||
drain(game, screen, 200)
|
||||
screen:submit({ kind = "move", move = "BIDE" })
|
||||
drain(game, screen, 400)
|
||||
check(screen.battle:forcedMove(bider) == "BIDE", "the second Bide is up")
|
||||
screen:useItem("POTION")
|
||||
drain(game, screen, 400)
|
||||
U.shot(game, out .. "/04-item-cancelled.png")
|
||||
print("[driver] after the PACK: forcedMove="
|
||||
.. tostring(screen.battle:forcedMove(bider)))
|
||||
check(screen.battle:forcedMove(bider) == nil,
|
||||
"using an item cancels the Bide, as .reset_bide does")
|
||||
check(screen.battle:volatile(bider).bideStored == nil,
|
||||
"and the damage it had banked goes with it")
|
||||
for _ = 1, 400 do
|
||||
if not game.stack:top() or not game.stack:top().battle then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
end
|
||||
|
||||
print("[driver] NOTE: the move list is still drawn on a storing turn; the "
|
||||
.. "cart's bide arm skips MoveSelectionScreen and that half lives in "
|
||||
.. "src/ui/gen2/BattleState.lua")
|
||||
if #failures > 0 then
|
||||
for _, what in ipairs(failures) do print("[FAIL] " .. what) end
|
||||
error(#failures .. " bide checks failed")
|
||||
end
|
||||
print("[driver] all bide checks passed")
|
||||
print("[driver] shots in " .. out)
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
-- #1672: Bill's PC MOVE screen -- the box-name arrows.
|
||||
--
|
||||
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold POKEPORT_TOUCH=0 \
|
||||
-- POKEPORT_DRIVER=tests/drivers/gold_billspc_arrows_bug1672.lua \
|
||||
-- perl -e 'alarm 420; exec @ARGV' \
|
||||
-- python3 -c "import pty; pty.spawn(['love','.'])"
|
||||
-- POKEPORT_SHOT_DIR=/tmp/gold-bug1672-arrows (default)
|
||||
--
|
||||
-- BillsPC_MoveMonWOMail_BoxNameAndArrows writes $5f at hlcoord 8, 1 and $5e at
|
||||
-- hlcoord 19, 1 (engine/pokemon/bills_pc.asm:957-963), replacing the box-name
|
||||
-- Textbox's own side borders. Only _MovePKMNWithoutMail calls it: .Init (:545)
|
||||
-- and .PrepInsertCursor (:698). The withdraw (:299) and deposit (:56) inits
|
||||
-- call bare BillsPC_BoxName (:965) and are the negative controls here.
|
||||
--
|
||||
-- What to look for in each shot: a solid LEFT-pointing triangle in the left
|
||||
-- border of the name box and a solid RIGHT-pointing one in the right border,
|
||||
-- both level with the name, on every move-mode shot and on neither of the last
|
||||
-- two. Getting them the wrong way round is the failure this exists to catch.
|
||||
--
|
||||
-- The run ends with the MOVE 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")
|
||||
|
||||
return function(game)
|
||||
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/gold-bug1672-arrows"
|
||||
|
||||
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)
|
||||
assert(game.world and game.world.map, "gold world did not boot")
|
||||
local save, data = game.save, game.data
|
||||
|
||||
local function mon(species, level) return Mon.new(data, species, level) end
|
||||
|
||||
-- IsAnyMonHoldingMail refuses the whole screen (src/ui/gen2/PcMenu.lua:303),
|
||||
-- so the seeded party carries no mail.
|
||||
save.party = {}
|
||||
for _, species in ipairs({ "CYNDAQUIL", "PIDGEY", "SENTRET" }) do
|
||||
save.party[#save.party + 1] = mon(species, 12)
|
||||
end
|
||||
local stored = Boxes.box(save, 1)
|
||||
for i = #stored, 1, -1 do stored[i] = nil end
|
||||
for i, species in ipairs({ "GEODUDE", "ZUBAT", "RATTATA" }) do
|
||||
stored[i] = mon(species, 10 + i)
|
||||
end
|
||||
Boxes.rename(save, 1, "GRASS")
|
||||
save.currentBox = 1
|
||||
|
||||
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
|
||||
|
||||
-- ---- 1. the move list on a renamed box -----------------------------------
|
||||
local menu = openBox("move")
|
||||
U.log("01 move, box GRASS:", "want both arrows flanking GRASS on row 1")
|
||||
U.shot(game, out .. "/01-move-box.png")
|
||||
|
||||
-- ---- 2. LEFT onto the PARTY ----------------------------------------------
|
||||
-- BillsPC_BoxName's `.party` arm; only the move screen ever loads box 0.
|
||||
tap("left")
|
||||
U.log("02 move, PARTY:", "boxIndex " .. tostring(menu.boxIndex) ..
|
||||
", want 0 and both arrows still up")
|
||||
U.shot(game, out .. "/02-move-party.png")
|
||||
tap("right")
|
||||
|
||||
-- ---- 3. the MOVE / STATS / CANCEL submenu --------------------------------
|
||||
tap("a")
|
||||
U.log("03 submenu:", "phase " .. tostring(menu.phase) ..
|
||||
", want submenu and both arrows still up")
|
||||
U.shot(game, out .. "/03-move-submenu.png")
|
||||
|
||||
-- ---- 4. the insert cursor (.PrepInsertCursor rewrites them) --------------
|
||||
tap("a", 10)
|
||||
U.log("04 insert:", "phase " .. tostring(menu.phase) ..
|
||||
", want insert and both arrows still up")
|
||||
U.shot(game, out .. "/04-move-insert.png")
|
||||
close()
|
||||
|
||||
-- ---- 5/6. the negative controls ------------------------------------------
|
||||
openBox("withdraw")
|
||||
U.log("05 withdraw:", "want plain border tiles, NO arrows")
|
||||
U.shot(game, out .. "/05-withdraw-none.png")
|
||||
close()
|
||||
|
||||
openBox("deposit")
|
||||
U.log("06 deposit:", "want plain border tiles, NO arrows")
|
||||
U.shot(game, out .. "/06-deposit-none.png")
|
||||
close()
|
||||
|
||||
openBox("move")
|
||||
U.log("done -- the MOVE screen is open; the controls are yours")
|
||||
end
|
||||
@@ -252,6 +252,11 @@ return function(game)
|
||||
dep.index = 2 -- the mon holding FLOWER MAIL
|
||||
show("23b-pc-deposit-mail", dep)
|
||||
|
||||
-- Only the move list gets the box-name arrows, $5f left and $5e right
|
||||
-- (engine/pokemon/bills_pc.asm:957-963); 22 and 23 above are the controls.
|
||||
local mv = BoxMenu.new(game, { save = save, mode = "move" })
|
||||
show("23c-pc-move-arrows", mv)
|
||||
|
||||
-- The two clock screens NEW GAME and Mom open (timeset.asm InitClock and
|
||||
-- SetDayOfWeek), each at its picker rather than at its opening page.
|
||||
local clock = InitClock.new(game, { save = save })
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
--
|
||||
-- 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
|
||||
-- (engine/pokemon/status_screen.asm:238-287). 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.
|
||||
--
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
-- data/maps/force_bike_surf.asm:5 / engine/overworld/player_state.asm:34-72,
|
||||
-- home/overworld.asm:690-703 (the map-change fade has no fade back in).
|
||||
-- POKEPORT_DRIVER=tests/drivers/warp_midpoint_box_bug1663_test.lua \
|
||||
-- POKEPORT_IDENTITY=bug1663 POKEPORT_TOUCH=0 POKEPORT_VERSION=red love .
|
||||
-- Ride into the Route 16 gate, drop the BICYCLE inside, then walk back out
|
||||
-- the west door. The warp lands on a forced-bike tile, so the refusal box
|
||||
-- opens from inside the fade's midpoint: it must be on screen when the fade
|
||||
-- ends (#1663 ate it), and dismissing it must leave the player on Route 16's
|
||||
-- (17,10), not shoved into the gate wall at (18,10) (#1548).
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
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 where()
|
||||
local ow = game.overworld
|
||||
return ow.map.id, ow.player.cellX, ow.player.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) }
|
||||
game.save.inventory.BICYCLE = 1
|
||||
game.save.onBike, game.save.forcedBike = false, nil
|
||||
|
||||
-- (16,10) is the open cell west of the gate door; (17,10) is the forced
|
||||
-- tile the door sits on, so the step onto it mounts and then warps
|
||||
U.teleport(game, "ROUTE_16", 16, 10, "right")
|
||||
U.wait(20)
|
||||
U.hold(game, "right", 40)
|
||||
U.wait(40)
|
||||
local map, x, y = where()
|
||||
U.log(("rode east into the gate -> %s (%d,%d)"):format(map, x, y))
|
||||
check("the bike carried the player into the gate",
|
||||
map == "ROUTE_16_GATE_1F")
|
||||
|
||||
bikeless()
|
||||
U.hold(game, "left", 40)
|
||||
-- the map-change fade is 32 frames and hands back with no fade in
|
||||
U.wait(50)
|
||||
|
||||
map, x, y = where()
|
||||
local top = game.stack:top()
|
||||
U.log(("walked back out -> %s (%d,%d), top=%s"):format(map, x, y,
|
||||
tostring(getmetatable(top) == TextBox and "TextBox" or top)))
|
||||
check("the west door lands back on Route 16", map == "ROUTE_16")
|
||||
check("on the forced tile the gate exit warps to", x == 17 and y == 10)
|
||||
check("the refusal box the warp opened is on screen",
|
||||
getmetatable(top) == TextBox)
|
||||
U.shot(game, SHOT_DIR .. "/bug1663_refusal.png")
|
||||
|
||||
-- close it: the shove back east is refused by collision, (18,10) is wall
|
||||
for _ = 1, 12 do
|
||||
if game.stack:top() == game.overworld then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(25)
|
||||
end
|
||||
U.wait(30)
|
||||
map, x, y = where()
|
||||
local walkable = game.overworld.map:isWalkableCell(x, y)
|
||||
U.log(("after the refusal -> %s (%d,%d) walkable=%s")
|
||||
:format(map, x, y, tostring(walkable)))
|
||||
check("dismissing it leaves the player on a walkable cell", walkable)
|
||||
check("and not inside the gate wall at (18,10)", not (x == 18 and y == 10))
|
||||
U.shot(game, SHOT_DIR .. "/bug1663_after.png")
|
||||
|
||||
U.log(ok and "all clear" or "a check failed")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
Reference in New Issue
Block a user