Files
gen1recomp/tests/emerald_battle_rules_test.lua
T

260 lines
12 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
local GameVersion = require("src.core.GameVersion")
local Profile = require("src.core.game3.profile")
Profile.reset()
GameVersion.set("emerald")
local Dataset = require("src.core.game3.dataset")
local cache = Dataset.cache()
local function loadRel(rel)
local src = cache and cache.read and cache:read("data/generated/gba/" .. rel)
if type(src) ~= "string" then return nil end
local chunk = load(src, "@" .. rel, "t", {})
return chunk and chunk() or nil
end
local manifest = loadRel("pokemon/battle/manifest.lua")
if not (manifest and manifest.layout == "rse") then
print("emerald_battle_rules_test: skipped (no Emerald cache; set POKEPORT_IDENTITY)")
os.exit(0)
end
local Versions = require("src.import.gba.versions")
Versions.select("emerald")
local C = require("src.core.game3.constants").of("emerald")
local BattleProfile = require("src.core.game3.battle.profile")
local bp = BattleProfile.get({ version = "emerald" })
local BattleEngine = require("src.core.game3.battle.engine")
local frontierRun = { wild = false, player = {}, kinds = { frontier = true } }
local hillRun = { wild = false, player = {}, kinds = { trainerHill = true } }
local ordinaryTrainerRun = { wild = false, player = {}, kinds = {} }
eq(BattleEngine.canRun(frontierRun, {}), true, "Frontier trainer can select RUN to forfeit")
eq(BattleEngine.canRun(hillRun, {}), true, "Trainer Hill trainer can select RUN to forfeit")
local ordinaryRun, ordinaryWhy = BattleEngine.canRun(ordinaryTrainerRun, {})
eq(ordinaryRun, false, "ordinary trainer still cannot run")
check(ordinaryWhy ~= nil, "ordinary trainer refusal retains its message")
local Prize = require("src.core.game3.battle.prize")
local data = Prize.rseData()
eq(#data.pickupItems, 18, "sPickupItems has 18 rows (battle_script_commands.c:784)")
eq(#data.rarePickupItems, 11, "sRarePickupItems has 11 rows")
eq(#data.pickupProbabilities, 9, "sPickupProbabilities has 9 rows")
eq(Prize.pickupBanded(1, 0), C.items.byName.ITEM_POTION, "lv1 rand 0 -> Potion")
eq(Prize.pickupBanded(1, 30), C.items.byName.ITEM_ANTIDOTE, "lv1 rand 30 -> Antidote")
eq(Prize.pickupBanded(1, 99), C.items.byName.ITEM_HYPER_POTION, "lv1 rand 99 -> Hyper Potion")
eq(Prize.pickupBanded(1, 98), C.items.byName.ITEM_NUGGET, "lv1 rand 98 -> Nugget")
eq(Prize.pickupBanded(100, 97), C.items.byName.ITEM_MAX_ELIXIR, "lv100 rand 97 -> Max Elixir")
local pikeParty = { { species = C.species.byName.SPECIES_MEOWTH, level = 10, abilityId = Prize.ABILITY_PICKUP } }
local pickupRolls = 0
local suppressed = Prize.pickup(pikeParty, function() pickupRolls = pickupRolls + 1; return 0 end,
{ pickup = "level_bands", noPickup = true })
eq(#suppressed, 0, "Pike rule suppresses Pickup result")
eq(pickupRolls, 0, "Pike suppression consumes no random values")
eq(pikeParty[1].item, nil, "Pike suppression does not alter held item")
local Pyramid = require("src.core.game3.rse.frontier.pyramid")
local Rng = require("src.core.game3.rng")
local savedRandom = Rng.Random
Rng.Random = function() return 0 end
local pyramidSession = { version = "emerald", map = Pyramid.FLOOR_MAP, frontier = {} }
local pyramidItem = Pyramid.pickupItemId(pyramidSession)
local pyramidParty = { { species = C.species.byName.SPECIES_MEOWTH, level = 50,
abilityId = Prize.ABILITY_PICKUP } }
local pyramidPicked = Prize.pickup(pyramidParty, function() return 0 end,
{ pickup = "level_bands", pyramidSession = pyramidSession })
Rng.Random = savedRandom
eq(#pyramidPicked, 1, "Pyramid Pickup grants one floor-table item")
eq(pyramidParty[1].item, pyramidItem, "Pyramid Pickup uses GetBattlePyramidPickupItemId's table")
local Pike = require("src.core.game3.rse.frontier.pike")
local Game3 = require("src.core.Game3")
local Bag = require("src.core.game3.bag")
local ItemUse = require("src.core.game3.item_use")
local registeredBag = Bag.new()
local registeredItem = C.items.byName.ITEM_MACH_BIKE
Bag.add(registeredBag, registeredItem, 1)
local registeredSession = { version = "emerald", map = Pike.WILD_ROOM, bag = registeredBag,
registeredItem = registeredItem }
local oldUseField, registeredUses = ItemUse.useField, 0
ItemUse.useField = function() registeredUses = registeredUses + 1; return true, "bike" end
Game3._handleRegisteredItem({ session = registeredSession, input = {
wasPressed = function(_, key) return key == "select" end,
} })
ItemUse.useField = oldUseField
eq(registeredUses, 0, "SELECT registered-item use is blocked in the Battle Pike")
eq(registeredSession.registeredItem, registeredItem, "blocked Pike use keeps its registration")
registeredSession.map = Pyramid.FLOOR_MAP
Game3._handleRegisteredItem({ session = registeredSession, input = {
wasPressed = function(_, key) return key == "select" end,
} })
eq(registeredUses, 0, "SELECT registered-item use is blocked in the Battle Pyramid")
local PartyMenu = require("src.ui.game3.party_menu")
local function pressed(key)
return { wasPressed = function(_, button) return button == key end }
end
registeredSession.map = Pike.WILD_ROOM
PartyMenu.show({ { species = C.species.byName.SPECIES_MEOWTH, level = 10, moves = {} } }, {
session = registeredSession,
})
PartyMenu.handleInput(pressed("a"))
local pikeActions = table.concat(PartyMenu.ACTIONS, ",")
eq(pikeActions:find("SWITCH", 1, true), nil, "Battle Pike party menu omits SWITCH")
eq(pikeActions:find("ITEM", 1, true), nil, "Battle Pike party menu omits ITEM and MAIL")
PartyMenu.close()
registeredSession.map = "EM_ROUTE101"
PartyMenu.show({ { species = C.species.byName.SPECIES_MEOWTH, level = 10, moves = {} } }, {
session = registeredSession,
})
PartyMenu.handleInput(pressed("a"))
local fieldActions = table.concat(PartyMenu.ACTIONS, ",")
eq(fieldActions:find("SWITCH", 1, true) ~= nil, true, "ordinary field party menu keeps SWITCH")
eq(fieldActions:find("ITEM", 1, true) ~= nil, true, "ordinary field party menu keeps ITEM")
PartyMenu.close()
registeredSession.map = Pyramid.FLOOR_MAP
local BattleUi = require("src.core.game3.battle.ui")
local PyramidBag = require("src.ui.game3.rse.pyramid_bag")
local realPyramidBagShow, pyramidBagOpts = PyramidBag.show, nil
PyramidBag.show = function(opts) pyramidBagOpts = opts; return true end
BattleUi._session, BattleUi._st, BattleUi._mode = registeredSession, { link = false }, "command"
BattleUi.openBattleBag()
PyramidBag.show = realPyramidBagShow
eq(pyramidBagOpts and pyramidBagOpts.location, "battle", "Pyramid battle BAG opens its dedicated battle inventory")
eq(pyramidBagOpts and pyramidBagOpts.session, registeredSession, "Pyramid battle BAG uses the active challenge state")
BattleUi._session, BattleUi._st, BattleUi._mode = nil, nil, "none"
local Bag = require("src.core.game3.bag")
local BagMenu = require("src.ui.game3.bag_menu")
local Battle = require("src.core.game3.battle")
local State = require("src.core.game3.battle.state")
local Screens = require("src.ui.game3.screens")
local testBag = Bag.new()
Bag.add(testBag, C.items.byName.ITEM_POTION, 2)
local testParty = {
{ species = C.species.byName.SPECIES_TREECKO, hp = 10, maxHp = 20, moves = {} },
{ species = C.species.byName.SPECIES_TORCHIC, hp = 5, maxHp = 20, moves = {} },
}
local testSession = { bag = testBag, party = testParty }
local bst = {
wild = true,
turn = 1,
player = State.makeBattler(testParty[1], "player", { partyIndex = 1 }),
playerParty = testParty,
}
Battle._active = true
Battle._phase = "command"
Battle._st = bst
Battle._auto = false
BattleUi._session = testSession
BattleUi._st = bst
BattleUi._mode = "command"
BattleUi.openBattleBag()
eq(BagMenu.isOpen(), true, "Emerald battle BagMenu is open")
BagMenu.settle()
Screens.handleInput("bag", BagMenu, pressed("a"), testSession)
eq(BagMenu.mode, "action", "BagMenu mode is action")
Screens.handleInput("bag", BagMenu, pressed("a"), testSession)
eq(PartyMenu.isOpen(), true, "PartyMenu is open for in-battle item use")
eq(PartyMenu.mode, "use", "PartyMenu is in use mode")
eq(PartyMenu.cursor, 1, "PartyMenu initially selects slot 1")
Battle.update(0, { input = pressed("down") })
eq(PartyMenu.cursor, 2, "PartyMenu cursor moves to slot 2 in Emerald battle (not stuck on active mon)")
Battle.update(0, { input = pressed("a") })
-- Press A to fast-forward HP animation and advance message
PartyMenu.handleInput(pressed("a"))
eq(PartyMenu.mode, "message", "PartyMenu displays recovery message")
PartyMenu.handleInput(pressed("a"))
eq(PartyMenu.isOpen(), false, "PartyMenu closes after message dismissal")
BagMenu.settle()
eq(BagMenu.isOpen(), false, "BagMenu closes after committing battle item")
eq(BattleUi._pendingCommand and BattleUi._pendingCommand.partySlot, 2, "Pending battle command targets slot 2")
Battle._active = false
BattleUi._session, BattleUi._st, BattleUi._mode = nil, nil, "none"
-- Also test cancelling back from PartyMenu to BagMenu
Battle._active = true
Battle._phase = "command"
Battle._st = bst
BattleUi._session = testSession
BattleUi._st = bst
BattleUi._mode = "command"
BattleUi.openBattleBag()
BagMenu.settle()
Screens.handleInput("bag", BagMenu, pressed("a"), testSession)
Screens.handleInput("bag", BagMenu, pressed("a"), testSession)
eq(PartyMenu.isOpen(), true, "PartyMenu reopened for item use")
Battle.update(0, { input = pressed("b") })
eq(PartyMenu.isOpen(), false, "PartyMenu closed on B press")
eq(BagMenu.isOpen(), true, "BagMenu remains open after PartyMenu cancel")
eq(BagMenu.mode, "list", "BagMenu returned to list mode")
Screens.handleInput("bag", BagMenu, pressed("b"), testSession)
BagMenu.settle()
eq(BagMenu.isOpen(), false, "BagMenu closed on B press")
Battle._active = false
BattleUi._session, BattleUi._st, BattleUi._mode = nil, nil, "none"
local Trainers = require("src.core.game3.scripting.trainers")
local calvin = C.trainers.byName.TRAINER_CALVIN_1
local row = Trainers.get(calvin)
check(row ~= nil, "Calvin row exists")
local pack = Trainers.pack()
local value = pack.money[row.class] or pack.moneyDefault
local last = row.party[#row.party].level
eq(Prize.calcRse(calvin, {}), 4 * last * value, "Calvin prize money from gTrainerMoneyTable")
print(string.format("[info] Calvin class %d value %d last lv %d -> %d", row.class, value, last, Prize.calcRse(calvin, {})))
local RomText = require("src.core.game3.rom_text")
for _, key in ipairs({ bp.firstBattle.cantRun, "STRINGID_PLAYERWHITEOUT", "STRINGID_PLAYERWHITEOUT2",
"STRINGID_PLAYERGOTMONEY", "STRINGID_WILDPKMNFLED", "STRINGID_PKMNGAINEDEXP", "gText_BattleWallyName" }) do
check(RomText.has(key), "Emerald text has " .. key)
end
check(RomText.plain(bp.firstBattle.cantRun):find("Don't leave me") ~= nil, "DONTLEAVEBIRCH reads Birch's line")
local Ai = require("src.core.game3.battle.ai")
local aiPack = Ai.loadPack({ force = true, required = true })
eq(#aiPack.table, 32, "gBattleAI_ScriptsTable has 32 scripts")
local fb = aiPack.scripts[aiPack.table[32]]
check(fb ~= nil and fb[1].op == "if_hp_equal", "AI_FirstBattle is script 31 (battle_ai_scripts.s:3233)")
local AiCmds = require("src.core.game3.battle.ai_cmds")
local seen = {}
for _, body in pairs(aiPack.scripts) do
for _, op in ipairs(body) do seen[op.op] = true end
end
for name in pairs(seen) do
check(AiCmds.CMD[name] ~= nil or name == "end" or name == "goto" or name == "call_if_always_hit"
or name:find("^nop") ~= nil, "AI op " .. name .. " has a handler")
end
local Anim = require("src.core.game3.battle.anim")
Anim._packLoaded, Anim._pack = false, nil
local reads = {}
local realRead = cache.read
cache.read = function(self, rel)
reads[#reads + 1] = rel
return realRead(self, rel)
end
local animPack = Anim.tableScript and select(2, pcall(function() return Anim.tableIndex("general", "POKEBLOCK_THROW") end))
cache.read = realRead
eq(animPack, 4, "general anim 4 is POKEBLOCK_THROW on Emerald")
local fromFr = false
for _, rel in ipairs(reads) do
if rel:find("^firered/") then fromFr = true end
end
check(not fromFr, "Emerald anim pack load never reads firered/ paths")
eq(Anim.tableIndex("general", "BAIT_THROW"), nil, "Emerald has no BAIT_THROW general anim")
local AnimTasks = require("src.core.game3.battle.anim_tasks")
AnimTasks.init()
check(AnimTasks.REGISTRY.IsBallBlockedByTrainer ~= nil, "IsBallBlockedByTrainer is registered")
GameVersion.set("firered")
T.finish()