Files
gen1recomp/tests/game3_teachy_model_test.lua
T
bryanthaboi dee935a4ea CLOSES #2430
2026-09-23 11:58:32 -04:00

289 lines
12 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/teachy_tv.c:420 InitTeachyTvController
package.path = "./?.lua;./?/init.lua;" .. package.path
require("tests.game3_cache").requireData("game3_teachy_model_test")
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 eq(a, b, msg)
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
end
local okReq, TeachyTv = pcall(require, "src.core.game3.teachy_tv")
if not okReq then
print("[FAIL] src/core/game3/teachy_tv.lua does not load: " .. tostring(TeachyTv))
os.exit(1)
end
local Bag = require("src.core.game3.bag")
local Schema = require("src.core.game3.save_schema_firered")
local ItemUse = require("src.core.game3.item_use")
local ItemsData = require("src.core.game3.items_data")
local RomText = require("src.core.game3.rom_text")
local S = TeachyTv.SCRIPT
local function newSession()
return { modData = {}, bag = Bag.new(), party = {} }
end
print("[test] 1. pokefirered/include/teachy_tv.h:4 enum TeachyTvScript")
eq(S.BATTLE, 0, "TTVSCR_BATTLE")
eq(S.STATUS, 1, "TTVSCR_STATUS")
eq(S.MATCHUPS, 2, "TTVSCR_MATCHUPS")
eq(S.CATCHING, 3, "TTVSCR_CATCHING")
eq(S.TMS, 4, "TTVSCR_TMS")
eq(S.REGISTER, 5, "TTVSCR_REGISTER")
eq(#TeachyTv.ORDER, 6, "six lessons")
do
local want = { S.BATTLE, S.STATUS, S.MATCHUPS, S.CATCHING, S.TMS, S.REGISTER }
local same = true
for i = 1, #want do
if TeachyTv.ORDER[i] ~= want[i] then same = false end
end
check(same, "sListMenuItems order is battle, status, matchups, catching, TMs, register")
end
print("[test] 2. pokefirered/src/data/text/teachy_tv.h:1 every lesson has both halves")
do
local labels = {
[S.BATTLE] = "Teach me how to battle.",
[S.STATUS] = "What are status problems?",
[S.MATCHUPS] = "What are type matchups?",
[S.CATCHING] = "I want to catch POKéMON.",
[S.TMS] = "Teach me about TMs.",
[S.REGISTER] = "How do I register an item?",
}
for _, id in ipairs(TeachyTv.ORDER) do
local lesson = TeachyTv.lesson(id)
check(lesson ~= nil, "lesson " .. id .. " exists")
eq(RomText.plain(lesson.labelKey), labels[id], "row label for lesson " .. id)
check(RomText.has(lesson.introKey), lesson.key .. " has a Script1 string")
check(RomText.has(lesson.outroKey), lesson.key .. " has a Script2 string")
end
end
print("[test] 3. page sequences")
do
for _, id in ipairs(TeachyTv.ORDER) do
local intro = TeachyTv.introPages(id)
local outro = TeachyTv.outroPages(id)
check(#intro >= 2, TeachyTv.lesson(id).key .. " intro pages=" .. #intro)
check(#outro >= 2, TeachyTv.lesson(id).key .. " outro pages=" .. #outro)
local blank = 0
for _, page in ipairs(intro) do
if page == "" then blank = blank + 1 end
if select(2, page:gsub("\n", "")) > 1 then blank = blank + 1 end
end
eq(blank, 0, TeachyTv.lesson(id).key .. " intro pages are non-empty and at most two lines")
end
local battle = TeachyTv.introPages(S.BATTLE)
eq(battle[1], "Today, the POKé DUDE's here to\ntell you about how you can battle",
"gTeachyTvText_BattleScript1 page 1")
eq(battle[2], "POKéMON!", "the \\l line becomes page 2")
local last = TeachyTv.outroPages(S.BATTLE)
eq(last[#last], "Remember, TRAINERS, a good deed\na day brings happiness to stay!",
"BattleScript2 ends on the sign-off")
local tms = TeachyTv.outroPages(S.TMS)
eq(tms[1], "Wow, I talked a lot today!\nAll righty, be seeing you!",
"gTeachyTvText_TMsScript2 page 1")
end
print("[test] 4. pokefirered/src/data/text/teachy_tv.h:8 the Pokedude intros")
do
local hello = TeachyTv.pagesOf(TeachyTv.HELLO)
check(#hello >= 4, "PokedudeSaysHello pages=" .. #hello)
eq(hello[1], "Hey, all you TRAINERS out there!\nHELLO, TRAINERS!", "hello page 1")
local tmTypes = TeachyTv.pagesOf(TeachyTv.TM_TYPES)
eq(tmTypes[#tmTypes], "There's one other thing!", "gPokedudeText_TMTypes last page")
local tmDesc = TeachyTv.pagesOf(TeachyTv.TM_DESCRIPTION)
check(#tmDesc >= 5, "gPokedudeText_ReadTMDescription pages=" .. #tmDesc)
end
print("[test] 5. pokefirered/src/teachy_tv.c:554 the TM CASE gate")
do
local s = newSession()
check(not TeachyTv.hasTmCase(s), "no TM CASE on a fresh bag")
local rows = TeachyTv.menuItems(s)
eq(#rows, 5, "sListMenuItems_NoTMCase is four lessons plus CANCEL")
eq(rows[5].index, TeachyTv.CANCEL, "CANCEL is the last row")
eq(rows[4].index, S.CATCHING, "the last lesson without a TM CASE is catching")
eq(TeachyTv.maxShowed(s), 5, "maxShowed 5 without a TM CASE")
Bag.add(s.bag, TeachyTv.ITEM_TM_CASE, 1)
check(TeachyTv.hasTmCase(s), "CheckBagHasItem(ITEM_TM_CASE, 1)")
rows = TeachyTv.menuItems(s)
eq(#rows, 7, "sListMenuItems is six lessons plus CANCEL")
eq(rows[5].index, S.TMS, "the TM lesson appears with a TM CASE")
eq(rows[6].index, S.REGISTER, "the register lesson appears with a TM CASE")
eq(rows[7].index, TeachyTv.CANCEL, "CANCEL stays last")
eq(TeachyTv.maxShowed(s), 6, "maxShowed 6 with a TM CASE")
end
print("[test] 6. pokefirered/src/teachy_tv.c:272 the per-lesson step clusters")
do
for _, id in ipairs({ S.BATTLE, S.STATUS, S.MATCHUPS, S.CATCHING }) do
eq(#TeachyTv.STEPS[id], 19, TeachyTv.lesson(id).key .. " runs the 19-step grass cluster")
eq(TeachyTv.STEPS[id][9], "start_anim_npc_walk_into_grass",
TeachyTv.lesson(id).key .. " walks the dude into the grass")
eq(TeachyTv.RESUME_STEP[id], 12, TeachyTv.lesson(id).key .. " resumes at step 12")
check(TeachyTv.endsInBattle(id), TeachyTv.lesson(id).key .. " hands off to a battle")
eq(TeachyTv.bagLocation(id), nil, TeachyTv.lesson(id).key .. " opens no bag")
end
for _, id in ipairs({ S.TMS, S.REGISTER }) do
eq(#TeachyTv.STEPS[id], 16, TeachyTv.lesson(id).key .. " runs the 16-step bag cluster")
eq(TeachyTv.STEPS[id][9], "battle_or_fade",
TeachyTv.lesson(id).key .. " skips the grass walk")
eq(TeachyTv.RESUME_STEP[id], 9, TeachyTv.lesson(id).key .. " resumes at step 9")
check(not TeachyTv.endsInBattle(id), TeachyTv.lesson(id).key .. " runs no battle")
end
-- pokefirered/include/constants/item_menu.h:18
eq(TeachyTv.bagLocation(S.TMS), 10, "ITEMMENULOCATION_TTVSCR_TMS")
eq(TeachyTv.bagLocation(S.REGISTER), 9, "ITEMMENULOCATION_TTVSCR_REGISTER")
-- pokefirered/src/teachy_tv.c:1181
eq(TeachyTv.battleTransition(S.BATTLE), TeachyTv.TRANSITION.WHITE_BARS_FADE,
"the battle lesson uses B_TRANSITION_WHITE_BARS_FADE")
eq(TeachyTv.battleTransition(S.CATCHING), TeachyTv.TRANSITION.SLICE,
"every other lesson uses B_TRANSITION_SLICE")
end
print("[test] 7. pokefirered/src/teachy_tv.c:420 the controller modes")
do
local s = newSession()
local res = TeachyTv.resources(s)
eq(res.mode, TeachyTv.MODE.FRESH, "a fresh block is mode 0")
eq(res.whichScript, S.BATTLE, "whichScript starts at TTVSCR_BATTLE")
eq(res.scrollOffset, 0, "scrollOffset starts at 0")
eq(res.selectedRow, 0, "selectedRow starts at 0")
TeachyTv.selectLesson(s, S.REGISTER)
res.scrollOffset, res.selectedRow = 1, 3
TeachyTv.initController(s, TeachyTv.MODE.FRESH)
eq(res.whichScript, S.BATTLE, "mode 0 resets whichScript")
eq(res.scrollOffset, 0, "mode 0 resets scrollOffset")
eq(res.selectedRow, 0, "mode 0 resets selectedRow")
TeachyTv.selectLesson(s, S.MATCHUPS)
res.selectedRow = 2
-- pokefirered/src/teachy_tv.c:445
TeachyTv.setModeToResume(s)
eq(res.mode, TeachyTv.MODE.RESUME_LIST, "SetTeachyTvControllerModeToResume sets mode 1")
-- pokefirered/src/teachy_tv.c:437
TeachyTv.returnToTv(s)
eq(res.mode, TeachyTv.MODE.FRESH, "mode 1 collapses to mode 0 in InitTeachyTvController")
eq(res.whichScript, S.MATCHUPS, "but mode 1 does not run the mode 0 reset")
TeachyTv.selectLesson(s, S.CATCHING)
res.selectedRow = 3
TeachyTv.returnToTv(s)
eq(res.mode, TeachyTv.MODE.RESUME_SCRIPT, "a non-resume return comes back as mode 2")
eq(res.whichScript, S.CATCHING, "mode 2 keeps the lesson that ran")
eq(res.selectedRow, 3, "mode 2 keeps the list row")
-- pokefirered/src/teachy_tv.c:1208
eq(TeachyTv.modeAfterBattle(TeachyTv.B_OUTCOME_DREW), TeachyTv.MODE.RESUME_LIST,
"B_OUTCOME_DREW drops back to the lesson list")
eq(TeachyTv.modeAfterBattle(1), TeachyTv.MODE.RESUME_SCRIPT,
"any other outcome resumes the lesson script")
end
print("[test] 8. watched state survives a save round trip")
do
local s = newSession()
eq(TeachyTv.watchedCount(s), 0, "nothing watched on a new game")
check(TeachyTv.markWatched(s, S.CATCHING), "markWatched(CATCHING)")
check(TeachyTv.markWatched(s, S.TMS), "markWatched(TMS)")
TeachyTv.selectLesson(s, S.TMS)
eq(TeachyTv.watchedCount(s), 2, "two lessons watched")
local save = Schema.toSaveTable(s)
local back = Schema.fromSaveTable(save)
check(TeachyTv.hasWatched(back, S.CATCHING), "CATCHING survived the save")
check(TeachyTv.hasWatched(back, S.TMS), "TMS survived the save")
check(not TeachyTv.hasWatched(back, S.BATTLE), "BATTLE was never watched")
eq(TeachyTv.watchedCount(back), 2, "the count survived the save")
local res = TeachyTv.resources(back)
eq(res.whichScript, S.TMS, "whichScript survived the save")
end
print("[test] 9. pokefirered/src/item_use.c:518 FieldUseFunc_TeachyTv")
do
eq(TeachyTv.ITEM_TEACHY_TV, 366, "ITEM_TEACHY_TV")
local info = ItemsData.info(TeachyTv.ITEM_TEACHY_TV)
if not info then
print("[skip] no cache for the item pack, bag entry point not exercised")
else
eq(info.pocket, "KEY_ITEMS", "TEACHY TV is a key item")
local s = newSession()
Bag.add(s.bag, TeachyTv.ITEM_TEACHY_TV, 1)
local ok, kind, text = ItemUse.useField(s, s.bag, TeachyTv.ITEM_TEACHY_TV)
check(ok, "USE on the TEACHY TV succeeds")
eq(kind, "teachy_tv", "the key-item reject arm no longer swallows it")
eq(text, nil, "no OAK refusal message")
local res = TeachyTv.resources(s)
eq(res.mode, TeachyTv.MODE.FRESH, "the bag opens the TV with InitTeachyTvController(0)")
eq(res.whichScript, S.BATTLE, "and on the first lesson row")
eq(Bag.get(s.bag, TeachyTv.ITEM_TEACHY_TV), 1, "a key item is not consumed")
-- pokefirered/src/item_use.c:534 InitTeachyTvFromBag
local Stack = require("src.ui.game3.stack")
local Ui = require("src.ui.game3.teachy_tv")
check(Ui.isOpen(), "USE put the TV screen on the stack")
eq(Stack.top() and Stack.top().id, "teachy_tv", "the TV is the top layer")
eq(Ui.state, "list", "and it is showing the lesson list")
eq(#Ui.rows(), 5, "four lessons plus CANCEL in the screen's own rows")
Ui.close()
Stack.clear()
end
end
print("[test] 9b. pokefirered/src/item_menu.c:2022 UseRegisteredKeyItemOnField")
do
local info = ItemsData.info(TeachyTv.ITEM_TEACHY_TV)
if not info then
print("[skip] no cache for the item pack, the registered item path not exercised")
else
local Stack = require("src.ui.game3.stack")
local Ui = require("src.ui.game3.teachy_tv")
local Game3 = require("src.core.Game3")
local selectInput = {
wasPressed = function(_, key) return key == "select" end,
isDown = function() return false end,
}
Stack.clear()
local s = newSession()
Bag.add(s.bag, TeachyTv.ITEM_TEACHY_TV, 1)
-- pokefirered/src/data/text/teachy_tv.h:182 gTeachyTvText_RegisterScript2
s.registeredItem = TeachyTv.ITEM_TEACHY_TV
Game3._handleRegisteredItem({ input = selectInput, session = s })
check(Ui.isOpen(), "SELECT on the registered TEACHY TV opened the TV")
eq(TeachyTv.resources(s).mode, TeachyTv.MODE.FRESH, "through InitTeachyTvController(0)")
Ui.close()
Stack.clear()
end
end
print("[test] 10. pokefirered/data/specials.inc carries no Teachy TV special")
do
local Natives = require("src.core.game3.scripting.natives")
local mod = Natives.MODULES and Natives.MODULES["natives_teachy"]
check(mod == nil, "no natives_teachy module: the TV is reached from item_use.c, not a script")
end
if failed > 0 then
print(string.format("FAILED %d check(s)", failed))
os.exit(1)
end
print("All teachy tv model checks passed.")