mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
167 lines
6.4 KiB
Lua
167 lines
6.4 KiB
Lua
-- A stone evolution started from the bag must not be cancelable (#883).
|
|
--
|
|
-- engine/items/item_effects.asm ItemUseEvoStone sets wForceEvolution before
|
|
-- `call TryEvolvingMon`, and engine/movie/evolution.asm
|
|
-- Evolution_CheckForCancel reads the joypad but throws the B press away while
|
|
-- that flag is set (#290). So the B abort is a level-up/rare-candy behavior
|
|
-- only: a stone is removed from the bag the moment it is used, and an
|
|
-- evolution the player can cancel out of would eat the stone for nothing.
|
|
--
|
|
-- src/ui/EvolutionState.lua encodes the flag as `via`: cancelable is
|
|
-- (via ~= "TRADE" and via ~= "ITEM"). The bag's stone branch omitted the
|
|
-- argument entirely, so `via` arrived nil and the movie accepted B. The
|
|
-- assertion here is on the value that reaches the screen, which is the only
|
|
-- thing standing between the two behaviors.
|
|
--
|
|
-- ROM-free: the fixture dataset plus a registry-supplied EvolutionState, so
|
|
-- the real Screens.push resolution runs and no sprite is ever loaded.
|
|
-- luajit tests/engine/evo_stone_cancel_bug883_test.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
love = love or require("tests.love_stub")
|
|
|
|
-- Lazily required inside the use branches; seeding package.loaded first keeps
|
|
-- the suite silent and free of a real Font atlas.
|
|
package.loaded["src.core.Sound"] = {
|
|
play = function() end,
|
|
playCry = function() end,
|
|
}
|
|
package.loaded["src.render.TextBox"] = {
|
|
new = function(_, text, done) return { textBox = true, text = text, done = done } end,
|
|
}
|
|
-- BagMenu and PartyMenu bind TextBox at require time, so they load against the
|
|
-- stub; Screens caches its factory per id and must be told to forget.
|
|
package.loaded["src.ui.BagMenu"] = nil
|
|
package.loaded["src.ui.PartyMenu"] = nil
|
|
local BagMenu = require("src.ui.BagMenu")
|
|
local PartyMenu = require("src.ui.PartyMenu")
|
|
local Screens = require("src.ui.Screens")
|
|
Screens.invalidate()
|
|
|
|
local Fixtures = require("tests.modkit.fixtures")
|
|
local Bag = require("src.inventory.Bag")
|
|
local Pokemon = require("src.pokemon.Pokemon")
|
|
local EvolutionState = require("src.ui.EvolutionState")
|
|
|
|
local Data = Fixtures.fresh()
|
|
-- The fixture item table carries no stone, and ItemEffects keys its stone
|
|
-- branch on the id; BagMenu only reads name/keyItem off the def.
|
|
Data.items.THUNDER_STONE = {
|
|
id = "THUNDER_STONE", index = 33, name = "THUNDERSTONE", price = 2100,
|
|
tossable = true,
|
|
}
|
|
-- and no fixture species evolves, so give A the stone evolution the branch
|
|
-- looks for (evo.method == "ITEM" and evo.item == the stone used).
|
|
Data.pokemon.FIXMON_A.evolutions = {
|
|
{ method = "ITEM", item = "THUNDER_STONE", species = "FIXMON_B" },
|
|
}
|
|
|
|
-- The seam: Screens resolves an id through game.data.screens before falling
|
|
-- back to the builtin module, which is the same path a mod-replaced screen
|
|
-- takes. Recording the factory here catches exactly what Evolution.evolve
|
|
-- forwards, with no monkeypatching of Screens itself.
|
|
local pushed
|
|
Data.screens = Data.screens or {}
|
|
Data.screens.EvolutionState = function(game, mon, newSpecies, onDone, via)
|
|
pushed = { game = game, mon = mon, newSpecies = newSpecies,
|
|
onDone = onDone, via = via }
|
|
return { evoRecorder = true }
|
|
end
|
|
Screens.invalidate()
|
|
|
|
local function freshGame()
|
|
local mon = Pokemon.new(Data, "FIXMON_A", 20)
|
|
local game = {
|
|
data = Data,
|
|
save = {
|
|
party = { mon },
|
|
player = { name = "RED", id = 1 },
|
|
inventory = {},
|
|
options = {},
|
|
flags = {},
|
|
money = 0,
|
|
},
|
|
}
|
|
game.stack = {
|
|
states = {},
|
|
push = function(self, s) table.insert(self.states, s) end,
|
|
pop = function(self) return table.remove(self.states) end,
|
|
top = function(self) return self.states[#self.states] end,
|
|
}
|
|
-- one button edge per update, the way Input reports a fixed step
|
|
game.input = { pressed = nil }
|
|
function game.input:wasPressed(b) return self.pressed == b end
|
|
Bag.add(game.save, "THUNDER_STONE", 1)
|
|
return game, mon
|
|
end
|
|
|
|
local function isPicker(s) return getmetatable(s) == PartyMenu end
|
|
|
|
local function rowFor(list, id)
|
|
for i, r in ipairs(list.items) do
|
|
if r.value == id then return i end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Open the bag, put the cursor on the stone, choose it, take USE off the
|
|
-- USE/TOSS box, then press A on the party picker.
|
|
local function useStone(game)
|
|
local list = BagMenu.new(game, {})
|
|
game.stack:push(list)
|
|
local row = rowFor(list, "THUNDER_STONE")
|
|
if not row then return nil, "no THUNDER_STONE row in the bag" end
|
|
list.index = row
|
|
list.onChoose(list.items[row], list)
|
|
local sub = game.stack:top()
|
|
if sub and sub.items and sub.items[1] and sub.items[1].onSelect then
|
|
game.stack:pop() -- the USE/TOSS Menu pops itself on select
|
|
sub.items[1].onSelect()
|
|
end
|
|
local picker = game.stack:top()
|
|
if not isPicker(picker) then return nil, "party picker never opened" end
|
|
game.input.pressed = "a"
|
|
picker:update(1 / 60)
|
|
game.input.pressed = nil
|
|
return list
|
|
end
|
|
|
|
do
|
|
local game, mon = freshGame()
|
|
local list, why = useStone(game)
|
|
if check(list ~= nil, "the bag opened and reached the picker: " .. tostring(why)) then
|
|
if check(pushed ~= nil, "the stone use pushed the evolution screen") then
|
|
eq(pushed.newSpecies, "FIXMON_B", "and it is the stone's evolution")
|
|
eq(pushed.mon, mon, "for the mon the stone was used on")
|
|
eq(pushed.via, "ITEM",
|
|
"the evolution runs as via = \"ITEM\" (wForceEvolution), which is "
|
|
.. "what makes it non-cancelable (#883)")
|
|
end
|
|
eq(game.save.inventory.THUNDER_STONE, nil,
|
|
"the stone is already gone by then, so a cancel would cost it for "
|
|
.. "nothing")
|
|
end
|
|
end
|
|
|
|
-- The value only matters because of what EvolutionState does with it, so
|
|
-- assert that half against the real constructor rather than trusting the
|
|
-- comment. new() loads sprites through pcall and plays music through the
|
|
-- stubbed Sound, so it is safe headless.
|
|
do
|
|
local game = freshGame()
|
|
local mon = game.save.party[1]
|
|
local stoneEvo = EvolutionState.new(game, mon, "FIXMON_B", nil, "ITEM")
|
|
check(stoneEvo.cancelable == false,
|
|
"EvolutionState refuses B for a stone evolution (evolution.asm "
|
|
.. "Evolution_CheckForCancel with wForceEvolution set)")
|
|
local levelEvo = EvolutionState.new(game, mon, "FIXMON_B", nil, "LEVEL")
|
|
check(levelEvo.cancelable == true,
|
|
"and still honours B for a level-up evolution, so the fix did not "
|
|
.. "silently disable the cancel everywhere (#290, #213)")
|
|
end
|
|
|
|
T.finish()
|