Files
gen1recomp/tests/game3_moveteach_tutor_test.lua
2026-09-21 11:14:17 -04:00

423 lines
16 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/party_menu.c:1892 GetTutorMove, :1907 CanLearnTutorMove,
-- pokefirered/src/field_specials.c:2219 CapeBrinkGetMoveToTeachLeadPokemon
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function finish()
if failed > 0 then
print(string.format("[FAIL] %d check(s) failed", failed))
os.exit(1)
end
print("[PASS] game3_moveteach_tutor")
os.exit(0)
end
local Cache = require("tests.game3_cache")
local cacheRoot = Cache.mount("pokemon/learnsets.lua")
if not cacheRoot then
print("[skip] game3_moveteach_tutor: " .. tostring(Cache.reason))
os.exit(0)
end
print("[info] FireRed cache at " .. cacheRoot)
local Pokemon = require("src.core.game3.pokemon")
Pokemon.install(nil)
local MoveLearn = require("src.core.game3.move_learn")
local PartyMenu = require("src.ui.game3.party_menu")
local Natives = require("src.core.game3.scripting.natives")
local Flags = require("src.core.game3.scripting.flags")
local Ctx = require("src.core.game3.scripting.ctx")
local MEGA_PUNCH, SWORDS_DANCE, MEGA_KICK = 5, 14, 25
local BODY_SLAM, DOUBLE_EDGE, COUNTER = 34, 38, 68
local SEISMIC_TOSS, MIMIC, METRONOME = 69, 102, 118
local SOFT_BOILED, DREAM_EATER, THUNDER_WAVE = 135, 138, 86
local EXPLOSION, ROCK_SLIDE, SUBSTITUTE = 153, 157, 164
local FRENZY_PLANT, BLAST_BURN, HYDRO_CANNON = 338, 307, 308
local TACKLE = 33
local BULBASAUR, VENUSAUR, CHARIZARD, BLASTOISE = 1, 3, 6, 9
local GEODUDE, CHANSEY = 74, 113
-- pokefirered/include/constants/flags.h:761
local FLAG_TUTOR_FRENZY_PLANT = 0x2DE
local FLAG_TUTOR_BLAST_BURN = 0x2DF
local FLAG_TUTOR_HYDRO_CANNON = 0x2E0
local function monAt(species, level)
local moves, pp, maxPp = Pokemon.movesAtLevel(species, level)
return {
species = species, speciesId = species, level = level,
moves = moves, pp = pp, maxPp = maxPp,
hp = 20, maxHp = 20, friendship = 70, happiness = 70,
}
end
local pressed = nil
local input = {
wasPressed = function(_, key) return key == pressed end,
isDown = function(_, key) return key == pressed end,
}
local function press(key)
pressed = key
PartyMenu.handleInput(input)
pressed = nil
end
print("[test] 1. sTutorMoves")
do
local want = {
[0] = MEGA_PUNCH, SWORDS_DANCE, MEGA_KICK, BODY_SLAM, DOUBLE_EDGE, COUNTER,
SEISMIC_TOSS, MIMIC, METRONOME, SOFT_BOILED, DREAM_EATER, THUNDER_WAVE,
EXPLOSION, ROCK_SLIDE, SUBSTITUTE,
}
check(MoveLearn.TUTOR_MOVE_COUNT == 15, "TUTOR_MOVE_COUNT is 15")
local allOk = true
for tutor = 0, 14 do
if MoveLearn.tutorMove(tutor) ~= want[tutor] then
allOk = false
print(" tutor " .. tutor .. " gave " .. tostring(MoveLearn.tutorMove(tutor))
.. " want " .. tostring(want[tutor]))
end
end
check(allOk, "all fifteen regular tutor moves match pret's table")
check(MoveLearn.tutorMove(MoveLearn.TUTOR_MOVE_FRENZY_PLANT) == FRENZY_PLANT,
"GetTutorMove(TUTOR_MOVE_FRENZY_PLANT) is FRENZY PLANT")
check(MoveLearn.tutorMove(MoveLearn.TUTOR_MOVE_BLAST_BURN) == BLAST_BURN,
"GetTutorMove(TUTOR_MOVE_BLAST_BURN) is BLAST BURN")
check(MoveLearn.tutorMove(MoveLearn.TUTOR_MOVE_HYDRO_CANNON) == HYDRO_CANNON,
"GetTutorMove(TUTOR_MOVE_HYDRO_CANNON) is HYDRO CANNON")
end
print("[test] 2. CanLearnTutorMove, the three hardcoded starter branches")
do
check(MoveLearn.canLearnTutorMove(VENUSAUR, MoveLearn.TUTOR_MOVE_FRENZY_PLANT),
"VENUSAUR can learn FRENZY PLANT")
check(not MoveLearn.canLearnTutorMove(CHARIZARD, MoveLearn.TUTOR_MOVE_FRENZY_PLANT),
"CHARIZARD cannot")
check(MoveLearn.canLearnTutorMove(CHARIZARD, MoveLearn.TUTOR_MOVE_BLAST_BURN),
"CHARIZARD can learn BLAST BURN")
check(not MoveLearn.canLearnTutorMove(BLASTOISE, MoveLearn.TUTOR_MOVE_BLAST_BURN),
"BLASTOISE cannot")
check(MoveLearn.canLearnTutorMove(BLASTOISE, MoveLearn.TUTOR_MOVE_HYDRO_CANNON),
"BLASTOISE can learn HYDRO CANNON")
check(not MoveLearn.canLearnTutorMove(BULBASAUR, MoveLearn.TUTOR_MOVE_HYDRO_CANNON),
"BULBASAUR cannot")
end
print("[test] 3. CanLearnTutorMove, the per-species bitfield")
do
local sets = MoveLearn.tutorLearnsets()
if not sets then
print("[skip] no pokemon/tutor.lua in this cache; sTutorLearnsets is not imported yet")
check(not MoveLearn.canLearnTutorMove(GEODUDE, 13),
"with no imported table every regular tutor refuses")
else
-- pokefirered/src/data/pokemon/tutor_learnsets.h:22
check(MoveLearn.canLearnTutorMove(GEODUDE, 13), "GEODUDE can learn ROCK SLIDE")
check(not MoveLearn.canLearnTutorMove(GEODUDE, 9), "GEODUDE cannot learn SOFT-BOILED")
check(MoveLearn.canLearnTutorMove(CHANSEY, 9), "CHANSEY can learn SOFT-BOILED")
check(MoveLearn.canLearnTutorMove(BULBASAUR, 1), "BULBASAUR can learn SWORDS DANCE")
check(not MoveLearn.canLearnTutorMove(BULBASAUR, 0), "BULBASAUR cannot learn MEGA PUNCH")
check(not MoveLearn.canLearnTutorMove(0, 0), "SPECIES_NONE can learn nothing")
end
end
print("[test] 4. CanMonLearnTMTutor with item 0")
do
local venu = monAt(VENUSAUR, 40)
check(MoveLearn.canMonLearnTutorMove(venu, MoveLearn.TUTOR_MOVE_FRENZY_PLANT)
== MoveLearn.CAN_LEARN_MOVE,
"a VENUSAUR that does not know FRENZY PLANT reports CAN_LEARN_MOVE")
check(MoveLearn.canMonLearnTutorMove(venu, MoveLearn.TUTOR_MOVE_BLAST_BURN)
== MoveLearn.CANNOT_LEARN_MOVE,
"and CANNOT_LEARN_MOVE for the wrong starter move")
local known = monAt(VENUSAUR, 40)
known.moves[1] = FRENZY_PLANT
check(MoveLearn.canMonLearnTutorMove(known, MoveLearn.TUTOR_MOVE_FRENZY_PLANT)
== MoveLearn.ALREADY_KNOWS_MOVE,
"one that already knows it reports ALREADY_KNOWS_MOVE")
local egg = monAt(VENUSAUR, 40)
egg.isEgg = true
check(MoveLearn.canMonLearnTutorMove(egg, MoveLearn.TUTOR_MOVE_FRENZY_PLANT)
== MoveLearn.CANNOT_LEARN_MOVE_IS_EGG,
"an EGG reports CANNOT_LEARN_MOVE_IS_EGG")
end
print("[test] 5. GetLeadMonIndex skips eggs")
do
local egg = monAt(BULBASAUR, 5)
egg.isEgg = true
check(MoveLearn.leadMonIndex({ egg, monAt(VENUSAUR, 40) }) == 1,
"the lead is the first non-egg slot, got "
.. tostring(MoveLearn.leadMonIndex({ egg, monAt(VENUSAUR, 40) })))
check(MoveLearn.leadMonIndex({ monAt(VENUSAUR, 40) }) == 0, "slot 1 otherwise")
check(MoveLearn.leadMonIndex({}) == 0, "and 0 with no party")
end
local prevRuntime = package.loaded["src.core.game3.runtime"]
local store = { flags = {}, vars = {} }
local session = { store = store, party = {} }
package.loaded["src.core.game3.runtime"] = {
getSession = function() return session end,
isActive = function() return true end,
}
print("[test] 6. CapeBrinkGetMoveToTeachLeadPokemon")
do
local party = { monAt(VENUSAUR, 40) }
session.party = party
local function newCtx()
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
return ctx
end
local strings = {}
local adapters = { log = function() end,
setStringVar = function(i, t) strings[i] = t end }
local ctx = newCtx()
local handled = select(3, Natives.special(ctx, 0x1A3, adapters))
check(handled == true, "special 0x1A3 is bound (pokefirered/data/specials.inc:430)")
check(Flags.getVar(store, ctx, 0x800D) == 0,
"a VENUSAUR below max friendship is refused, got "
.. tostring(Flags.getVar(store, ctx, 0x800D)))
party[1].friendship = 255
party[1].happiness = 255
ctx = newCtx()
Natives.special(ctx, 0x1A3, adapters)
check(Flags.getVar(store, ctx, 0x800D) == 1, "at friendship 255 it returns TRUE")
check(Flags.getVar(store, ctx, 0x8005) == MoveLearn.TUTOR_MOVE_FRENZY_PLANT,
"VAR_0x8005 carries MOVETUTOR_FRENZY_PLANT, got "
.. tostring(Flags.getVar(store, ctx, 0x8005)))
check(Flags.getVar(store, ctx, 0x8006) == 4,
"VAR_0x8006 carries the number of known moves, got "
.. tostring(Flags.getVar(store, ctx, 0x8006)))
check(Flags.getVar(store, ctx, 0x8007) == 0, "VAR_0x8007 carries the lead slot")
check(strings[2] == Pokemon.moveName(FRENZY_PLANT),
"STR_VAR_2 holds the move name, got " .. tostring(strings[2]))
ctx = newCtx()
Flags.setFlag(store, ctx, FLAG_TUTOR_FRENZY_PLANT, true)
Natives.special(ctx, 0x1A3, adapters)
check(Flags.getVar(store, ctx, 0x800D) == 0,
"with FLAG_TUTOR_FRENZY_PLANT set it refuses a second time")
Flags.setFlag(store, ctx, FLAG_TUTOR_FRENZY_PLANT, false)
party[1] = monAt(BULBASAUR, 40)
party[1].friendship = 255
ctx = newCtx()
Natives.special(ctx, 0x1A3, adapters)
check(Flags.getVar(store, ctx, 0x800D) == 0, "a non-starter-final lead is refused")
end
print("[test] 7. HasLearnedAllMovesFromCapeBrinkTutor")
do
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
Flags.setVar(store, ctx, 0x8005, MoveLearn.TUTOR_MOVE_FRENZY_PLANT)
local handled = select(3, Natives.special(ctx, 0x1A4, { log = function() end }))
check(handled == true, "special 0x1A4 is bound (pokefirered/data/specials.inc:431)")
check(Flags.getFlag(store, ctx, FLAG_TUTOR_FRENZY_PLANT) == true,
"it burns FLAG_TUTOR_FRENZY_PLANT")
check(Flags.getVar(store, ctx, 0x800D) == 0, "one of three is not all of them")
Flags.setVar(store, ctx, 0x8005, MoveLearn.TUTOR_MOVE_BLAST_BURN)
Natives.special(ctx, 0x1A4, { log = function() end })
check(Flags.getFlag(store, ctx, FLAG_TUTOR_BLAST_BURN) == true,
"then FLAG_TUTOR_BLAST_BURN")
check(Flags.getVar(store, ctx, 0x800D) == 0, "two of three is still not all")
Flags.setVar(store, ctx, 0x8005, MoveLearn.TUTOR_MOVE_HYDRO_CANNON)
Natives.special(ctx, 0x1A4, { log = function() end })
check(Flags.getFlag(store, ctx, FLAG_TUTOR_HYDRO_CANNON) == true,
"then FLAG_TUTOR_HYDRO_CANNON")
check(Flags.getVar(store, ctx, 0x800D) == 1, "all three returns TRUE")
end
print("[test] 8. the party menu MOVE_TUTOR action")
do
local party = { monAt(VENUSAUR, 40), monAt(BULBASAUR, 10) }
party[1].moves[4] = nil
party[1].pp[4] = nil
party[1].maxPp[4] = nil
PartyMenu.show(party, nil, {
mode = "move_tutor",
tutor = MoveLearn.TUTOR_MOVE_FRENZY_PLANT,
})
check(PartyMenu.mode == "move_tutor", "the menu opens in the tutor action")
check(PartyMenu.slotDescription(1) == "ABLE!",
"the VENUSAUR slot reads ABLE!, got " .. tostring(PartyMenu.slotDescription(1)))
check(PartyMenu.slotDescription(2) == "NOT ABLE!",
"the BULBASAUR slot reads NOT ABLE!, got " .. tostring(PartyMenu.slotDescription(2)))
press("a")
check(PartyMenu.mode == "message", "picking the VENUSAUR prints a page")
check(tostring(PartyMenu._messageText):find("learned", 1, true) ~= nil,
"which is the learned page, got " .. tostring(PartyMenu._messageText))
check(Pokemon.knowsMove(party[1], FRENZY_PLANT), "the VENUSAUR knows FRENZY PLANT")
PartyMenu.dismissMessage()
check(PartyMenu._tutorResult == true, "and the menu reports a real teach")
check(not PartyMenu.isOpen(), "the menu closed")
end
print("[test] 9. the refusals")
do
local party = { monAt(BULBASAUR, 10) }
PartyMenu.show(party, nil, {
mode = "move_tutor",
tutor = MoveLearn.TUTOR_MOVE_FRENZY_PLANT,
})
press("a")
check(tostring(PartyMenu._messageText):find("are not compatible", 1, true) ~= nil,
"an incompatible mon gets pret's first page, got " .. tostring(PartyMenu._messageText))
PartyMenu.dismissMessage()
check(tostring(PartyMenu._messageText):find("can't be\nlearned", 1, true) ~= nil,
"then its second page, got " .. tostring(PartyMenu._messageText))
PartyMenu.dismissMessage()
check(PartyMenu._tutorResult == false, "and nothing was taught")
local known = { monAt(VENUSAUR, 40) }
known[1].moves[1] = FRENZY_PLANT
PartyMenu.show(known, nil, {
mode = "move_tutor",
tutor = MoveLearn.TUTOR_MOVE_FRENZY_PLANT,
})
press("a")
check(tostring(PartyMenu._messageText):find("already knows", 1, true) ~= nil,
"one that already knows the move is told so, got " .. tostring(PartyMenu._messageText))
PartyMenu.dismissMessage()
check(PartyMenu._tutorResult == false, "and VAR_RESULT stays FALSE")
end
print("[test] 10. a full moveset takes the replace flow")
do
local party = { monAt(VENUSAUR, 40) }
local mon = party[1]
check(Pokemon.moveSlotCount(mon) == 4, "the VENUSAUR starts with four moves")
PartyMenu.show(party, nil, {
mode = "move_tutor",
tutor = MoveLearn.TUTOR_MOVE_FRENZY_PLANT,
})
press("a")
check(PartyMenu.mode == "message", "the wants-to-learn page opens")
PartyMenu.dismissMessage()
PartyMenu.dismissMessage()
check(PartyMenu.mode == "yesno", "then the delete prompt, got " .. tostring(PartyMenu.mode))
check(tostring(PartyMenu._yesNoPrompt):find("Should a move be deleted", 1, true) ~= nil,
"with pret's field wording, got " .. tostring(PartyMenu._yesNoPrompt))
local cb = PartyMenu._yesNoCallback
cb(false)
check(tostring(PartyMenu._yesNoPrompt):find("Stop trying to teach", 1, true) ~= nil,
"declining asks the stop question, got " .. tostring(PartyMenu._yesNoPrompt))
PartyMenu._yesNoCallback(true)
check(tostring(PartyMenu._messageText):find("did not learn", 1, true) ~= nil,
"confirming prints the did-not-learn page, got " .. tostring(PartyMenu._messageText))
PartyMenu.dismissMessage()
check(PartyMenu._tutorResult == false, "and VAR_RESULT stays FALSE")
check(not Pokemon.knowsMove(mon, FRENZY_PLANT), "the moveset is untouched")
end
print("[test] 11. ChooseMonForMoveTutor sets VAR_RESULT only on a real teach")
do
local party = { monAt(VENUSAUR, 40) }
party[1].moves[4] = nil
party[1].pp[4] = nil
party[1].maxPp[4] = nil
session.party = party
local function newCtx()
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
return ctx
end
local ctx = newCtx()
Flags.setVar(store, ctx, 0x8005, MoveLearn.TUTOR_MOVE_FRENZY_PLANT)
Flags.setVar(store, ctx, 0x8007, 0)
local yielded, _, handled = Natives.special(ctx, 0x18D, { log = function() end })
check(handled == true, "special 0x18D is bound (pokefirered/data/specials.inc:408)")
check(yielded == true, "the script yields while the tutor party menu is up")
check(PartyMenu.isOpen() and PartyMenu.mode == "move_tutor",
"and the menu opened in the tutor action")
press("a")
PartyMenu.dismissMessage()
check(ctx.nativePoll and ctx.nativePoll() == true, "the native finishes when it closes")
check(Flags.getVar(store, ctx, 0x800D) == 1,
"VAR_RESULT is TRUE after a real teach, got " .. tostring(Flags.getVar(store, ctx, 0x800D)))
check(Pokemon.knowsMove(party[1], FRENZY_PLANT), "the mon learned FRENZY PLANT")
party[1] = monAt(VENUSAUR, 40)
ctx = newCtx()
Flags.setVar(store, ctx, 0x8005, MoveLearn.TUTOR_MOVE_FRENZY_PLANT)
Natives.special(ctx, 0x18D, { log = function() end })
press("b")
check(Flags.getVar(store, ctx, 0x800D) == 0,
"cancelling leaves VAR_RESULT FALSE, got " .. tostring(Flags.getVar(store, ctx, 0x800D)))
check(not Pokemon.knowsMove(party[1], FRENZY_PLANT), "and teaches nothing")
end
print("[test] 12. the Cape Brink tutor picks the lead mon with no player choice")
do
local party = { monAt(BLASTOISE, 40), monAt(BULBASAUR, 10) }
party[1].moves[3] = nil
party[1].moves[4] = nil
party[1].pp[3], party[1].pp[4] = nil, nil
party[1].maxPp[3], party[1].maxPp[4] = nil, nil
session.party = party
local ctx = Ctx.new({})
ctx.mode = "bytecode"
ctx.status = "running"
Flags.setVar(store, ctx, 0x8005, MoveLearn.TUTOR_MOVE_HYDRO_CANNON)
Flags.setVar(store, ctx, 0x8007, 0)
Natives.special(ctx, 0x18D, { log = function() end })
check(PartyMenu.mode == "move_tutor", "the party menu opened")
PartyMenu.update(1 / 60)
check(tostring(PartyMenu._messageText):find("learned", 1, true) ~= nil,
"the lead mon is taught with no A press, got " .. tostring(PartyMenu._messageText))
PartyMenu.dismissMessage()
check(Pokemon.knowsMove(party[1], HYDRO_CANNON), "the BLASTOISE knows HYDRO CANNON")
check(Flags.getVar(store, ctx, 0x800D) == 1, "VAR_RESULT is TRUE")
end
package.loaded["src.core.game3.runtime"] = prevRuntime
print("[test] 13. the tutor action never disturbs the plain chooser")
do
local party = { monAt(BULBASAUR, 10) }
PartyMenu.show(party, nil, { mode = "choose" })
check(PartyMenu._tutor == nil, "a plain choose carries no tutor")
check(PartyMenu.slotDescription(1) == nil,
"and prints no ABLE line, got " .. tostring(PartyMenu.slotDescription(1)))
PartyMenu.close()
local free = monAt(BULBASAUR, 10)
check(MoveLearn.tutorMove(99) == nil, "an out-of-range tutor has no move")
check(MoveLearn.canMonLearnTutorMove(free, 99) == MoveLearn.CANNOT_LEARN_MOVE,
"and refuses")
check(Pokemon.knowsMove(free, TACKLE),
"a level 10 BULBASAUR still knows TACKLE from its own learnset")
end
finish()