mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
575 lines
22 KiB
Lua
575 lines
22 KiB
Lua
-- ../pokecrystal/engine/events/move_tutor.asm:1, engine/events/buena.asm:1
|
|
-- and :64, engine/events/poke_seer.asm:18, mobile/mobile_12_2.asm:191.
|
|
-- ROM-free:
|
|
-- luajit tests/gen2_crystal_extras_test.lua
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
-- The same love stub tests/gen2_menus_test.lua installs; nothing here draws.
|
|
love = love or {}
|
|
love.graphics = love.graphics or {
|
|
getColor = function() return 1, 1, 1, 1 end,
|
|
setColor = function() end,
|
|
rectangle = function() end,
|
|
print = function() end,
|
|
printf = function() end,
|
|
draw = function() end,
|
|
newQuad = function() return {} end,
|
|
newImage = function() return nil end,
|
|
getShader = function() return nil end,
|
|
setShader = function() end,
|
|
newShader = function() error("no shaders in this harness") end,
|
|
getDimensions = function() return 160, 144 end,
|
|
push = function() end, pop = function() end,
|
|
translate = function() end, scale = function() end,
|
|
circle = function() end, clear = function() end,
|
|
}
|
|
love.math = love.math or { random = function(a, b) return b and a or 1 end }
|
|
love.image = love.image or {}
|
|
love.filesystem = love.filesystem or {
|
|
load = function() return nil end,
|
|
getInfo = function() return nil end,
|
|
read = function() return nil end,
|
|
}
|
|
love.timer = love.timer or { getTime = function() return 0 end }
|
|
|
|
local S = require("tests.harness").suite("gen2 crystal extras")
|
|
local check, eq = S.check, S.eq
|
|
|
|
require("src.core.Logger").warn = function() end
|
|
|
|
local Events = require("src.world.gen2.Events")
|
|
local Mon = require("src.battle.gen2.Mon")
|
|
local MoveTutorScreen = require("src.ui.gen2.MoveTutor")
|
|
local Save = require("src.core.gen2.Save")
|
|
local Specials = require("src.script.gen2.Specials")
|
|
local Vm = require("src.script.gen2.Vm")
|
|
|
|
local H = Specials.HANDLERS
|
|
|
|
-- --------------------------------------------------------------- fixtures
|
|
|
|
-- The cache tables the handlers read, plus the landmark registry
|
|
-- GetLandmarkName walks (../pokecrystal/engine/overworld/landmarks.asm:16).
|
|
local DATA = {
|
|
pokemon = {
|
|
tutorMoves = { "FLAMETHROWER", "THUNDERBOLT", "ICE_BEAM" },
|
|
TYPHLOSION = { index = 157, name = "TYPHLOSION",
|
|
tmhm = { "CUT", "STRENGTH" }, tutorMoves = { "FLAMETHROWER" } },
|
|
GEODUDE = { index = 74, name = "GEODUDE",
|
|
tmhm = { "STRENGTH" }, tutorMoves = { "FLAMETHROWER" } },
|
|
LAPRAS = { index = 131, name = "LAPRAS", tmhm = { "SURF" } },
|
|
CYNDAQUIL = { index = 155, name = "CYNDAQUIL" },
|
|
TOTODILE = { index = 158, name = "TOTODILE" },
|
|
CHIKORITA = { index = 152, name = "CHIKORITA" },
|
|
PIKACHU = { index = 25, name = "PIKACHU" },
|
|
RATTATA = { index = 19, name = "RATTATA" },
|
|
HOOTHOOT = { index = 163, name = "HOOTHOOT" },
|
|
SPINARAK = { index = 165, name = "SPINARAK" },
|
|
DROWZEE = { index = 96, name = "DROWZEE" },
|
|
},
|
|
items = {
|
|
ULTRA_BALL = { index = 2, name = "ULTRA BALL" },
|
|
FULL_RESTORE = { index = 16, name = "FULL RESTORE" },
|
|
NUGGET = { index = 92, name = "NUGGET" },
|
|
RARE_CANDY = { index = 50, name = "RARE CANDY" },
|
|
PROTEIN = { index = 33, name = "PROTEIN" },
|
|
IRON = { index = 34, name = "IRON" },
|
|
CARBOS = { index = 35, name = "CARBOS" },
|
|
CALCIUM = { index = 37, name = "CALCIUM" },
|
|
HP_UP = { index = 32, name = "HP UP" },
|
|
POTION = { index = 17, name = "POTION" },
|
|
ANTIDOTE = { index = 18, name = "ANTIDOTE" },
|
|
PARLYZ_HEAL = { index = 21, name = "PARLYZ HEAL" },
|
|
FRESH_WATER = { index = 39, name = "FRESH WATER" },
|
|
SODA_POP = { index = 40, name = "SODA POP" },
|
|
LEMONADE = { index = 41, name = "LEMONADE" },
|
|
POKE_BALL = { index = 5, name = "POKé BALL" },
|
|
GREAT_BALL = { index = 1, name = "GREAT BALL" },
|
|
X_ATTACK = { index = 68, name = "X ATTACK" },
|
|
X_DEFEND = { index = 69, name = "X DEFEND" },
|
|
X_SPEED = { index = 67, name = "X SPEED" },
|
|
},
|
|
moves = {
|
|
FLAMETHROWER = { name = "FLAMETHROWER" },
|
|
THUNDERBOLT = { name = "THUNDERBOLT" },
|
|
ICE_BEAM = { name = "ICE BEAM" },
|
|
TACKLE = { name = "TACKLE" },
|
|
GROWL = { name = "GROWL" },
|
|
MUD_SLAP = { name = "MUD-SLAP" },
|
|
},
|
|
gen2Landmarks = {
|
|
order = {},
|
|
landmarks = {
|
|
NEW_BARK_TOWN = { index = 1, name = "NEW BARK\nTOWN" },
|
|
ROUTE_29 = { index = 2, name = "ROUTE 29" },
|
|
},
|
|
},
|
|
}
|
|
|
|
local function newSave()
|
|
local save = {
|
|
version = "crystal",
|
|
player = { name = "KRIS", id = 0x1234, money = 0 },
|
|
party = {},
|
|
inventory = {},
|
|
bagOrder = {},
|
|
}
|
|
Save.crystalState(save)
|
|
return save
|
|
end
|
|
|
|
-- World:specialHooks, stubbed; every screen answers through its own onDone.
|
|
local function newHooks(save, opts)
|
|
opts = opts or {}
|
|
local vars = opts.vars or {}
|
|
local log = { pushed = {}, sfx = {} }
|
|
local hooks = {
|
|
log = log,
|
|
vars = vars,
|
|
save = function() return save end,
|
|
data = function() return DATA end,
|
|
party = function() return save.party end,
|
|
itemIndex = function(id)
|
|
local def = DATA.items[id]
|
|
return def and def.index
|
|
end,
|
|
hasItem = function(index) return opts.held and opts.held[index] == true end,
|
|
playSfxNamed = function(name) log.sfx[#log.sfx + 1] = name end,
|
|
selectPartyMon = function(prompt, done)
|
|
log.selectPrompt = prompt
|
|
done(opts.pickIndex, opts.pickIndex and save.party[opts.pickIndex])
|
|
end,
|
|
pushScreen = function(id, screenOpts)
|
|
log.pushed[#log.pushed + 1] = { id = id, opts = screenOpts }
|
|
if opts.noScreens then return false end
|
|
local answer = opts.answers and opts.answers[#log.pushed]
|
|
if screenOpts.onDone then screenOpts.onDone(answer) end
|
|
return true
|
|
end,
|
|
}
|
|
return hooks
|
|
end
|
|
|
|
-- The var pair is Vm.readVarFn / Vm.writeVarFn, not a `specials` hook:
|
|
-- engine/overworld/variables.asm's rows belong to `readvar` / `writevar`.
|
|
local function newVm(hooks, scriptVar)
|
|
local vars = hooks.vars
|
|
local vm = Vm.new({}, {}, Events.new(), {
|
|
specials = hooks,
|
|
readVar = function(id) return vars[id] or 0 end,
|
|
writeVar = function(id, value) vars[id] = value end,
|
|
})
|
|
vm.showTextFn = function() end
|
|
vm.scriptVar = scriptVar or 0
|
|
return vm
|
|
end
|
|
|
|
-- Run one handler out, collecting its pages and feeding its YES/NO answers.
|
|
local function run(vm, handler, answers)
|
|
answers = answers or {}
|
|
local pages, asked = {}, 0
|
|
local co = coroutine.create(function() handler(vm) end)
|
|
vm.co = co
|
|
local send
|
|
while true do
|
|
local ok, req = coroutine.resume(co, send)
|
|
if not ok then error(req, 0) end
|
|
if coroutine.status(co) == "dead" then break end
|
|
send = nil
|
|
if type(req) == "table" and req.kind == "text" then
|
|
pages[#pages + 1] = req.text
|
|
elseif type(req) == "table" and req.kind == "yesorno" then
|
|
asked = asked + 1
|
|
send = answers[asked]
|
|
else
|
|
pages.parked = req
|
|
break
|
|
end
|
|
end
|
|
return pages
|
|
end
|
|
|
|
-- ============================================================ the move tutor
|
|
|
|
-- .GetMoveTutorMove (engine/events/move_tutor.asm:36-52); anything that is
|
|
-- not MOVETUTOR_FLAMETHROWER or _THUNDERBOLT falls through to MT03.
|
|
do
|
|
local function tutorMoveFor(value, answer)
|
|
local save = newSave()
|
|
local hooks = newHooks(save, { answers = { answer } })
|
|
local vm = newVm(hooks, value)
|
|
run(vm, H.MoveTutor)
|
|
local pushed = hooks.log.pushed[1]
|
|
return pushed and pushed.opts.move, vm.scriptVar
|
|
end
|
|
eq(select(1, tutorMoveFor(1, true)), "FLAMETHROWER", "MOVETUTOR_FLAMETHROWER is MT01")
|
|
eq(select(1, tutorMoveFor(2, true)), "THUNDERBOLT", "MOVETUTOR_THUNDERBOLT is MT02")
|
|
eq(select(1, tutorMoveFor(3, true)), "ICE_BEAM", "MOVETUTOR_ICE_BEAM is MT03")
|
|
eq(select(1, tutorMoveFor(0, true)), "ICE_BEAM",
|
|
"and anything else falls through to MT03, as the `cp` ladder does")
|
|
|
|
local _, learned = tutorMoveFor(1, true)
|
|
eq(learned, 0, "a taught move leaves wScriptVar FALSE, which is .TeachMove")
|
|
local _, cancelled = tutorMoveFor(1, false)
|
|
eq(cancelled, 255, "and a cancel leaves -1, which is .Incompatible")
|
|
end
|
|
|
|
-- engine/events/move_tutor.asm:29 .cancel, ahead of the script's
|
|
-- takecoins 4000 (maps/GoldenrodCity.asm:124).
|
|
do
|
|
local save = newSave()
|
|
local hooks = newHooks(save, { noScreens = true })
|
|
local vm = newVm(hooks, 2)
|
|
run(vm, H.MoveTutor)
|
|
eq(vm.scriptVar, 255, "no screen is the -1 cancel, not a free lesson")
|
|
end
|
|
|
|
-- `add_mt` gives the three tutor moves TMHM flags 58-60
|
|
-- (constants/item_constants.asm:295-307), so CanLearnTMHMMove sees them.
|
|
do
|
|
local view = MoveTutorScreen.speciesView(DATA.pokemon)
|
|
check(MoveTutorScreen.canLearn(view.TYPHLOSION, "FLAMETHROWER"),
|
|
"TYPHLOSION can be taught FLAMETHROWER")
|
|
check(MoveTutorScreen.canLearn(view.TYPHLOSION, "CUT"),
|
|
"and its ordinary TM list still answers")
|
|
check(not MoveTutorScreen.canLearn(view.LAPRAS, "FLAMETHROWER"),
|
|
"LAPRAS cannot")
|
|
local merged = view.TYPHLOSION.tmhm
|
|
eq(#merged, 3, "the view's tmhm list carries the tutor row too")
|
|
eq(merged[3], "FLAMETHROWER", "appended after the TM/HM rows")
|
|
check(view.TYPHLOSION == view.TYPHLOSION, "and the view is memoised")
|
|
eq(view.MISSINGNO, nil, "a species the cache does not carry stays nil")
|
|
eq(#DATA.pokemon.TYPHLOSION.tmhm, 2, "the cache's own record is not touched")
|
|
end
|
|
|
|
-- ================================================================== Buena
|
|
|
|
-- engine/events/buena_menu.asm:1-9: carry (NO or B) is 0, YES is 1.
|
|
do
|
|
local save = newSave()
|
|
local vm = newVm(newHooks(save))
|
|
run(vm, H.AskRememberPassword, { true })
|
|
eq(vm.scriptVar, 1, "YES is 1, which the script reads as `iffalse` not taken")
|
|
local vm2 = newVm(newHooks(save))
|
|
run(vm2, H.AskRememberPassword, { false })
|
|
eq(vm2.scriptVar, 0, "and NO is 0, .ForgotPassword")
|
|
end
|
|
|
|
-- engine/events/buena.asm:19-23: only wBuenasPassword's low nybble is right.
|
|
do
|
|
local save = newSave()
|
|
-- BuenasPassword4's two rejection rolls, pinned
|
|
-- (engine/pokegear/radio.asm:1470-1487).
|
|
local rolls = { 1, 2 }
|
|
local taken = 0
|
|
local realRandom = Specials.random
|
|
Specials.random = function() taken = taken + 1 return rolls[taken] end
|
|
|
|
local hooks = newHooks(save, { answers = { 1 } })
|
|
local vm = newVm(hooks)
|
|
run(vm, H.BuenasPassword)
|
|
Specials.random = realRandom
|
|
|
|
local pushed = hooks.log.pushed[1]
|
|
eq(pushed.id, "Gen2BuenaPassword", "the show opens its own menu")
|
|
eq(pushed.opts.mode, "password", "in password mode")
|
|
eq(pushed.opts.width, 10, "the box is as wide as the category's points byte")
|
|
eq(table.concat(pushed.opts.words, ","), "CYNDAQUIL,TOTODILE,CHIKORITA",
|
|
".PlacePasswordChoices resolves BUENA_MON rows through GetPokemonName")
|
|
eq(vm.scriptVar, 1, "picking the low nybble's row is the right answer")
|
|
eq(save.crystal.buenaPassword.word, 0x01,
|
|
"and the roll is packed group-high, word-low into wBuenasPassword")
|
|
eq(hooks.vars[0x19], 0x01, "VAR_BUENASPASSWORD sees the same byte")
|
|
|
|
-- DAILYFLAGS2_BUENAS_PASSWORD_F holds the roll for the day
|
|
-- (engine/pokegear/radio.asm:1467).
|
|
local hooks2 = newHooks(save, { answers = { 0 } })
|
|
local vm2 = newVm(hooks2)
|
|
run(vm2, H.BuenasPassword)
|
|
eq(vm2.scriptVar, 0, "row 0 is a wrong guess")
|
|
eq(save.crystal.buenaPassword.word, 0x01, "and the day's password is unchanged")
|
|
eq(table.concat(hooks2.log.pushed[1].opts.words, ","),
|
|
"CYNDAQUIL,TOTODILE,CHIKORITA", "so the menu offers the same three words")
|
|
end
|
|
|
|
-- GetBuenasPassword's four BUENA_* arms (engine/pokegear/radio.asm:1534),
|
|
-- and the points byte as the menu width (engine/events/buena.asm:9-12).
|
|
do
|
|
local save = newSave()
|
|
local realRandom = Specials.random
|
|
local function pin(group, word)
|
|
local rolls, taken = { group + 1, word + 1 }, 0
|
|
Specials.random = function() taken = taken + 1 return rolls[taken] end
|
|
save.crystal.buenaPassword.word = nil
|
|
save.crystal.buenaPassword.day = nil
|
|
local hooks = newHooks(save, { answers = { word } })
|
|
local vm = newVm(hooks)
|
|
run(vm, H.BuenasPassword)
|
|
return hooks.log.pushed[1].opts, vm.scriptVar
|
|
end
|
|
local balls = pin(3, 2)
|
|
eq(table.concat(balls.words, ","), "POKé BALL,GREAT BALL,ULTRA BALL",
|
|
"BUENA_ITEM rows resolve through GetItemName")
|
|
eq(balls.width, 12, "and .Balls is worth 12 points")
|
|
local towns = pin(6, 0)
|
|
eq(table.concat(towns.words, ","), "NEW BARK TOWN,CHERRYGROVE CITY,AZALEA TOWN",
|
|
"BUENA_STRING rows are the literals themselves")
|
|
eq(towns.width, 16, "the widest category is 16 wide")
|
|
local moves = pin(8, 1)
|
|
eq(table.concat(moves.words, ","), "TACKLE,GROWL,MUD-SLAP",
|
|
"BUENA_MOVE rows resolve through GetMoveName")
|
|
local types = pin(7, 0)
|
|
eq(types.width, 6, ".Types is the narrowest box at 6")
|
|
local _, right = pin(10, 2)
|
|
eq(right, 1, "the eleventh category exists and its third row can be picked")
|
|
Specials.random = realRandom
|
|
end
|
|
|
|
-- engine/events/buena.asm:25 .wrong is what a menu-less run has to take.
|
|
do
|
|
local save = newSave()
|
|
local vm = newVm(newHooks(save, { noScreens = true }))
|
|
run(vm, H.BuenasPassword)
|
|
eq(vm.scriptVar, 0, "no menu means no correct answer")
|
|
end
|
|
|
|
-- ------------------------------------------------------------ the prizes
|
|
|
|
-- data/items/buena_prizes.asm, and the cost / ReceiveItem / subtract order
|
|
-- (engine/events/buena.asm:95-119).
|
|
do
|
|
local save = newSave()
|
|
local hooks = newHooks(save, { vars = { [0x18] = 5 }, answers = { 1 } })
|
|
local vm = newVm(hooks)
|
|
local pages = run(vm, H.BuenaPrize, { true })
|
|
|
|
local pushed = hooks.log.pushed[1]
|
|
eq(pushed.opts.mode, "prize", "the counter opens the prize list")
|
|
eq(#pushed.opts.prizes, 9, "NUM_BUENA_PRIZES rows")
|
|
eq(pushed.opts.prizes[1].name, "ULTRA BALL", "the first prize")
|
|
eq(pushed.opts.prizes[1].cost, 2, "costs 2 points")
|
|
eq(pushed.opts.prizes[9].name, "HP UP", "the last prize")
|
|
eq(pushed.opts.prizes[9].cost, 5, "costs 5")
|
|
eq(pushed.opts.balance, 5, "and the Points box shows wBlueCardBalance")
|
|
|
|
eq(save.inventory.ULTRA_BALL, 1, "one ULTRA BALL leaves the counter")
|
|
eq(hooks.vars[0x18], 3, "and the two points come off the card")
|
|
eq(hooks.log.sfx[1], "Sfx_Transaction", "SFX_TRANSACTION rings the sale")
|
|
eq(pages[#pages], "Oh. Please come\nback again!",
|
|
".done prints _BuenaComeAgainText after the loop")
|
|
end
|
|
|
|
-- engine/events/buena.asm:93 `jr c, .loop`: nothing is spent.
|
|
do
|
|
local save = newSave()
|
|
local hooks = newHooks(save, { vars = { [0x18] = 9 }, answers = { 1, 0 } })
|
|
local vm = newVm(hooks)
|
|
run(vm, H.BuenaPrize, { false })
|
|
eq(save.inventory.ULTRA_BALL, nil, "a refused confirmation buys nothing")
|
|
eq(hooks.vars[0x18], 9, "and spends no points")
|
|
eq(#hooks.log.pushed, 2, "the list reopens once before the B press ends it")
|
|
end
|
|
|
|
-- .InsufficientBalance and .BagFull (engine/events/buena.asm:121-127).
|
|
do
|
|
local save = newSave()
|
|
local hooks = newHooks(save, { vars = { [0x18] = 1 }, answers = { 1, 0 } })
|
|
local vm = newVm(hooks)
|
|
local pages = run(vm, H.BuenaPrize, { true })
|
|
eq(save.inventory.ULTRA_BALL, nil, "one point cannot buy a two point ball")
|
|
eq(hooks.vars[0x18], 1, "the balance is untouched")
|
|
check(pages[3] == "You don't have\nenough points.",
|
|
"_BuenaNotEnoughPointsText is the refusal")
|
|
|
|
-- ReceiveItem's own refusal: a stack already at 99.
|
|
local full = newSave()
|
|
full.inventory.ULTRA_BALL = 99
|
|
local fullHooks = newHooks(full, { vars = { [0x18] = 9 }, answers = { 1, 0 } })
|
|
local fullVm = newVm(fullHooks)
|
|
local fullPages = run(fullVm, H.BuenaPrize, { true })
|
|
eq(full.inventory.ULTRA_BALL, 99, "a full stack takes no more")
|
|
eq(fullHooks.vars[0x18], 9, "a full bag spends nothing either")
|
|
check(fullPages[3] == "You have no room\nfor it.",
|
|
"_BuenaNoRoomText is that refusal")
|
|
end
|
|
|
|
-- ================================================================ the Seer
|
|
|
|
local function seerMon(fields)
|
|
local mon = {
|
|
species = "GEODUDE", nickname = "ROCKY", level = 30,
|
|
otId = 0x1234, otName = "KRIS", moves = {},
|
|
}
|
|
for key, value in pairs(fields or {}) do mon[key] = value end
|
|
return mon
|
|
end
|
|
|
|
local function runSeer(mon, cancel)
|
|
local save = newSave()
|
|
save.party = { mon }
|
|
local hooks = newHooks(save, { pickIndex = (not cancel) and 1 or nil })
|
|
local vm = newVm(hooks)
|
|
return run(vm, H.PokeSeer), hooks
|
|
end
|
|
|
|
-- engine/events/poke_seer.asm:38 .cancel, SeerDoNothingText.
|
|
do
|
|
local pages = runSeer(seerMon(), true)
|
|
eq(#pages, 2, "the intro and then the refusal")
|
|
eq(pages[2], "Fufufu! I saw that\nyou'd do nothing!", "_SeerDoNothingText")
|
|
end
|
|
|
|
-- engine/events/poke_seer.asm:28-29 `cp EGG / jr z, .egg`.
|
|
do
|
|
local pages = runSeer(seerMon({ isEgg = true }))
|
|
eq(pages[2], "Hey!\fThat's an EGG!\fYou can't say that\nyou've met it yet…",
|
|
"_SeerEggText")
|
|
end
|
|
|
|
-- ReadCaughtData's `.error` (engine/events/poke_seer.asm:104-105, :133).
|
|
do
|
|
local pages = runSeer(seerMon())
|
|
check(pages[2]:find("Whaaaat", 1, true) ~= nil,
|
|
"a mon with no caught data at all gets _SeerCantTellAThingText")
|
|
end
|
|
|
|
-- SeerAction0 (engine/events/poke_seer.asm:64-70), then SeerAdvice.
|
|
do
|
|
local mon = seerMon({ caughtTime = 1, caughtLevel = 10,
|
|
caughtLocation = 1, caughtByGender = "boy" })
|
|
local pages = runSeer(mon)
|
|
eq(pages[2], "Hm… I see you met\nROCKY here:\vNEW BARK TOWN!",
|
|
"_SeerNameLocationText, with the town map's line break spent as a space")
|
|
eq(pages[3], "The time was\nMorning!\fIts level was 10!\fAm I good or what?",
|
|
"_SeerTimeLevelText")
|
|
check(pages[4]:find("more confident", 1, true) ~= nil,
|
|
"a 20 level gain is SeerMoreConfidentText, the 29 row")
|
|
end
|
|
|
|
-- SeerAction1, and engine/events/poke_seer.asm:110-119 where the OT id's
|
|
-- second `cp` is commented out, so only the HIGH byte decides.
|
|
do
|
|
local traded = seerMon({ otId = 0x9999, otName = "SILVER",
|
|
caughtTime = 3, caughtLevel = 5, caughtLocation = 2 })
|
|
local pages = runSeer(traded)
|
|
eq(pages[2], "Hm… ROCKY\ncame from SILVER\vin a trade?\fROUTE 29\n"
|
|
.. "was where SILVER\vmet ROCKY!", "_SeerTradeText")
|
|
eq(pages[3], "The time was\nNight!\fIts level was 5!\fAm I good or what?",
|
|
"the same time/level page follows")
|
|
|
|
local sameHigh = seerMon({ otId = 0x12ff, caughtTime = 2, caughtLevel = 5,
|
|
caughtLocation = 2 })
|
|
local samePages = runSeer(sameHigh)
|
|
check(samePages[2]:find("I see you met", 1, true) ~= nil,
|
|
"an OT id that differs only in its LOW byte reads as met, bug and all")
|
|
end
|
|
|
|
-- GetCaughtLocation's two sentinels (engine/events/poke_seer.asm:239-249).
|
|
do
|
|
local event = seerMon({ caughtLevel = 20, caughtLocation = Mon.LANDMARK_EVENT })
|
|
local pages = runSeer(event)
|
|
check(pages[2]:find("What!? Incredible!", 1, true) ~= nil,
|
|
"LANDMARK_EVENT is SEERACTION_LEVEL_ONLY, _SeerNoLocationText")
|
|
check(pages[2]:find("was at level 20", 1, true) ~= nil,
|
|
"which still prints the level")
|
|
check(pages[3] ~= nil, "and still gives the advice page")
|
|
|
|
local gift = seerMon({ caughtLevel = 5, caughtLocation = Mon.LANDMARK_GIFT })
|
|
local giftPages = runSeer(gift)
|
|
check(giftPages[2]:find("Whaaaat", 1, true) ~= nil,
|
|
"LANDMARK_GIFT is SEERACTION_CANT_TELL_2, the same refusal")
|
|
eq(#giftPages, 2, "and there is no advice behind it")
|
|
end
|
|
|
|
-- GetCaughtLevel (engine/events/poke_seer.asm:148-179) and GetCaughtTime's
|
|
-- .none arm (:198-201).
|
|
do
|
|
local unknown = seerMon({ caughtLevel = 0, caughtLocation = 1, caughtTime = 0 })
|
|
-- byte1 is set, so ReadCaughtData's `or [hl]` misses its `.error` arm
|
|
-- (engine/events/poke_seer.asm:105).
|
|
local pages = runSeer(unknown)
|
|
eq(pages[3], "The time was\nUnknown!\fIts level was ???!\fAm I good or what?",
|
|
"no time and no level print Unknown and ???")
|
|
|
|
local hatched = seerMon({ caughtLevel = 1, caughtLocation = 1, caughtTime = 2,
|
|
level = 6 })
|
|
local hatchedPages = runSeer(hatched)
|
|
check(hatchedPages[3]:find("level was 5", 1, true) ~= nil,
|
|
"CAUGHT_EGG_LEVEL prints EGG_LEVEL, not 1")
|
|
end
|
|
|
|
-- GetCaughtLocation's .Unknown arm, ../pokecrystal/engine/events/poke_seer.asm
|
|
do
|
|
local Save = require("src.core.gen2.Save")
|
|
local legacy = runSeer(seerMon({ caughtLevel = 30 }))
|
|
check(legacy[2]:find("Unknown", 1, true) ~= nil,
|
|
"a half-stamped mon reads as Unknown before the migration")
|
|
|
|
local file = { format = 7, version = "crystal",
|
|
party = { seerMon({ caughtLevel = 30 }) } }
|
|
Save.migrate(file)
|
|
local pages = runSeer(file.party[1])
|
|
check(pages[2]:find("Whaaaat", 1, true) ~= nil,
|
|
"and as _SeerCantTellAThingText after it")
|
|
end
|
|
|
|
-- SeerAdviceTexts (engine/events/poke_seer.asm:357-364), walked in order;
|
|
-- `sub c` is one byte, so the 255 row catches an underflow.
|
|
do
|
|
local function adviceFor(level, caught)
|
|
local mon = seerMon({ level = level, caughtLevel = caught,
|
|
caughtLocation = 1, caughtTime = 2 })
|
|
return runSeer(mon)[4]
|
|
end
|
|
check(adviceFor(12, 10):find("little more care", 1, true) ~= nil,
|
|
"a 2 level gain is the 9 row")
|
|
check(adviceFor(35, 10):find("more confident", 1, true) ~= nil,
|
|
"25 is the 29 row")
|
|
check(adviceFor(50, 10):find("much strength", 1, true) ~= nil,
|
|
"40 is the 59 row")
|
|
check(adviceFor(70, 10):find("grown mighty", 1, true) ~= nil,
|
|
"60 is the 89 row")
|
|
check(adviceFor(95, 5):find("I'm impressed", 1, true) ~= nil,
|
|
"90 is the 100 row")
|
|
check(adviceFor(5, 10):find("little more care", 1, true) ~= nil,
|
|
"and `sub c` underflowing lands on the 255 row, which repeats the first")
|
|
end
|
|
|
|
-- ================================================ UnusedFindItemInPCOrBag
|
|
|
|
-- mobile/mobile_12_2.asm:194 wNumPCItems, then :201 wNumItems.
|
|
do
|
|
local save = newSave()
|
|
save.pcItems = { RARE_CANDY = 1 }
|
|
local vm = newVm(newHooks(save), 50)
|
|
run(vm, H.UnusedFindItemInPCOrBag)
|
|
eq(vm.scriptVar, 1, "an item in the PC answers TRUE")
|
|
|
|
local vm2 = newVm(newHooks(save, { held = { [92] = true } }), 92)
|
|
run(vm2, H.UnusedFindItemInPCOrBag)
|
|
eq(vm2.scriptVar, 1, "an item in the pack answers TRUE too")
|
|
|
|
local vm3 = newVm(newHooks(save), 92)
|
|
run(vm3, H.UnusedFindItemInPCOrBag)
|
|
eq(vm3.scriptVar, 0, "and neither is FALSE")
|
|
end
|
|
|
|
-- ==================================================== the seam these ride
|
|
|
|
do
|
|
for _, name in ipairs({ "MoveTutor", "BuenasPassword", "BuenaPrize",
|
|
"AskRememberPassword", "PokeSeer", "UnusedFindItemInPCOrBag" }) do
|
|
eq(Specials.HANDLER_SOURCE[name], "specials/crystal_extras.lua",
|
|
name .. " is owned by this module")
|
|
eq(Specials.STUBS[name], nil, name .. " is no longer a stub")
|
|
check(Specials.SUPERSEDED_STUBS[name] ~= nil,
|
|
"and its old stub reason was retired")
|
|
end
|
|
local ids = {}
|
|
for _, id in ipairs(require("src.ui.Screens").GEN2_IDS) do ids[id] = true end
|
|
check(ids.Gen2MoveTutor, "Gen2MoveTutor is a registered screen id")
|
|
check(ids.Gen2BuenaPassword, "Gen2BuenaPassword is one too")
|
|
end
|
|
|
|
S.finish()
|