mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
660 lines
26 KiB
Lua
660 lines
26 KiB
Lua
-- The Battle Tower: engine/events/battle_tower/battle_tower.asm,
|
|
-- engine/events/battle_tower/rules.asm and the room menu at
|
|
-- mobile/mobile_46.asm:137.
|
|
--
|
|
-- luajit tests/gen2_battle_tower_test.lua
|
|
--
|
|
-- ROM-free. The lobby rules, the challenge-state machine, the level/uber
|
|
-- room gates, the reward roll and the wInBattleTowerBattle badge guard, all
|
|
-- against fixtures.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 battle tower")
|
|
local check, eq = S.check, S.eq
|
|
|
|
love = require("tests.love_stub")
|
|
|
|
local Battle = require("src.battle.gen2.Battle")
|
|
local BattleTower = require("src.core.gen2.BattleTower")
|
|
local BattleTowerMenu = require("src.ui.gen2.BattleTowerMenu")
|
|
local Mon = require("src.battle.gen2.Mon")
|
|
local Save = require("src.core.gen2.Save")
|
|
local Screens = require("src.ui.Screens")
|
|
local Specials = require("src.script.gen2.Specials")
|
|
|
|
local A = BattleTower.ACTIONS
|
|
|
|
-- ---------------------------------------------------------------- fixtures
|
|
|
|
local ITEM_ORDER = {}
|
|
do
|
|
-- ../pokecrystal/constants/item_constants.asm: only the span the reward roll
|
|
-- walks has to be at its real index, so the filler carries the rest.
|
|
for i = 1, 250 do ITEM_ORDER[i] = "FILLER_" .. i end
|
|
ITEM_ORDER[18] = "POTION"
|
|
ITEM_ORDER[26] = "HP_UP"
|
|
ITEM_ORDER[27] = "PROTEIN"
|
|
ITEM_ORDER[28] = "IRON"
|
|
ITEM_ORDER[29] = "CARBOS"
|
|
ITEM_ORDER[30] = "LUCKY_PUNCH"
|
|
ITEM_ORDER[31] = "CALCIUM"
|
|
end
|
|
|
|
local DATA = {
|
|
gen2Constants = { itemOrder = ITEM_ORDER },
|
|
items = {
|
|
POTION = { index = 18, pocket = "ITEM" },
|
|
HP_UP = { index = 26, pocket = "ITEM" },
|
|
CALCIUM = { index = 31, pocket = "ITEM" },
|
|
},
|
|
}
|
|
|
|
local function mon(species, level, item, isEgg)
|
|
return { species = species, level = level, item = item, isEgg = isEgg }
|
|
end
|
|
|
|
local function crystalSave()
|
|
local save = { version = "crystal", player = { id = 7, badges = {} },
|
|
party = {}, inventory = {} }
|
|
return save
|
|
end
|
|
|
|
-- The hooks World:specialHooks hands a handler, stubbed.
|
|
local function fakeVm(opts)
|
|
opts = opts or {}
|
|
local vm = {
|
|
scriptVar = opts.scriptVar or 0,
|
|
specials = opts.hooks or {},
|
|
pages = {},
|
|
stringBuffer = "",
|
|
}
|
|
function vm:setStringBuffer(value) self.stringBuffer = value or "" end
|
|
function vm:showRaw(body)
|
|
body = tostring(body)
|
|
if self.stringBuffer ~= "" then
|
|
body = body:gsub("{STRBUF}", self.stringBuffer)
|
|
end
|
|
self.pages[#self.pages + 1] = body
|
|
end
|
|
return vm
|
|
end
|
|
|
|
local function towerVm(save, extra)
|
|
local hooks = {
|
|
save = function() return save end,
|
|
data = function() return DATA end,
|
|
party = function() return save.party end,
|
|
itemIndex = function(id)
|
|
for index, name in ipairs(ITEM_ORDER) do
|
|
if name == id then return index end
|
|
end
|
|
return 0
|
|
end,
|
|
}
|
|
for key, value in pairs(extra or {}) do hooks[key] = value end
|
|
return fakeVm({ hooks = hooks })
|
|
end
|
|
|
|
local function action(vm, id)
|
|
vm.scriptVar = id
|
|
Specials.ALL.BattleTowerAction(vm)
|
|
return vm.scriptVar
|
|
end
|
|
|
|
-- ============================================== the seam and the ownership
|
|
do
|
|
-- ../pokecrystal/engine/events/battle_tower/battle_tower.asm:181-259 and
|
|
-- :1534-1576 joined the list once the roster reached trainers.lua; their own
|
|
-- behaviour is tests/gen2_battle_tower_battle_test.lua.
|
|
for _, name in ipairs({ "BattleTowerAction", "CheckForBattleTowerRules",
|
|
"Menu_ChallengeExplanationCancel", "BattleTowerRoomMenu",
|
|
"BattleTowerBattle", "LoadOpponentTrainerAndPokemonWithOTSprite" }) do
|
|
eq(Specials.HANDLER_SOURCE[name], "specials/battle_tower.lua",
|
|
name .. " is owned by specials/battle_tower.lua")
|
|
check(type(Specials.ALL[name]) == "function",
|
|
name .. " resolves to a function")
|
|
eq(Specials.STUBS[name], nil, name .. " is no longer a stub")
|
|
end
|
|
|
|
for _, name in ipairs({ "BattleTowerMobileError" }) do
|
|
check(type(Specials.STUB_REASONS[name]) == "string",
|
|
name .. " is still a deliberate stub, with its reason")
|
|
end
|
|
|
|
local found = false
|
|
for _, id in ipairs(Screens.GEN2_IDS) do
|
|
if id == "Gen2BattleTowerMenu" then found = true end
|
|
end
|
|
check(found, "Gen2BattleTowerMenu joined Screens.GEN2_IDS")
|
|
end
|
|
|
|
-- ================================================= _CheckForBattleTowerRules
|
|
do
|
|
local legal = { mon("PIKACHU", 40, "BERRY"), mon("GEODUDE", 40, "GOLD_BERRY"),
|
|
mon("HORSEA", 40) }
|
|
local lines, failed = BattleTower.checkRules(legal)
|
|
eq(#lines, 0, "a legal three-mon party prints nothing")
|
|
eq(failed, false, "and _CheckForBattleTowerRules returns no carry")
|
|
|
|
-- Two itemless mons do not collide: `ld a, [hl] / and a / jr z, .next`
|
|
-- drops a zero value before the inner scan ever runs.
|
|
local bare = { mon("PIKACHU", 40), mon("GEODUDE", 40), mon("HORSEA", 40) }
|
|
eq(#(BattleTower.checkRules(bare)), 0,
|
|
"three mons holding nothing is legal")
|
|
|
|
lines = BattleTower.checkRules({ mon("PIKACHU", 40), mon("GEODUDE", 40) })
|
|
eq(table.concat(lines, "|"),
|
|
"_ExcuseMeYoureNotReadyText|_OnlyThreeMonMayBeEnteredText|"
|
|
.. "_BattleTowerReturnWhenReadyText",
|
|
"two mons: the header, the count line and the tail")
|
|
|
|
lines = BattleTower.checkRules({ mon("PIKACHU", 40), mon("PIKACHU", 40),
|
|
mon("HORSEA", 40) })
|
|
eq(lines[2], "_TheMonMustAllBeDifferentKindsText",
|
|
"a duplicate species is the second check")
|
|
|
|
lines = BattleTower.checkRules({ mon("PIKACHU", 40, "BERRY"),
|
|
mon("GEODUDE", 40, "BERRY"), mon("HORSEA", 40) })
|
|
eq(lines[2], "_TheMonMustNotHoldTheSameItemsText",
|
|
"a duplicate held item is the third")
|
|
|
|
lines = BattleTower.checkRules({ mon("PIKACHU", 40), mon("GEODUDE", 40),
|
|
mon("ODD_EGG", 5, nil, true) })
|
|
eq(lines[2], "_YouCantTakeAnEggText", "an egg is the fourth")
|
|
|
|
-- An egg is skipped by BOTH uniqueness walks (CheckPartyValueIsUnique's
|
|
-- `.isegg` guards each side), so it only trips its own check.
|
|
lines = BattleTower.checkRules({ mon("PIKACHU", 40, "BERRY"),
|
|
mon("PIKACHU", 40, "BERRY", true), mon("HORSEA", 40) })
|
|
eq(table.concat(lines, "|"),
|
|
"_ExcuseMeYoureNotReadyText|_YouCantTakeAnEggText|"
|
|
.. "_BattleTowerReturnWhenReadyText",
|
|
"an egg duplicating a species and an item still only fails the egg rule")
|
|
|
|
-- Every check runs; BattleTower_ExecuteJumptable does not stop at the first.
|
|
lines = BattleTower.checkRules({ mon("PIKACHU", 40, "BERRY"),
|
|
mon("PIKACHU", 40, "BERRY"), mon("HORSEA", 40, "BERRY"),
|
|
mon("ODD_EGG", 5, nil, true) })
|
|
eq(table.concat(lines, "|"),
|
|
"_ExcuseMeYoureNotReadyText|_OnlyThreeMonMayBeEnteredText|"
|
|
.. "_TheMonMustAllBeDifferentKindsText|"
|
|
.. "_TheMonMustNotHoldTheSameItemsText|_YouCantTakeAnEggText|"
|
|
.. "_BattleTowerReturnWhenReadyText",
|
|
"four failures print the header, all four lines and the tail, in order")
|
|
end
|
|
|
|
-- ---- the special itself
|
|
do
|
|
local save = crystalSave()
|
|
save.party = { mon("PIKACHU", 40), mon("GEODUDE", 40), mon("HORSEA", 40) }
|
|
local vm = towerVm(save)
|
|
Specials.ALL.CheckForBattleTowerRules(vm)
|
|
eq(vm.scriptVar, 0, "a legal party answers FALSE, the `ifnotequal FALSE` arm")
|
|
eq(#vm.pages, 0, "and prints nothing")
|
|
|
|
save.party = { mon("PIKACHU", 40) }
|
|
vm = towerVm(save)
|
|
Specials.ALL.CheckForBattleTowerRules(vm)
|
|
eq(vm.scriptVar, 1, "a broken party answers TRUE")
|
|
eq(#vm.pages, 3, "and prints header, refusal and tail")
|
|
eq(vm.stringBuffer, "3", "wStringBuffer2 holds the '3' the lines splice in")
|
|
end
|
|
|
|
-- ============================================== the rooms and their gates
|
|
do
|
|
local save = crystalSave()
|
|
eq(#BattleTower.levelGroupRows(save), 4,
|
|
"before the Hall of Fame only L10-L40 are offered")
|
|
save.hallOfFame = { count = 1, teams = {} }
|
|
eq(#BattleTower.levelGroupRows(save), 10,
|
|
"and all ten once STATUSFLAGS_HALL_OF_FAME_F is set")
|
|
|
|
-- maps/BattleTowerHallway.asm:40-47, the room the receptionist walks to.
|
|
eq(BattleTower.roomOf(1), 0, "L10 is the 10/20 room")
|
|
eq(BattleTower.roomOf(2), 0, "L20 shares it")
|
|
eq(BattleTower.roomOf(3), 1, "L30 is the 30/40 room")
|
|
eq(BattleTower.roomOf(4), 1, "L40 shares it")
|
|
eq(BattleTower.roomOf(9), 4, "L90 is the 90/100 room")
|
|
eq(BattleTower.roomOf(10), 4, "L100 shares it")
|
|
|
|
local party = { mon("PIKACHU", 30), mon("GEODUDE", 30), mon("HORSEA", 30) }
|
|
eq(BattleTower.levelCheck(party, 3), false, "L30 mons fit the L30 room")
|
|
eq(BattleTower.levelCheck(party, 2), true, "and top the L20 room")
|
|
party[1].level = 31
|
|
eq(BattleTower.levelCheck(party, 3), true, "one mon over is enough")
|
|
|
|
local ubers = { mon("LUGIA", 40), mon("GEODUDE", 40), mon("HORSEA", 40) }
|
|
eq(BattleTower.ubersCheck(ubers, 4), "LUGIA",
|
|
"an uber is refused below the L70 rooms")
|
|
eq(BattleTower.ubersCheck(ubers, 7), nil,
|
|
"and allowed from the L70 room up")
|
|
ubers[1] = mon("MEWTWO", 70)
|
|
eq(BattleTower.ubersCheck(ubers, 6), nil,
|
|
"an uber already at L70 is past the `cp 70 / jr c` gate")
|
|
eq(BattleTower.ubersCheck({ mon("DRAGONITE", 40) }, 4), nil,
|
|
"and DRAGONITE is not on the list")
|
|
end
|
|
|
|
-- ==================================================== the reward (:906-976)
|
|
do
|
|
-- The `maskbits` roll is over eight values folded into six, so HP_UP and
|
|
-- PROTEIN come up twice as often; LUCKY_PUNCH is rerolled.
|
|
local seen = {}
|
|
for roll = 1, 8 do
|
|
local n = 0
|
|
local item = BattleTower.rollReward(ITEM_ORDER, function(mask)
|
|
n = n + 1
|
|
-- Specials.random answers 1..mask, and the handler folds it with % mask.
|
|
return (n == 1) and roll or 1
|
|
end)
|
|
seen[roll % 8] = item
|
|
end
|
|
eq(seen[1], "PROTEIN", "roll 1 is PROTEIN")
|
|
eq(seen[2], "IRON", "roll 2 is IRON")
|
|
eq(seen[3], "CARBOS", "roll 3 is CARBOS")
|
|
eq(seen[5], "CALCIUM", "roll 5 is CALCIUM")
|
|
eq(seen[0], "HP_UP", "roll 0 is HP_UP")
|
|
eq(seen[6], "HP_UP", "roll 6 folds back onto HP_UP")
|
|
eq(seen[7], "PROTEIN", "roll 7 folds back onto PROTEIN")
|
|
check(seen[4] ~= "LUCKY_PUNCH", "roll 4 lands on LUCKY_PUNCH and rerolls")
|
|
|
|
eq(BattleTower.rewardFits(19, 20, nil), true,
|
|
"a pocket with a free slot takes the five")
|
|
eq(BattleTower.rewardFits(20, 20, nil), false,
|
|
"a full pocket holding none of the reward cannot")
|
|
eq(BattleTower.rewardFits(20, 20, 94), true,
|
|
"a full pocket already holding 94 can stack five more")
|
|
eq(BattleTower.rewardFits(20, 20, 95), false,
|
|
"95 is where MAX_ITEM_STACK - 5 + 1 stops it")
|
|
end
|
|
|
|
-- ================================== BattleTowerAction, every jumptable row
|
|
do
|
|
local save = crystalSave()
|
|
local vm = towerVm(save)
|
|
-- ../pokecrystal/engine/events/battle_tower/battle_tower.asm:855-887
|
|
local unhandled = {}
|
|
for id = 0, BattleTower.NUM_ACTIONS - 1 do
|
|
vm.scriptVar = id
|
|
local before = vm.scriptVar
|
|
local ok = pcall(Specials.ALL.BattleTowerAction, vm)
|
|
if not ok then unhandled[#unhandled + 1] = id end
|
|
if ok and vm.scriptVar == before and before ~= 0 then
|
|
-- a row that answers nothing must be one of the documented no-writes
|
|
local writesNothing = (id == A.SET_EXPLANATION_READ)
|
|
or (id == A.SAVE_AND_QUIT) or (id == A.CHALLENGECANCELED)
|
|
or (id == A.ACTION_06) or (id == A.SAVELEVELGROUP)
|
|
or (id == A.LOADLEVELGROUP) or (id == A.ACTION_0A)
|
|
or (id == A.ACTION_0C) or (id == A.ACTION_11) or (id == A.ACTION_12)
|
|
or (id == A.ACTION_15) or (id == A.ACTION_16) or (id == A.RESETDATA)
|
|
or (id == A.ACTION_1C) or (id == A.ACTION_1D)
|
|
or (id == A.CHOOSEREWARD) or (id == A.SAVEOPTIONS)
|
|
if not writesNothing then unhandled[#unhandled + 1] = id end
|
|
end
|
|
end
|
|
eq(table.concat(unhandled, ","), "",
|
|
"all 32 BattleTowerAction rows run, and only the cart's silent ones "
|
|
.. "leave wScriptVar alone")
|
|
end
|
|
|
|
-- ---- the challenge state machine (:992-1022, :935-949)
|
|
do
|
|
local save = crystalSave()
|
|
local vm = towerVm(save)
|
|
eq(action(vm, A.GET_CHALLENGE_STATE), BattleTower.NO_CHALLENGE,
|
|
"a fresh save is BATTLETOWER_NO_CHALLENGE")
|
|
action(vm, A.SAVE_AND_QUIT)
|
|
eq(action(vm, A.GET_CHALLENGE_STATE), BattleTower.SAVED_AND_LEFT,
|
|
"the quicksave arm parks on BATTLETOWER_SAVED_AND_LEFT")
|
|
action(vm, A.ACTION_1C)
|
|
eq(action(vm, A.GET_CHALLENGE_STATE), BattleTower.WON_CHALLENGE,
|
|
"beating the seventh trainer is BATTLETOWER_WON_CHALLENGE")
|
|
action(vm, A.ACTION_1D)
|
|
eq(action(vm, A.GET_CHALLENGE_STATE), BattleTower.RECEIVED_REWARD,
|
|
"and taking the prize is BATTLETOWER_RECEIVED_REWARD")
|
|
action(vm, A.CHALLENGECANCELED)
|
|
eq(action(vm, A.GET_CHALLENGE_STATE), BattleTower.NO_CHALLENGE,
|
|
"cancelling clears it")
|
|
end
|
|
|
|
-- ---- sBattleTowerSaveFileFlags (:978-1008, :1471-1492)
|
|
do
|
|
local save = crystalSave()
|
|
local tower = BattleTower.state(save)
|
|
eq(tower.saveFileFlags, 0, "a fresh save has no tower flags")
|
|
BattleTower.setSaveFileFlag(save, BattleTower.SAVEFILE_EXPLANATION)
|
|
eq(BattleTower.saveFileFlag(save, BattleTower.SAVEFILE_EXPLANATION), 2,
|
|
"`and 2` answers the masked byte, not a boolean")
|
|
eq(BattleTower.saveFileFlag(save, BattleTower.SAVEFILE_REGISTERED), 0,
|
|
"and bit 0 is untouched")
|
|
BattleTower.setSaveFileFlag(save, BattleTower.SAVEFILE_EXPLANATION)
|
|
eq(tower.saveFileFlags, 2, "`or 2` twice is still 2")
|
|
BattleTower.setSaveFileFlag(save, BattleTower.SAVEFILE_REGISTERED)
|
|
eq(tower.saveFileFlags, 3, "both bits live in the one byte")
|
|
|
|
-- :979-982 -- with no save file on disk the routine returns FALSE without
|
|
-- ever reading the flags.
|
|
local vm = towerVm(save)
|
|
eq(action(vm, A.CHECK_EXPLANATION_READ), 0,
|
|
"no save file means the explanation check answers FALSE")
|
|
eq(action(vm, A.CHECKSAVEFILEISYOURS), 0,
|
|
"and so does the save-file check itself")
|
|
end
|
|
|
|
-- ---- the level group (:1129-1155) and the GS Ball (:1179-1185)
|
|
do
|
|
local save = crystalSave()
|
|
local vm = towerVm(save)
|
|
vm.btLevelGroup = 7
|
|
action(vm, A.SAVELEVELGROUP)
|
|
eq(BattleTower.state(save).levelGroup, 7,
|
|
"SAVELEVELGROUP banks wBTChoiceOfLvlGroup in SRAM")
|
|
vm.btLevelGroup = nil
|
|
action(vm, A.LOADLEVELGROUP)
|
|
eq(vm.btLevelGroup, 7, "and LOADLEVELGROUP brings it back for the hallway")
|
|
|
|
eq(action(vm, A.GSBALL), 0, "no GS Ball, no scene")
|
|
Save.crystalState(save).gsBall = "have"
|
|
eq(action(vm, A.GSBALL), BattleTower.GS_BALL_AVAILABLE,
|
|
"sGSBallFlag reads back as GS_BALL_AVAILABLE ($b)")
|
|
Save.crystalState(save).gsBall = "given"
|
|
eq(action(vm, A.GSBALL), BattleTower.GS_BALL_AVAILABLE,
|
|
"and stays set once the ball has been handed over")
|
|
end
|
|
|
|
-- ---- RESETDATA and the prize (:890-933, :955-976)
|
|
do
|
|
local save = crystalSave()
|
|
local tower = BattleTower.state(save)
|
|
tower.streak = 5
|
|
tower.trainers = { 3, 9 }
|
|
local vm = towerVm(save)
|
|
action(vm, A.RESETDATA)
|
|
eq(tower.streak, 0, "RESETDATA clears sNrOfBeatenBattleTowerTrainers")
|
|
eq(next(tower.trainers), nil, "and the seven sBTTrainers slots")
|
|
|
|
local rewards = {}
|
|
for _ = 1, 200 do
|
|
action(vm, A.CHOOSEREWARD)
|
|
rewards[tower.reward] = true
|
|
end
|
|
eq(rewards.POTION, nil,
|
|
"CHOOSEREWARD never falls back on POTION: it reads gen2Constants.itemOrder")
|
|
eq(rewards[BattleTower.SKIPPED_REWARD], nil, "and never rolls LUCKY_PUNCH")
|
|
for name in pairs(rewards) do
|
|
check(name == "HP_UP" or name == "PROTEIN" or name == "IRON"
|
|
or name == "CARBOS" or name == "CALCIUM",
|
|
name .. " is inside BATTLETOWER_MIN_REWARD..MAX_REWARD")
|
|
end
|
|
eq(rewards.HP_UP, true, "and the whole span comes up (HP_UP)")
|
|
eq(rewards.CALCIUM, true, "through CALCIUM")
|
|
|
|
tower.reward = "CALCIUM"
|
|
eq(action(vm, A.GIVEREWARD), 31, "GIVEREWARD answers the item id")
|
|
-- A full ITEM pocket that does not already hold the reward: the desk hands
|
|
-- over a POTION, which is the script's `ifequal POTION` arm.
|
|
for i = 1, 20 do save.inventory["FILLER_" .. i] = 1 end
|
|
eq(action(vm, A.GIVEREWARD), 18,
|
|
"a stuffed pack turns the prize into POTION")
|
|
end
|
|
|
|
-- ---- Menu_ChallengeExplanationCancel (mobile/mobile_5f.asm:425-468)
|
|
do
|
|
local save = crystalSave()
|
|
for row = 1, 3 do
|
|
local vm = towerVm(save, {
|
|
scriptMenu = function(_header, done) done(row) end,
|
|
})
|
|
vm.scriptVar = 1
|
|
Specials.ALL.Menu_ChallengeExplanationCancel(vm)
|
|
eq(vm.scriptVar, row, "row " .. row .. " comes back as wScriptVar")
|
|
end
|
|
local vm = towerVm(save, {
|
|
scriptMenu = function(_header, done) done(0) end,
|
|
})
|
|
vm.scriptVar = 1
|
|
Specials.ALL.Menu_ChallengeExplanationCancel(vm)
|
|
eq(vm.scriptVar, 4, "a B press is the `.Exit` arm's 4")
|
|
|
|
vm = towerVm(save)
|
|
vm.scriptVar = 1
|
|
Specials.ALL.Menu_ChallengeExplanationCancel(vm)
|
|
eq(vm.scriptVar, 4, "and so is a run with no menu to open")
|
|
end
|
|
|
|
-- ---- BattleTowerRoomMenu (battle_tower.asm:1-5)
|
|
do
|
|
local save = crystalSave()
|
|
save.hallOfFame = { count = 1, teams = {} }
|
|
local pushed
|
|
local vm = towerVm(save, {
|
|
pushScreen = function(id, opts)
|
|
pushed = id
|
|
opts.onDone(6)
|
|
return true
|
|
end,
|
|
})
|
|
Specials.ALL.BattleTowerRoomMenu(vm)
|
|
eq(pushed, "Gen2BattleTowerMenu", "the desk opens the room menu screen")
|
|
eq(vm.scriptVar, 0, "a chosen room answers 0, the script's `ifnotequal $0`")
|
|
eq(vm.btLevelGroup, 6, "and wBTChoiceOfLvlGroup holds the L60 room")
|
|
|
|
vm = towerVm(save, {
|
|
pushScreen = function(_id, opts)
|
|
opts.onDone(nil)
|
|
return true
|
|
end,
|
|
})
|
|
Specials.ALL.BattleTowerRoomMenu(vm)
|
|
eq(vm.scriptVar, 0x0a, "cancelling answers $a, the desk's loop-back arm")
|
|
|
|
vm = towerVm(save)
|
|
Specials.ALL.BattleTowerRoomMenu(vm)
|
|
eq(vm.scriptVar, 0x0a, "and so does a run with no screen to push")
|
|
end
|
|
|
|
-- ============================================ the room menu screen itself
|
|
do
|
|
eq(BattleTowerMenu.levelLabel(1), " L:10 ", "Strings_L10ToL100 row 1")
|
|
eq(BattleTowerMenu.levelLabel(10), " L:100", "and row 10, six tiles wide")
|
|
|
|
local pressed = {}
|
|
local game = { input = { wasPressed = function(_, name)
|
|
return pressed[name] == true
|
|
end } }
|
|
local function press(name) pressed = { [name] = true } end
|
|
|
|
local save = crystalSave()
|
|
save.hallOfFame = { count = 1, teams = {} }
|
|
local result, calls = nil, 0
|
|
local screen = BattleTowerMenu.new(game, {
|
|
save = save,
|
|
party = { mon("PIKACHU", 20), mon("GEODUDE", 20), mon("HORSEA", 20) },
|
|
onDone = function(value) result = value; calls = calls + 1 end,
|
|
})
|
|
eq(screen.cursor, 1, "the spinner opens on L:10")
|
|
eq(screen:rowCount(), 11, "ten rooms plus CANCEL")
|
|
|
|
press("up"); screen:update()
|
|
eq(screen.cursor, 2, "UP walks the level up")
|
|
press("down"); screen:update()
|
|
press("down"); screen:update()
|
|
eq(screen.cursor, 11, "DOWN off the top rolls over to CANCEL")
|
|
press("up"); screen:update()
|
|
eq(screen.cursor, 1, "and UP off CANCEL rolls back to L:10")
|
|
|
|
-- A party at L20 tops the L10 room (mobile/mobile_46.asm:3915-3917).
|
|
press("a"); screen:update()
|
|
eq(screen.phase, "message", "picking L:10 with L20 mons prints a refusal")
|
|
eq(calls, 0, "and does not answer the script")
|
|
for _ = 1, 0x80 do pressed = {}; screen:update() end
|
|
eq(screen.phase, "pick", "the $80-frame hold puts the menu back")
|
|
eq(screen.cursor, 1, "at jumptable index 0, cursor reset")
|
|
|
|
press("up"); screen:update()
|
|
press("a"); screen:update()
|
|
eq(result, 2, "L:20 is accepted and hands back the level group")
|
|
eq(calls, 1, "exactly once")
|
|
end
|
|
|
|
do
|
|
-- The uber gate, and the CANCEL path's yes/no.
|
|
local pressed = {}
|
|
local game = { input = { wasPressed = function(_, name)
|
|
return pressed[name] == true
|
|
end } }
|
|
local function press(name) pressed = { [name] = true } end
|
|
|
|
local save = crystalSave()
|
|
local result, answered = nil, false
|
|
local screen = BattleTowerMenu.new(game, {
|
|
save = save,
|
|
party = { mon("LUGIA", 40), mon("GEODUDE", 40), mon("HORSEA", 40) },
|
|
monName = function(id) return id end,
|
|
onDone = function(value) result = value; answered = true end,
|
|
})
|
|
eq(screen:rowCount(), 5, "no Hall of Fame, so four rooms plus CANCEL")
|
|
for _ = 1, 3 do press("up"); screen:update() end
|
|
eq(screen.cursor, 4, "on L:40")
|
|
press("a"); screen:update()
|
|
eq(screen.phase, "message", "an uber under L70 is refused below the L70 room")
|
|
check(screen.message:find("LUGIA", 1, true) ~= nil,
|
|
"and the refusal names it, the way text_ram wcd49 does")
|
|
-- mobile/mobile_46.asm:5464-5471, home/text.asm:479
|
|
eq(screen.pages and #screen.pages, 2, "Text_UberRestriction's para is a second page")
|
|
for _ = 1, 0x80 do pressed = {}; screen:update() end
|
|
eq(screen.phase, "message", "the first page waits on a button, not the countdown")
|
|
press("a"); screen:update()
|
|
eq(screen.page, 2, "A turns the page")
|
|
check(screen.message:find("Lv.70", 1, true) ~= nil, "to the Lv.70 half")
|
|
|
|
for _ = 1, 0x80 do pressed = {}; screen:update() end
|
|
eq(screen.cursor, 1, "the refusal restarts the menu at L:10")
|
|
for _ = 1, 4 do press("up"); screen:update() end
|
|
eq(screen.cursor, 5, "CANCEL is the last row")
|
|
press("a"); screen:update()
|
|
eq(screen.phase, "quit", "CANCEL opens the yes/no")
|
|
eq(screen.yes, true, "on YES")
|
|
press("down"); screen:update()
|
|
press("a"); screen:update()
|
|
eq(screen.phase, "pick", "NO puts the level spinner back")
|
|
eq(answered, false, "and answers nothing yet")
|
|
press("b"); screen:update()
|
|
eq(screen.phase, "quit", "B on the spinner is the same cancel prompt")
|
|
press("a"); screen:update()
|
|
eq(answered, true, "YES ends the menu")
|
|
eq(result, nil, "with no level group, which the handler turns into $a")
|
|
end
|
|
|
|
-- mobile/mobile_46.asm:5303 hlcoord 1, 14 and home/text.asm:473-477 LineChar
|
|
do
|
|
local Chrome = require("src.ui.gen2.Chrome")
|
|
local seen
|
|
local basePrint, baseBox = Chrome.printWrapped, Chrome.textbox
|
|
Chrome.printWrapped = function(text, tx, ty, _width, rows, step)
|
|
seen = { text = text, tx = tx, ty = ty, rows = rows, step = step }
|
|
return 2
|
|
end
|
|
Chrome.textbox = function() end
|
|
local save = crystalSave()
|
|
local screen = BattleTowerMenu.new(nil, {
|
|
save = save, party = {}, rows = BattleTower.levelGroupRows(save),
|
|
})
|
|
screen:refuse("Cancel your BATTLE\nROOM challenge?")
|
|
screen:drawPanel()
|
|
Chrome.printWrapped, Chrome.textbox = basePrint, baseBox
|
|
eq(seen and seen.tx, 1, "the message starts at column 1")
|
|
eq(seen and seen.ty, 14, "on row 14")
|
|
eq(seen and seen.step, 2, "and its second line lands on row 16")
|
|
eq(seen and seen.rows, 2, "two lines fit the box")
|
|
end
|
|
|
|
-- ================================ wInBattleTowerBattle and the badge boosts
|
|
do
|
|
local GROWTH = { GROWTH_MEDIUM_FAST = { numerator = 1, denominator = 1,
|
|
squared = 0, linear = 0, constant = 0 } }
|
|
local BATTLE_DATA = {
|
|
pokemon = {
|
|
growthRates = GROWTH,
|
|
MACHOP = { id = "MACHOP", index = 66, name = "MACHOP",
|
|
baseStats = { hp = 70, attack = 80, defense = 50, speed = 35,
|
|
specialAttack = 35, specialDefense = 35 },
|
|
types = { "NORMAL", "NORMAL" }, catchRate = 180, baseExp = 75,
|
|
growthRate = "GROWTH_MEDIUM_FAST", genderRatio = 63,
|
|
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", "FLYING" }, catchRate = 255, baseExp = 55,
|
|
growthRate = "GROWTH_MEDIUM_FAST", genderRatio = 127,
|
|
levelMoves = { { level = 1, move = "TACKLE" } }, evolutions = {} },
|
|
},
|
|
moves = {}, type_chart = { types = {}, matchups = {} }, items = {},
|
|
}
|
|
local perfect = { attack = 15, defense = 15, speed = 15, special = 15 }
|
|
perfect.hp = Mon.hpDV(perfect)
|
|
|
|
local BADGES = { ZEPHYR = true, HIVE = true, PLAIN = true, FOG = true,
|
|
MINERAL = true, STORM = true, GLACIER = true, RISING = true }
|
|
|
|
local function newBattle(tower)
|
|
local player = Mon.new(BATTLE_DATA, "MACHOP", 40, { dvs = perfect })
|
|
local wild = Mon.new(BATTLE_DATA, "PIDGEY", 40, { dvs = perfect })
|
|
return Battle.new({ data = BATTLE_DATA, party = { player }, wild = wild,
|
|
save = { player = { id = 7, badges = BADGES, kantoBadges = {} } },
|
|
battleTower = tower,
|
|
random = function(n) return (n or 1) > 1 and 1 or 0 end }), player
|
|
end
|
|
|
|
local outside, player = newBattle(false)
|
|
eq(outside.inBattleTowerBattle, false, "an ordinary battle is not a Tower one")
|
|
eq(outside:battleStat(player, "attack"),
|
|
Battle.boostStat(player.stats.attack),
|
|
"outside the Tower ZEPHYRBADGE still boosts Attack")
|
|
eq(outside:battleStat(player, "speed"),
|
|
Battle.boostStat(player.stats.speed), "and PLAINBADGE Speed")
|
|
eq(outside:badgeTypeBoost(player, "FLYING"), true,
|
|
"and DoBadgeTypeBoosts still fires")
|
|
|
|
local inside, towerPlayer = newBattle(true)
|
|
eq(inside.inBattleTowerBattle, true, "the Tower battle sets it")
|
|
eq(inside:battleStat(towerPlayer, "attack"), towerPlayer.stats.attack,
|
|
"BadgeStatBoosts' second early return drops the Attack boost")
|
|
eq(inside:battleStat(towerPlayer, "defense"), towerPlayer.stats.defense,
|
|
"and Defense")
|
|
eq(inside:battleStat(towerPlayer, "speed"), towerPlayer.stats.speed,
|
|
"and Speed")
|
|
eq(inside:battleStat(towerPlayer, "specialAttack"),
|
|
towerPlayer.stats.specialAttack, "and Special Attack")
|
|
eq(inside:battleStat(towerPlayer, "specialDefense"),
|
|
towerPlayer.stats.specialDefense,
|
|
"and GLACIERBADGE's buggy Special Defense re-check with it")
|
|
eq(inside:badgeTypeBoost(towerPlayer, "FLYING"), false,
|
|
"DoBadgeTypeBoosts takes the same guard (engine/battle/misc.asm:152)")
|
|
|
|
-- The obedience ladder shares Battle:hasBadge and the cart does NOT guard
|
|
-- it (engine/battle/effect_commands.asm:671-696 reads wJohtoBadges raw).
|
|
eq(inside:obedienceLevel(), outside:obedienceLevel(),
|
|
"obedience reads the badges either way")
|
|
eq(inside:hasBadge("badges", "ZEPHYR"), true,
|
|
"and Battle:hasBadge itself is untouched")
|
|
end
|
|
|
|
-- ================================================ Gold and Silver are clean
|
|
do
|
|
for _, version in ipairs({ "gold", "silver" }) do
|
|
local save = Save.normalize({ version = version, generation = 2 })
|
|
eq(save.version, version, version .. " keeps its own version")
|
|
eq(save.battleTower, nil, version .. " grows no battleTower block")
|
|
eq(save.crystal, nil, "nor a crystal one")
|
|
end
|
|
local crystal = Save.normalize({ version = "crystal", generation = 2 })
|
|
eq(crystal.version, "crystal", "and the Crystal file stays Crystal")
|
|
check(type(crystal.battleTower) == "table",
|
|
"which does carry sBattleTowerChallengeState")
|
|
eq(crystal.battleTower.challenge, BattleTower.NO_CHALLENGE,
|
|
"starting at BATTLETOWER_NO_CHALLENGE")
|
|
end
|
|
|
|
S.finish()
|