mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
d03d2af5f8
Both call sites -- Game2:usePartyItem (the field pack) and BattleState:useItem (the battle pack) -- asked ItemEffects.partyAction for an item's family with no `data` argument, even though every other call in the same functions (useOnMon, usePpItem, applyPartyItem) passed it through correctly. partyAction resolves through recordFor, which reads data.gen2ItemEffects when given a dataset and falls back to the module's own built-in RECORDS table when not -- so with no data, a mod's own item_effects record was invisible and every mod-defined Gen 2 field or battle item resolved to a nil action, falling straight through to "isn't going to help here" / "isn't going to help here" without ever opening the party picker. Both now pass the live dataset (self.data on Game2, self.game.data on the battle screen) the same way their sibling calls already did.
560 lines
23 KiB
Lua
560 lines
23 KiB
Lua
-- The PACK inside a Gen 2 battle: every family engine/items/item_effects.asm
|
|
-- can spend on a party mon, driven through the real BattleState:useItem.
|
|
--
|
|
-- luajit tests/gen2_battle_items_test.lua
|
|
--
|
|
-- ROM-free. The HP line already had its own flow (gen2_battle_ui_test pins
|
|
-- it); what is asserted here is the rest of UseItem_SelectMon's fan-out --
|
|
-- StatusHealingEffect and its berries, ReviveEffect on a fainted BENCHED mon,
|
|
-- RestorePPEffect's move pick for the ETHER pair and its no-pick ELIXER pair,
|
|
-- BitterBerryEffect's substatus-only cure -- plus the battle-only arm the
|
|
-- field routine has no way to reach (HealStatus clearing SUBSTATUS_CONFUSED on
|
|
-- whoever is out).
|
|
--
|
|
-- The tail is the fishing path: FishFunction writes BATTLETYPE_FISH beside the
|
|
-- hooked species, which is the single condition LureBallMultiplier reads, so
|
|
-- the rod's own encounter is walked end to end and a LURE BALL thrown in it.
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 battle items")
|
|
local check, eq = S.check, S.eq
|
|
|
|
love = require("tests.love_stub")
|
|
-- The rod leg puts a real TextBox up over a stub font, which has no glyphs to
|
|
-- measure; the warning per character is noise, not a finding.
|
|
require("src.core.Logger").warn = function() end
|
|
|
|
local Battle = require("src.battle.gen2.Battle")
|
|
local BattleState = require("src.ui.gen2.BattleState")
|
|
local Input = require("src.core.Input")
|
|
local Mon = require("src.battle.gen2.Mon")
|
|
local MoveDeleter = require("src.ui.gen2.MoveDeleter")
|
|
local PackMenu = require("src.ui.gen2.PackMenu")
|
|
local PartyMenu = require("src.ui.gen2.PartyMenu")
|
|
local World = require("src.world.gen2.World")
|
|
|
|
-- ---------------------------------------------------------------- fixtures
|
|
|
|
local TYPES = {
|
|
NORMAL = { id = "NORMAL", index = 0, category = "physical" },
|
|
WATER = { id = "WATER", index = 21, category = "special" },
|
|
}
|
|
|
|
local MOVES = {
|
|
TACKLE = { id = "TACKLE", name = "TACKLE", power = 35, type = "NORMAL",
|
|
accuracy = 95, pp = 35, effect = "EFFECT_NORMAL_HIT" },
|
|
SCRATCH = { id = "SCRATCH", name = "SCRATCH", power = 40, type = "NORMAL",
|
|
accuracy = 100, pp = 35, effect = "EFFECT_NORMAL_HIT" },
|
|
}
|
|
|
|
local GROWTH = {
|
|
GROWTH_MEDIUM_SLOW = { numerator = 6, denominator = 5, squared = -15,
|
|
linear = 100, constant = 140 },
|
|
}
|
|
|
|
local POKEMON = {
|
|
growthRates = GROWTH,
|
|
CYNDAQUIL = {
|
|
id = "CYNDAQUIL", index = 155, name = "CYNDAQUIL",
|
|
baseStats = { hp = 39, attack = 52, defense = 43, speed = 65,
|
|
specialAttack = 60, specialDefense = 50 },
|
|
types = { "NORMAL", "NORMAL" }, catchRate = 45, baseExp = 65,
|
|
growthRate = "GROWTH_MEDIUM_SLOW", genderRatio = 31,
|
|
levelMoves = { { level = 1, move = "TACKLE" } }, evolutions = {},
|
|
},
|
|
TOTODILE = {
|
|
id = "TOTODILE", index = 158, name = "TOTODILE",
|
|
baseStats = { hp = 50, attack = 65, defense = 64, speed = 43,
|
|
specialAttack = 44, specialDefense = 48 },
|
|
types = { "WATER", "WATER" }, catchRate = 45, baseExp = 66,
|
|
growthRate = "GROWTH_MEDIUM_SLOW", genderRatio = 31,
|
|
levelMoves = { { level = 1, move = "TACKLE" } }, evolutions = {},
|
|
},
|
|
PIDGEY = {
|
|
id = "PIDGEY", index = 16, name = "PIDGEY",
|
|
baseStats = { hp = 40, attack = 45, defense = 40, speed = 56,
|
|
specialAttack = 35, specialDefense = 35 },
|
|
types = { "NORMAL", "NORMAL" }, catchRate = 255, baseExp = 55,
|
|
growthRate = "GROWTH_MEDIUM_SLOW", genderRatio = 127,
|
|
levelMoves = { { level = 1, move = "TACKLE" } }, evolutions = {},
|
|
},
|
|
MAGIKARP = {
|
|
id = "MAGIKARP", index = 129, name = "MAGIKARP",
|
|
baseStats = { hp = 20, attack = 10, defense = 55, speed = 80,
|
|
specialAttack = 15, specialDefense = 20 },
|
|
types = { "WATER", "WATER" }, catchRate = 45, baseExp = 20,
|
|
growthRate = "GROWTH_MEDIUM_SLOW", genderRatio = 127,
|
|
levelMoves = { { level = 1, move = "TACKLE" } }, evolutions = {},
|
|
},
|
|
}
|
|
|
|
local function item(id, pocket, name)
|
|
return { id = id, pocket = pocket or "ITEM", name = name or id }
|
|
end
|
|
|
|
local DATA = {
|
|
pokemon = POKEMON,
|
|
moves = MOVES,
|
|
type_chart = { types = TYPES, matchups = {} },
|
|
items = {
|
|
POTION = item("POTION"),
|
|
ANTIDOTE = item("ANTIDOTE"),
|
|
BURN_HEAL = item("BURN_HEAL"),
|
|
FULL_HEAL = item("FULL_HEAL"),
|
|
PSNCUREBERRY = item("PSNCUREBERRY"),
|
|
BITTER_BERRY = item("BITTER_BERRY"),
|
|
REVIVE = item("REVIVE"),
|
|
MAX_REVIVE = item("MAX_REVIVE"),
|
|
ETHER = item("ETHER"),
|
|
ELIXER = item("ELIXER"),
|
|
MAX_ELIXER = item("MAX_ELIXER"),
|
|
POKE_BALL = item("POKE_BALL", "BALL", "POKe BALL"),
|
|
LURE_BALL = item("LURE_BALL", "BALL", "LURE BALL"),
|
|
-- The two nibbles of data/items/attributes.asm that disagree, spelled the
|
|
-- way the extractor spells them.
|
|
RARE_CANDY = { id = "RARE_CANDY", pocket = "ITEM", name = "RARE CANDY",
|
|
fieldMenu = "ITEMMENU_PARTY", battleMenu = "ITEMMENU_NOUSE" },
|
|
OLD_ROD = { id = "OLD_ROD", pocket = "KEY", name = "OLD ROD",
|
|
fieldMenu = "ITEMMENU_CURRENT", battleMenu = "ITEMMENU_NOUSE" },
|
|
-- a mod's own battle-pack item: its action lives only in
|
|
-- gen2ItemEffects below, which ItemEffects.RECORDS (the module's
|
|
-- built-in table) has never heard of (#8)
|
|
MOD_ITEM = item("MOD_ITEM"),
|
|
},
|
|
gen2ItemEffects = {
|
|
-- a status cure rather than an HP heal: HP is exposed to the wild
|
|
-- mon's own reply once the item spends the turn, which would make a
|
|
-- direct before/after HP check depend on incidental battle math this
|
|
-- fix has nothing to do with. Status is not.
|
|
MOD_ITEM = {
|
|
action = "status", field = true, needsTarget = true,
|
|
use = function(ctx)
|
|
local mon = ctx.mon
|
|
if mon.status ~= "poison" then
|
|
return { used = false, text = "It won't have\nany effect." }
|
|
end
|
|
mon.status = nil
|
|
return { used = true, text = "MOD ITEM used!" }
|
|
end,
|
|
},
|
|
},
|
|
}
|
|
|
|
local perfect = { attack = 15, defense = 15, speed = 15, special = 15 }
|
|
perfect.hp = Mon.hpDV(perfect)
|
|
|
|
-- The smallest roll that neither crits nor misses, so a spent turn resolves
|
|
-- the same way every run.
|
|
local function detRandom(n)
|
|
if (n or 1) <= 1 then return 0 end
|
|
return 1
|
|
end
|
|
|
|
local function newScreen(opts)
|
|
opts = opts or {}
|
|
Input:init()
|
|
local pushed = {}
|
|
local player = opts.player or Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
|
|
if not player.moves or #player.moves == 0 then
|
|
player.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
end
|
|
local party = opts.party or { player }
|
|
local wild = opts.wild or Mon.new(DATA, "PIDGEY", 5, { dvs = perfect })
|
|
wild.moves = wild.moves or { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
local save = { party = party, inventory = opts.inventory or {} }
|
|
local game = {
|
|
data = DATA,
|
|
save = save,
|
|
input = Input,
|
|
options = {},
|
|
stack = {
|
|
push = function(_, screen) pushed[#pushed + 1] = screen end,
|
|
pop = function() table.remove(pushed) end,
|
|
top = function() return pushed[#pushed] end,
|
|
},
|
|
}
|
|
local battle = Battle.new({ data = DATA, party = party, wild = wild,
|
|
save = save, random = opts.random or detRandom })
|
|
local screen = BattleState.new(game, { battle = battle, save = save })
|
|
return screen, battle, player, save, pushed
|
|
end
|
|
|
|
-- Battle lines end in `prompt`, and PromptButton waits on A or B with no
|
|
-- frame countdown (home/joypad.asm:383-412), so a drain has to press like a
|
|
-- player does rather than wait for a timer that never runs out.
|
|
local function runToMenu(screen, cap)
|
|
for _ = 1, (cap or 3000) do
|
|
local waiting = (screen.messageTimer or 0) > 0
|
|
if waiting then Input:overlayPressed("a") end
|
|
Input:step()
|
|
screen:update(1 / 60)
|
|
if waiting then Input:overlayReleased("a") end
|
|
if screen.phase == "menu" then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- ---- StatusHealingEffect on a BENCHED mon ---------------------------------
|
|
do
|
|
local lead = Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
|
|
lead.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
local bench = Mon.new(DATA, "TOTODILE", 8, { dvs = perfect })
|
|
bench.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
bench.status = "poison"
|
|
bench.toxicCounter = 3
|
|
local screen, battle, _, save, pushed = newScreen({
|
|
player = lead, party = { lead, bench },
|
|
inventory = { ANTIDOTE = 2, BURN_HEAL = 1, PSNCUREBERRY = 1 },
|
|
})
|
|
check(runToMenu(screen), "the intro drains to the battle menu")
|
|
|
|
local turn0 = battle.turn
|
|
screen:useItem("ANTIDOTE")
|
|
eq(screen.phase, "submenu", "a status cure opens UseItem_SelectMon first")
|
|
local picker = pushed[#pushed]
|
|
eq(getmetatable(picker), PartyMenu, "and the pick is the party screen")
|
|
eq(picker.prompt, PartyMenu.PROMPTS.useItem, "under the cart's own question")
|
|
picker.onChoose(2, bench)
|
|
eq(bench.status, nil, "the ANTIDOTE cured the BENCHED mon")
|
|
eq(bench.toxicCounter, nil, "and the toxic ramp went with the status byte")
|
|
eq(save.inventory.ANTIDOTE, 1, "one ANTIDOTE left the bag")
|
|
eq(battle.turn, turn0 + 1, "a cure that lands spends the turn")
|
|
check(runToMenu(screen), "the enemy's answer drains out")
|
|
|
|
-- UseStatusHealer's mask test: a clean mon, or the wrong class of item,
|
|
-- refuses without spending anything.
|
|
local turn1 = battle.turn
|
|
screen:useItem("ANTIDOTE")
|
|
pushed[#pushed].onChoose(2, bench)
|
|
eq(screen.message, "It won't have any\neffect.",
|
|
"a clean target answers the cart's refusal")
|
|
eq(save.inventory.ANTIDOTE, 1, "with the ANTIDOTE still in the bag")
|
|
eq(battle.turn, turn1, "and the turn not spent")
|
|
check(runToMenu(screen), "back to the menu")
|
|
|
|
bench.status = "poison"
|
|
screen:useItem("BURN_HEAL")
|
|
pushed[#pushed].onChoose(2, bench)
|
|
eq(screen.message, "It won't have any\neffect.",
|
|
"a BURN HEAL does not answer poison")
|
|
eq(bench.status, "poison", "the status is untouched")
|
|
eq(save.inventory.BURN_HEAL, 1, "and nothing spent")
|
|
check(runToMenu(screen), "back to the menu")
|
|
|
|
-- The berries carry the same StatusHealingActions rows as the shop cures.
|
|
screen:useItem("PSNCUREBERRY")
|
|
pushed[#pushed].onChoose(2, bench)
|
|
eq(bench.status, nil, "PSNCUREBERRY cures poison like an ANTIDOTE")
|
|
eq(save.inventory.PSNCUREBERRY, nil, "and the berry is eaten")
|
|
check(runToMenu(screen), "back to the menu")
|
|
|
|
-- Backing out of the picker returns to the pack with nothing spent.
|
|
screen:useItem("ANTIDOTE")
|
|
pushed[#pushed].onCancel()
|
|
eq(getmetatable(pushed[#pushed]), PackMenu,
|
|
"cancelling the picker reopens the PACK (.SelectMon's carry path)")
|
|
eq(save.inventory.ANTIDOTE, 1, "with the ANTIDOTE untouched")
|
|
end
|
|
|
|
-- ---- a mod's own battle-pack item (#8) -------------------------------------
|
|
-- BattleState:useItem asked ItemEffects.partyAction for the item's family
|
|
-- with no `data` argument, the same omission Game2:usePartyItem had for the
|
|
-- field pack, so a mod item's action -- present only in the merged
|
|
-- gen2ItemEffects table -- resolved to nil and the pack fell straight to
|
|
-- "That isn't going to help here." instead of opening the party list.
|
|
do
|
|
local sick = Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
|
|
sick.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
sick.status = "poison"
|
|
local screen, _, _, save, pushed = newScreen({
|
|
player = sick, party = { sick }, inventory = { MOD_ITEM = 1 },
|
|
})
|
|
check(runToMenu(screen), "reached the menu")
|
|
|
|
screen:useItem("MOD_ITEM")
|
|
eq(screen.phase, "submenu",
|
|
"a mod's own gen2ItemEffects record opens UseItem_SelectMon")
|
|
local picker = pushed[#pushed]
|
|
eq(getmetatable(picker), PartyMenu, "and the pick is the party screen")
|
|
if picker and picker.onChoose then
|
|
picker.onChoose(1, sick)
|
|
eq(sick.status, nil, "the mod item's own use() ran through the real screens")
|
|
eq(save.inventory.MOD_ITEM, nil, "and the mod item was spent")
|
|
end
|
|
end
|
|
|
|
-- ---- IsItemUsedOnConfusedMon: the battle-only arm --------------------------
|
|
do
|
|
local screen, battle, player, save, pushed = newScreen({
|
|
inventory = { FULL_HEAL = 2, ANTIDOTE = 1 },
|
|
})
|
|
check(runToMenu(screen), "reached the menu")
|
|
|
|
-- A mon whose only complaint is the confusion volatile: the field routine
|
|
-- refuses (the status byte is clean), the battle one spends the item.
|
|
battle:volatile(player).confuseCount = 3
|
|
local turn0 = battle.turn
|
|
screen:useItem("FULL_HEAL")
|
|
pushed[#pushed].onChoose(1, player)
|
|
eq(battle:volatile(player).confuseCount, nil,
|
|
"a $ff-mask item clears SUBSTATUS_CONFUSED on whoever is out")
|
|
eq(save.inventory.FULL_HEAL, 1, "and it costs the FULL HEAL")
|
|
eq(battle.turn, turn0 + 1, "and the turn")
|
|
check(runToMenu(screen), "the turn drains")
|
|
|
|
-- The mask is what gates it: an ANTIDOTE is 1 << PSN, so it answers poison
|
|
-- and leaves the confusion standing.
|
|
player.status = "poison"
|
|
battle:volatile(player).confuseCount = 3
|
|
screen:useItem("ANTIDOTE")
|
|
pushed[#pushed].onChoose(1, player)
|
|
eq(player.status, nil, "the ANTIDOTE still cures the poison")
|
|
eq(battle:volatile(player).confuseCount, 3,
|
|
"but a masked cure leaves the confusion alone")
|
|
check(runToMenu(screen), "back to the menu")
|
|
|
|
-- BitterBerryEffect never opens the party list: it acts on the mon that is
|
|
-- out, and one that is not confused refuses.
|
|
save.inventory.BITTER_BERRY = 2
|
|
local depth = #pushed
|
|
screen:useItem("BITTER_BERRY")
|
|
eq(#pushed, depth, "a BITTER BERRY opens no picker")
|
|
eq(battle:volatile(player).confuseCount, nil, "it cured the confusion")
|
|
eq(save.inventory.BITTER_BERRY, 1, "and was eaten doing it")
|
|
check(runToMenu(screen), "the turn drains")
|
|
|
|
screen:useItem("BITTER_BERRY")
|
|
eq(screen.message, "It won't have any\neffect.",
|
|
"a clear-headed mon refuses the berry")
|
|
eq(save.inventory.BITTER_BERRY, 1, "with the berry still in the bag")
|
|
end
|
|
|
|
-- ---- ReviveEffect on a fainted BENCHED mon --------------------------------
|
|
do
|
|
local lead = Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
|
|
lead.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
local bench = Mon.new(DATA, "TOTODILE", 8, { dvs = perfect })
|
|
bench.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
bench.hp = 0
|
|
bench.status = "faint"
|
|
local screen, battle, _, save, pushed = newScreen({
|
|
player = lead, party = { lead, bench },
|
|
inventory = { REVIVE = 2, MAX_REVIVE = 1 },
|
|
})
|
|
check(runToMenu(screen), "reached the menu")
|
|
|
|
-- A healthy target is RevivePokemon's `ret nz`: nothing spent.
|
|
screen:useItem("REVIVE")
|
|
pushed[#pushed].onChoose(1, lead)
|
|
eq(screen.message, "It won't have any\neffect.",
|
|
"a live mon refuses the REVIVE")
|
|
eq(save.inventory.REVIVE, 2, "with both REVIVEs still in the bag")
|
|
check(runToMenu(screen), "back to the menu")
|
|
|
|
local maxHp = bench.maxHp or bench.stats.hp
|
|
local turn0 = battle.turn
|
|
screen:useItem("REVIVE")
|
|
pushed[#pushed].onChoose(2, bench)
|
|
eq(bench.hp, math.max(1, math.floor(maxHp / 2)),
|
|
"REVIVE stands the BENCHED mon up at half max HP (ReviveHalfHP)")
|
|
eq(bench.status, nil, "with its status byte cleared")
|
|
eq(save.inventory.REVIVE, 1, "one REVIVE spent")
|
|
eq(battle.turn, turn0 + 1, "and the turn spent with it")
|
|
check(runToMenu(screen), "the turn drains")
|
|
|
|
bench.hp = 0
|
|
screen:useItem("MAX_REVIVE")
|
|
pushed[#pushed].onChoose(2, bench)
|
|
eq(bench.hp, maxHp, "MAX REVIVE takes ReviveFullHP instead")
|
|
eq(save.inventory.MAX_REVIVE, nil, "and is spent")
|
|
check(runToMenu(screen), "the turn drains")
|
|
|
|
-- The revived mon is a mon again as far as the engine is concerned: it can
|
|
-- be switched to, which is the whole point of reviving mid-battle.
|
|
check(battle:switch(2), "the revived mon can be sent out")
|
|
eq(battle.player, bench, "and it is the one in play")
|
|
end
|
|
|
|
-- ---- RestorePPEffect: the ETHER pick and the ELIXER sweep ------------------
|
|
do
|
|
local player = Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
|
|
player.moves = {
|
|
{ id = "TACKLE", pp = 35, maxPp = 35 },
|
|
{ id = "SCRATCH", pp = 4, maxPp = 35 },
|
|
}
|
|
local screen, battle, _, save, pushed = newScreen({
|
|
player = player,
|
|
inventory = { ETHER = 2, ELIXER = 1, MAX_ELIXER = 1 },
|
|
})
|
|
check(runToMenu(screen), "reached the menu")
|
|
|
|
screen:useItem("ETHER")
|
|
eq(getmetatable(pushed[#pushed]), PartyMenu, "the ETHER picks a mon first")
|
|
pushed[#pushed].onChoose(1, player)
|
|
local moveList = pushed[#pushed]
|
|
eq(getmetatable(moveList), MoveDeleter,
|
|
"then the move list (MoveSelectionScreen shares SetUpMoveList with it)")
|
|
eq(#pushed, 2, "stacked over the party list, which is still standing")
|
|
|
|
-- Backing out of the move list drops only that screen, which is
|
|
-- RestorePPEffect's own `jr nz, .loop` back to the pick.
|
|
moveList.onCancel()
|
|
eq(#pushed, 1, "cancelling the move list leaves the party list up")
|
|
eq(getmetatable(pushed[#pushed]), PartyMenu, "on the party list")
|
|
eq(save.inventory.ETHER, 2, "with nothing spent")
|
|
|
|
pushed[#pushed].onChoose(1, player)
|
|
local turn0 = battle.turn
|
|
pushed[#pushed].onChoose(2)
|
|
eq(player.moves[2].pp, 14, "the ETHER put 10 PP back into the chosen slot")
|
|
eq(player.moves[1].pp, 35, "and left the other slot alone")
|
|
eq(save.inventory.ETHER, 1, "one ETHER spent")
|
|
eq(battle.turn, turn0 + 1, "and the turn with it")
|
|
check(runToMenu(screen), "the turn drains")
|
|
|
|
-- A slot already at max is `cp b / jr nc, .dont_restore`: nothing spent.
|
|
screen:useItem("ETHER")
|
|
pushed[#pushed].onChoose(1, player)
|
|
pushed[#pushed].onChoose(1)
|
|
eq(screen.message, "It won't have any\neffect.",
|
|
"a full slot answers the refusal")
|
|
eq(save.inventory.ETHER, 1, "with the ETHER still in the bag")
|
|
check(runToMenu(screen), "back to the menu")
|
|
|
|
-- Elixer_RestorePPofAllMoves needs no pick at all.
|
|
player.moves[1].pp = 30
|
|
player.moves[2].pp = 14
|
|
local depth = #pushed
|
|
screen:useItem("ELIXER")
|
|
pushed[#pushed].onChoose(1, player)
|
|
eq(#pushed, depth, "the ELIXER opened no move list")
|
|
eq(player.moves[1].pp, 35, "it filled the first slot (capped at max)")
|
|
eq(player.moves[2].pp, 24, "and walked on to the second")
|
|
eq(save.inventory.ELIXER, nil, "spending the ELIXER")
|
|
check(runToMenu(screen), "the turn drains")
|
|
|
|
screen:useItem("MAX_ELIXER")
|
|
pushed[#pushed].onChoose(1, player)
|
|
eq(player.moves[2].pp, 35, "MAX ELIXER takes the .restore_all arm")
|
|
eq(save.inventory.MAX_ELIXER, nil, "and is spent")
|
|
end
|
|
|
|
-- ---- an EGG refuses every family before anything runs ---------------------
|
|
do
|
|
local screen, _, _, save, pushed = newScreen({
|
|
inventory = { ANTIDOTE = 1, REVIVE = 1, ETHER = 1 },
|
|
})
|
|
check(runToMenu(screen), "reached the menu")
|
|
for _, id in ipairs({ "ANTIDOTE", "REVIVE", "ETHER" }) do
|
|
screen:useItem(id)
|
|
pushed[#pushed].onChoose(3, { isEgg = true, hp = 0, maxHp = 12,
|
|
moves = { { id = "TACKLE", pp = 0, maxPp = 35 } } })
|
|
eq(screen.message, "That can't be used\non an EGG.",
|
|
id .. " answers CantUseOnEggMessage")
|
|
eq(save.inventory[id], 1, "and is not spent")
|
|
check(runToMenu(screen), "back to the menu")
|
|
end
|
|
end
|
|
|
|
-- ---- ITEMMENU_NOUSE in a battle is .Oak -----------------------------------
|
|
do
|
|
local screen, battle, player, save, pushed = newScreen({
|
|
inventory = { RARE_CANDY = 1, OLD_ROD = 1 },
|
|
})
|
|
check(runToMenu(screen), "reached the menu")
|
|
local level, turn0, depth = player.level, battle.turn, #pushed
|
|
screen:useItem("RARE_CANDY")
|
|
eq(#pushed, depth, "a RARE CANDY opens no party list in a battle")
|
|
eq(player.level, level, "and levels nothing")
|
|
eq(save.inventory.RARE_CANDY, 1, "with the candy still in the bag")
|
|
eq(battle.turn, turn0, "and the turn not spent")
|
|
|
|
screen:useItem("OLD_ROD")
|
|
eq(save.inventory.OLD_ROD, 1, "a rod is ITEMMENU_NOUSE in battle too")
|
|
end
|
|
|
|
-- ---- the rod's own battle carries BATTLETYPE_FISH --------------------------
|
|
do
|
|
local pushed = {}
|
|
local save = { player = {}, party = {}, inventory = { LURE_BALL = 1,
|
|
POKE_BALL = 1 } }
|
|
local player = Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
|
|
player.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
save.party[1] = player
|
|
-- The rod's line rides a real TextBox, so the world needs an input its
|
|
-- update can read; `press` is the A that turns the page.
|
|
local fakeInput = { pressed = {} }
|
|
function fakeInput:press(button) self.pressed[button] = true end
|
|
function fakeInput:wasPressed(button)
|
|
if self.pressed[button] then
|
|
self.pressed[button] = nil
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
function fakeInput:isDown() return false end
|
|
local game = {
|
|
data = DATA, save = save, input = fakeInput, options = {},
|
|
stack = {
|
|
push = function(_, screen) pushed[#pushed + 1] = screen end,
|
|
pop = function() table.remove(pushed) end,
|
|
top = function() return pushed[#pushed] end,
|
|
},
|
|
}
|
|
local world = World.new(game)
|
|
-- Script_GotABite's tail, with the state the rod itself left behind: the
|
|
-- hooked mon, the bite bob already served, the rod being put away. No map
|
|
-- is mounted, so DoBattleTransition has nothing to wipe and the battle comes
|
|
-- straight in, which is the same arm a headless run takes.
|
|
local hooked = Mon.new(DATA, "MAGIKARP", 10, { dvs = perfect })
|
|
hooked.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
world.fishing = { phase = "bite", timer = 0, outcome = "battle",
|
|
wild = hooked }
|
|
world:updateFishing()
|
|
-- RodBiteText is up; A pages it, and its dismissal is what runs startbattle.
|
|
local screen
|
|
for _ = 1, 600 do
|
|
local top = pushed[#pushed]
|
|
if top and getmetatable(top) == BattleState then screen = top break end
|
|
fakeInput:press("a")
|
|
if top and top.update then top:update(1 / 60) end
|
|
end
|
|
eq(world.fishing, nil, "PutTheRodAway clears the rod state before the battle")
|
|
check(screen ~= nil, "the rod's encounter pushed a battle")
|
|
eq(screen.battle.battleType, "fish",
|
|
"carrying BATTLETYPE_FISH out of FishFunction's .goodtofish")
|
|
|
|
-- LureBallMultiplier's x3, through the real throw. A full-HP MAGIKARP at
|
|
-- catch rate 45 computes to 15; the LURE BALL's tripled rate is 45, so a
|
|
-- fixed roll of 30 catches with one ball and not with the other.
|
|
local function throw(ball, type_)
|
|
Input:init()
|
|
local wild = Mon.new(DATA, "MAGIKARP", 10, { dvs = perfect })
|
|
wild.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
local lead = Mon.new(DATA, "CYNDAQUIL", 10, { dvs = perfect })
|
|
lead.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
|
|
local bagSave = { party = { lead }, inventory = { [ball] = 1 } }
|
|
local shot = {
|
|
data = DATA, save = bagSave, input = Input, options = {},
|
|
stack = { push = function() end, pop = function() end,
|
|
top = function() return nil end },
|
|
}
|
|
local battle = Battle.new({ data = DATA, party = { lead }, wild = wild,
|
|
save = bagSave, battleType = type_,
|
|
random = function(n) if n == 256 then return 30 end return 0 end })
|
|
local state = BattleState.new(shot, { battle = battle, save = bagSave })
|
|
state:useItem(ball)
|
|
return battle.outcome
|
|
end
|
|
|
|
eq(throw("LURE_BALL", screen.battle.battleType), "caught",
|
|
"a LURE BALL thrown in the rod's battle catches on that roll")
|
|
eq(throw("LURE_BALL", nil), nil,
|
|
"the same ball and the same roll off a rod does not")
|
|
eq(throw("POKE_BALL", screen.battle.battleType), nil,
|
|
"and neither does a plain ball in the rod's battle")
|
|
end
|
|
|
|
S.finish()
|