mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
296 lines
11 KiB
Lua
296 lines
11 KiB
Lua
-- Driver: a POTION used in battle prints, moves the HUD bar and spends the
|
|
-- turn (#379). pokered engine/items/item_effects.asm:1-3 leaves
|
|
-- wActionResultOrTookBattleTurn at its success value, and ItemUseMedicine only
|
|
-- clears it on .healingItemNoEffect (:1241), so a medicine that lands costs
|
|
-- the turn; the party-menu bar fill is the field case (#252). Data half:
|
|
-- tests/parity_battle_item_turn.lua. Never under POKEPORT_SPEED: fast-forward
|
|
-- desynchronizes SFX_HEAL_HP from the bar.
|
|
-- POKEPORT_DRIVER=tests/drivers/battle_item_turn_bug379_test.lua POKEPORT_IDENTITY=bug379 POKEPORT_TOUCH=0 POKEPORT_VERSION=red love .
|
|
return function(game)
|
|
local U = dofile("tests/drivers/util.lua")
|
|
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local Bag = require("src.inventory.Bag")
|
|
local BattleState = require("src.battle.BattleState")
|
|
local ItemEffects = require("src.inventory.ItemEffects")
|
|
local PartyMenu = require("src.ui.PartyMenu")
|
|
local TextBox = require("src.render.TextBox")
|
|
|
|
local pass, fail = 0, 0
|
|
local function check(label, ok)
|
|
if ok then pass = pass + 1 else fail = fail + 1 end
|
|
U.log(ok and "PASS" or "FAIL", label)
|
|
return ok
|
|
end
|
|
local function top() return game.stack:top() end
|
|
local function isPicker(s)
|
|
return s ~= nil and (s.screenId == "PartyMenu" or getmetatable(s) == PartyMenu)
|
|
end
|
|
local function isBox(s) return getmetatable(s) == TextBox end
|
|
local function isBag(s) return s ~= nil and s.screenId == "BagMenu" end
|
|
local function inStack(pred)
|
|
for _, s in ipairs(game.stack.states or {}) do
|
|
if pred(s) then return true end
|
|
end
|
|
return false
|
|
end
|
|
local function boxText(s)
|
|
local out = {}
|
|
for _, page in ipairs(s.pages or {}) do
|
|
for _, line in ipairs(page) do out[#out + 1] = line end
|
|
end
|
|
return table.concat(out, " ")
|
|
end
|
|
|
|
-- ---- preconditions -----------------------------------------------------
|
|
U.log("#379 in-battle medicine: machine checks")
|
|
check("POTION takes the HP-medicine path", ItemEffects.healsHP("POTION") == true)
|
|
check("ANTIDOTE does not (status cure, always spent the turn)",
|
|
ItemEffects.healsHP("ANTIDOTE") ~= true)
|
|
for _, id in ipairs({ "POTION", "SUPER_POTION", "ANTIDOTE" }) do
|
|
check(id .. " resolves in the item table", game.data.items[id] ~= nil)
|
|
end
|
|
check("SFX_HEAL_HP is in the generated audio",
|
|
game.data.audio and game.data.audio.sfx
|
|
and game.data.audio.sfx.Heal_HP ~= nil)
|
|
check("BattleState:itemUsed exists (queues the foe's turn)",
|
|
type(BattleState.itemUsed) == "function")
|
|
|
|
local vol = game.save.options and game.save.options.sfxVol
|
|
if vol == 0 then
|
|
U.log("sfxVol is 0: the heal chime will be SILENT, raise it in OPTION first")
|
|
else
|
|
U.log("sfxVol", tostring(vol), "-- expect the heal chime under the bar drain")
|
|
end
|
|
|
|
-- use() must hand back the pre-heal HP; without it neither bar has a start
|
|
do
|
|
local scratch = Pokemon.new(game.data, "BULBASAUR", 20)
|
|
scratch.hp = 4
|
|
local scratchSave = { party = { scratch }, inventory = {}, flags = {},
|
|
pokedex = { seen = {}, owned = {} } }
|
|
local result, msgs, extra = ItemEffects.use(game.data, scratchSave,
|
|
"POTION", scratch)
|
|
check("POTION on a hurt mon reports consumed", result == "consumed")
|
|
check("...and hands back the pre-heal HP",
|
|
type(extra) == "table" and extra.healedFrom == 4)
|
|
check("...with the restored-HP message",
|
|
type(msgs) == "table" and type(msgs[1]) == "string"
|
|
and msgs[1]:find("restored", 1, true) ~= nil)
|
|
end
|
|
|
|
-- ---- fixture -----------------------------------------------------------
|
|
local lead = Pokemon.new(game.data, "CHARIZARD", 50)
|
|
local second = Pokemon.new(game.data, "PIKACHU", 30)
|
|
lead.hp = 12
|
|
game.save.party = { lead, second }
|
|
game.save.player.name = "RED"
|
|
for _, id in ipairs({ "POTION", "SUPER_POTION", "ANTIDOTE" }) do
|
|
Bag.add(game.save, id, 9)
|
|
end
|
|
|
|
-- ROUTE_1 is open field with tall grass either side of the path
|
|
-- (data/generated/maps.lua ROUTE_1); the cell comes off the loaded map so a
|
|
-- map edit degrades to somewhere else in the grass instead of a wall.
|
|
U.teleport(game, "ROUTE_1", 5, 5, "down")
|
|
U.wait(10)
|
|
local ow = game.overworld
|
|
local map = ow.map
|
|
local gx, gy
|
|
if not map:isWalkableCell(5, 5) then
|
|
for cy = 0, map.heightCells - 1 do
|
|
for cx = 0, map.widthCells - 1 do
|
|
if map:isGrassCell(cx, cy) and map:isWalkableCell(cx, cy) then
|
|
gx, gy = cx, cy
|
|
break
|
|
end
|
|
end
|
|
if gx then break end
|
|
end
|
|
if gx then
|
|
U.teleport(game, "ROUTE_1", gx, gy, "down")
|
|
U.wait(10)
|
|
ow = game.overworld
|
|
end
|
|
end
|
|
check("player is standing on a walkable ROUTE_1 cell",
|
|
ow.map:isWalkableCell(ow.player.cellX, ow.player.cellY))
|
|
|
|
-- ---- navigation --------------------------------------------------------
|
|
local function mashUntil(cond, max)
|
|
for _ = 1, max or 120 do
|
|
if cond() then return true end
|
|
U.tap(game, "a")
|
|
U.wait(4)
|
|
end
|
|
return cond()
|
|
end
|
|
|
|
-- FIGHT PKMN / ITEM RUN (BattleState.menuIndex): down from FIGHT is ITEM
|
|
local function openBagFrom(battle)
|
|
for _ = 1, 30 do
|
|
if battle.menuIndex == 3 then break end
|
|
U.tap(game, battle.menuIndex > 2 and "up" or "down")
|
|
U.wait(4)
|
|
end
|
|
if battle.menuIndex ~= 3 then return nil, "ITEM cell unreachable" end
|
|
U.tap(game, "a")
|
|
U.wait(20)
|
|
local bag = top()
|
|
if not isBag(bag) then return nil, "bag never opened" end
|
|
return bag
|
|
end
|
|
|
|
local function cursorTo(menu, want)
|
|
for _ = 1, 40 do
|
|
if not menu or menu.index == want then return menu and menu.index == want end
|
|
U.tap(game, menu.index < want and "down" or "up")
|
|
U.wait(3)
|
|
end
|
|
return menu.index == want
|
|
end
|
|
|
|
local function chooseItem(bag, id)
|
|
local row
|
|
for i, r in ipairs(bag.items or {}) do
|
|
if r.value == id then row = i break end
|
|
end
|
|
if not row or not cursorTo(bag, row) then return false end
|
|
U.tap(game, "a") -- in battle A uses it straight away, no USE / TOSS
|
|
U.wait(16)
|
|
return true
|
|
end
|
|
|
|
local function newFight()
|
|
local battle = BattleState.newWild(game, "PIDGEY", 8)
|
|
battle.onFinish = function() end
|
|
game.overworld:pushBattle(battle)
|
|
mashUntil(function() return battle.phase == "menu" end)
|
|
return battle
|
|
end
|
|
|
|
-- ======== scripted run: POTION on the hurt lead ==========================
|
|
U.log("#379 scripted run: POTION on a 12 HP CHARIZARD, mid-battle")
|
|
local battle = newFight()
|
|
check("the wild battle reached its FIGHT menu", battle.phase == "menu")
|
|
|
|
local turns, ends = 0, 0
|
|
local realAction, realEnd = battle.executeAction, battle.endOfTurn
|
|
battle.executeAction = function(self, ...)
|
|
turns = turns + 1
|
|
return realAction(self, ...)
|
|
end
|
|
battle.endOfTurn = function(self, ...)
|
|
ends = ends + 1
|
|
return realEnd(self, ...)
|
|
end
|
|
|
|
local bag, why = openBagFrom(battle)
|
|
check("ITEM opened the bag" .. (why and (" (" .. why .. ")") or ""), bag ~= nil)
|
|
if bag then
|
|
U.shot(game, DIR .. "/bug379_bag_in_battle.png")
|
|
check("POTION chosen from the bag", chooseItem(bag, "POTION"))
|
|
local picker = top()
|
|
check("the party picker opened to pick a target", isPicker(picker))
|
|
if isPicker(picker) then
|
|
check("keepOpen is off in battle (the fill is the field case, #252)",
|
|
picker.keepOpen ~= true)
|
|
cursorTo(picker, 1)
|
|
U.tap(game, "a")
|
|
U.wait(10)
|
|
|
|
check("the mon was healed", lead.hp > 12)
|
|
check("no party-menu bar fill was started in battle", picker.heal == nil)
|
|
check("the picker popped itself", not inStack(isPicker))
|
|
check("the bag list closed underneath it", not inStack(isBag))
|
|
|
|
-- THE DEFECT: the animate branch swallowed this message, and with it the
|
|
-- itemUsed tail that spends the turn
|
|
for _ = 1, 60 do
|
|
if isBox(top()) then break end
|
|
U.wait(1)
|
|
end
|
|
local box = top()
|
|
check("the restored-HP message printed (#379)", isBox(box))
|
|
if isBox(box) then
|
|
U.wait(50)
|
|
local said = boxText(box)
|
|
U.log("box reads:", said)
|
|
check("...and it is the restored-HP line",
|
|
said:find("restored", 1, true) ~= nil)
|
|
U.shot(game, DIR .. "/bug379_message_in_battle.png")
|
|
end
|
|
|
|
-- the turn: drain, the foe's move, end-of-turn, back to the menu
|
|
for _ = 1, 400 do
|
|
if battle.phase == "menu" and #battle.queue == 0 then break end
|
|
U.tap(game, "a")
|
|
U.wait(4)
|
|
end
|
|
check("the foe took its turn after the item (#379)", turns >= 1)
|
|
check("end-of-turn effects ran (#379)", ends >= 1)
|
|
check("and the FIGHT menu came back", battle.phase == "menu")
|
|
U.shot(game, DIR .. "/bug379_back_on_menu.png")
|
|
end
|
|
end
|
|
|
|
-- ======== control: a refused POTION is free =============================
|
|
U.log("#379 control: a POTION on a full-HP mon is refused, turn NOT spent")
|
|
lead.hp = lead.stats.hp
|
|
local freeTurns = 0
|
|
battle.executeAction = function(self, ...)
|
|
freeTurns = freeTurns + 1
|
|
return realAction(self, ...)
|
|
end
|
|
local held = game.save.inventory.POTION or 0
|
|
local bag2 = openBagFrom(battle)
|
|
if check("the bag reopened for the refusal", bag2 ~= nil) then
|
|
chooseItem(bag2, "POTION")
|
|
local picker = top()
|
|
if isPicker(picker) then
|
|
cursorTo(picker, 1)
|
|
U.tap(game, "a")
|
|
U.wait(12)
|
|
end
|
|
for _ = 1, 60 do
|
|
if isBox(top()) then break end
|
|
U.wait(1)
|
|
end
|
|
local box = top()
|
|
if check("the refusal printed", isBox(box)) then
|
|
U.wait(40)
|
|
check("...as \"It won't have any effect.\"",
|
|
boxText(box):find("effect", 1, true) ~= nil)
|
|
U.shot(game, DIR .. "/bug379_refused.png")
|
|
end
|
|
check("a refused item costs no turn (item_effects.asm:1241)", freeTurns == 0)
|
|
check("and the POTION was not consumed",
|
|
(game.save.inventory.POTION or 0) == held)
|
|
end
|
|
|
|
U.log(("machine checks: %d passed, %d failed"):format(pass, fail))
|
|
|
|
-- ---- re-arm and hand the pad over --------------------------------------
|
|
for _ = 1, 200 do
|
|
if battle.phase == "menu" and #battle.queue == 0 then break end
|
|
U.tap(game, "a")
|
|
U.wait(4)
|
|
end
|
|
lead.hp = 12
|
|
local handoff = openBagFrom(battle)
|
|
if handoff then
|
|
for i, r in ipairs(handoff.items or {}) do
|
|
if r.value == "POTION" then cursorTo(handoff, i) break end
|
|
end
|
|
end
|
|
|
|
U.log("The ITEMS list is open mid-battle with the cursor on POTION. Press A,")
|
|
U.log("pick CHARIZARD: the picker and the list both close, \"CHARIZARD's HP was")
|
|
U.log("restored!\" prints with the heal chime, the HUD bar climbs, then PIDGEY")
|
|
U.log("attacks before FIGHT comes back. #379 was no message and a free heal.")
|
|
|
|
while true do
|
|
coroutine.yield()
|
|
end
|
|
end
|