mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
ca4d3d283c
Crystal boots from a user-supplied ROM, imports a full cache and is playable: copyright, the Crystal intro movie, the animated title, gender select, Oak, and out into Johto. 122 of the cart's 169 script specials are implemented. Import and data - tools/make_crystal_manifest.py derives the manifest by importing make_gold_manifest as a library, with three additive keyword seams. Gold and Silver still regenerate byte-identical, which is the standing requirement for touching that generator. - crystal_symbol_deltas.py and crystal_movie_symbols.py carry the symbol delta: Crystal renames the credits mons, splits the trainer card, Pokegear and pack-pal blocks by gender, and replaces the intro and title outright. - Crystal-only manifest keys: engineFlagOrder (162 flags to Gold's 93, so the badge block sits one higher) and unownCharmap (the main charmap parser stops at the first newcharmap so the two cannot contaminate each other). Extractor - RomExtractorGen2 becomes three-edition. Crystal corrections: PAL_MAP_BANK 0x13, a flat PICS_FIX pic bank, audio bank 0x5e, the mapSongs id-100 hole, seven NPC trades, a TradeTexts stride of 8, the five Crystal tileset anim steps with per-row degrade, and the column-major trainer card portraits. - New: animated front sprites (frames, bitmasks, play and idle scripts), the Battle Tower roster, Kris assets, Mobile System GB art, and the Crystal intro and title via src/import/CrystalMovie.lua. Engine - GameVersion gains engine(id) and fixes(id). Gold and Silver keep their original bugs where the bug is not hardware dependent; Crystal gets the fixes Crystal shipped: Lucky Number boxes 10-14, surfing onto an NPC, and the Reflect and Light Screen defence overflow. - Crystal story: Suicune and Eusine, Celebi behind the GS Ball flag, the Ruins of Alph chambers, Buena, the Move Tutor, the Poke Seer, and the Battle Tower including the wInBattleTowerBattle badge-boost guard. - Kris and the gender flag, animated fronts in battle and the summary screen, and mon caught data. Verification - Every extracted asset is pixel-compared against pret's own source PNGs. - Gold caches are byte-identical before and after, file for file. - New Crystal suites plus a T2 Gen 2 tier; the full suite passes.
560 lines
21 KiB
Lua
560 lines
21 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
|
|
|
|
-- 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()
|