mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-28 23:00:21 -04:00
267 lines
12 KiB
Lua
267 lines
12 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
|
|
local home = os.getenv("HOME") or ""
|
|
local roots = {}
|
|
local identity = os.getenv("POKEPORT_IDENTITY")
|
|
if identity and identity ~= "" then
|
|
roots[#roots + 1] = home .. "/Library/Application Support/LOVE/" .. identity .. "/emerald/"
|
|
roots[#roots + 1] = home .. "/.local/share/love/" .. identity .. "/emerald/"
|
|
end
|
|
local ROOT
|
|
for _, r in ipairs(roots) do
|
|
local f = io.open(r .. "data/generated/gba/rse/misc/manifest.lua", "rb")
|
|
if f then
|
|
f:close()
|
|
ROOT = r
|
|
break
|
|
end
|
|
end
|
|
if not ROOT then
|
|
print("SKIP emerald_town_npcs_test: no Emerald cache with rse/misc (set POKEPORT_IDENTITY to a current import)")
|
|
os.exit(0)
|
|
end
|
|
|
|
local fs = {}
|
|
function fs:read(rel)
|
|
local f = io.open(ROOT .. rel, "rb")
|
|
if not f then return nil end
|
|
local s = f:read("*a")
|
|
f:close()
|
|
return s
|
|
end
|
|
function fs:exists(rel) return self:read(rel) ~= nil end
|
|
package.loaded["src.core.game3.dataset"] = {
|
|
cache = function() return fs end,
|
|
mountExtractRoots = function() end,
|
|
}
|
|
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local before = GameVersion.get()
|
|
GameVersion.set("emerald")
|
|
|
|
local store = { flags = {}, vars = {} }
|
|
local session = { version = "emerald", gender = 0, name = "BRENDAN", trainerId = 12345, party = {}, flags = {},
|
|
vars = {}, store = store, dex = { seen = {}, caught = {}, owned = {} }, gameStats = {} }
|
|
package.loaded["src.core.game3.runtime"] = { getSession = function() return session end }
|
|
local bundle
|
|
package.loaded["src.core.game3.scripting.space"] = { store = store, ensureBundle = function()
|
|
bundle = bundle or require("src.import.gba.extract_scripts").loadBundle(fs, "data/generated/gba", { allowIncomplete = true })
|
|
return bundle
|
|
end }
|
|
|
|
local Rng = require("src.core.game3.rng")
|
|
local C = require("src.core.game3.constants").of("emerald")
|
|
local Rse = require("src.core.game3.rse.init")
|
|
local Town = require("src.core.game3.rse.town_common")
|
|
|
|
local specials = {}
|
|
local ctx = {
|
|
stringVars = {},
|
|
getVar = function(_, id) return specials[id] or 0 end,
|
|
setVar = function(_, id, v) specials[id] = v end,
|
|
}
|
|
|
|
local function word(name)
|
|
local text = name:gsub("^EC_WORD_", "")
|
|
local groups = require("src.core.game3.easy_chat_text").groups()
|
|
for gid = 1, 20 do
|
|
local g = groups[gid]
|
|
for _, w in ipairs(g and g.words or {}) do
|
|
if w.text == text and gid ~= 18 and gid ~= 19 then return w.id end
|
|
end
|
|
end
|
|
error("no easy chat word " .. name)
|
|
end
|
|
|
|
print("[test] Dewford trend (pokeemerald/src/dewford_trend.c)")
|
|
local Dewford = require("src.core.game3.rse.dewford_trend")
|
|
Rng.SeedRng(0x1234)
|
|
local trends = Dewford.init(session)
|
|
eq(#trends, 5, "five saved trends")
|
|
for i = 2, 5 do
|
|
check(not Dewford.compare(trends[i], trends[i - 1], Dewford.SORT_MODE_NORMAL) or trends[i].trendiness == trends[i - 1].trendiness,
|
|
"trends sorted by trendiness " .. i)
|
|
end
|
|
for _, t in ipairs(trends) do
|
|
check(t.maxTrendiness >= 30 and t.maxTrendiness <= 127, "maxTrendiness in 30..127")
|
|
check(t.trendiness >= 30 and t.trendiness <= t.maxTrendiness, "trendiness in 30..max")
|
|
eq(math.floor(t.words[1] / 512), Town.EC_GROUP.CONDITIONS, "first word from CONDITIONS")
|
|
local g2 = math.floor(t.words[2] / 512)
|
|
check(g2 == Town.EC_GROUP.LIFESTYLE or g2 == Town.EC_GROUP.HOBBIES, "second word from LIFESTYLE/HOBBIES")
|
|
end
|
|
local t = { { trendiness = 40, maxTrendiness = 50, gainingTrendiness = true, rand = 1, words = { 1, 2 } },
|
|
{ trendiness = 10, maxTrendiness = 60, gainingTrendiness = false, rand = 2, words = { 3, 4 } },
|
|
{ trendiness = 3, maxTrendiness = 40, gainingTrendiness = false, rand = 3, words = { 5, 6 } },
|
|
{ trendiness = 45, maxTrendiness = 45, gainingTrendiness = true, rand = 4, words = { 7, 8 } },
|
|
{ trendiness = 30, maxTrendiness = 31, gainingTrendiness = true, rand = 5, words = { 9, 10 } } }
|
|
session.dewfordTrends = t
|
|
Dewford.updatePerDay(2, session)
|
|
local byWords = {}
|
|
for _, x in ipairs(session.dewfordTrends) do byWords[x.words[1]] = x end
|
|
eq(byWords[1].trendiness, 50, "gaining 40 + 10 hits max 50")
|
|
eq(byWords[1].gainingTrendiness, false, "reaching max turns boring")
|
|
eq(byWords[3].trendiness, 0, "boring 10 - 10 reaches 0")
|
|
eq(byWords[3].gainingTrendiness, true, "reaching 0 starts gaining")
|
|
eq(byWords[5].trendiness, 7, "boring 3 drops to 0 then gains the remaining 7")
|
|
eq(byWords[7].trendiness, 35, "45 + 10 over max 45: 55 % 45 = 10, odd quotient -> 45 - 10")
|
|
eq(byWords[7].gainingTrendiness, false, "odd quotient turns boring")
|
|
eq(byWords[9].trendiness, 22, "30 + 10 over 31: 40 % 31 = 9, odd quotient -> 31 - 9")
|
|
Rse.setFlag("FLAG_SYS_CHANGED_DEWFORD_TREND", false)
|
|
Rse.setFlag("FLAG_SYS_MIX_RECORD", false)
|
|
Dewford.init(session)
|
|
local phrase = { word("EC_WORD_COOL"), word("EC_WORD_CAMERA") }
|
|
check(Dewford.trySetTrendyPhrase(phrase, session), "first submission always becomes the trend")
|
|
eq(session.dewfordTrends[1].words[1], phrase[1], "trend word 1 replaced in place")
|
|
check(Rse.flag("FLAG_SYS_CHANGED_DEWFORD_TREND"), "FLAG_SYS_CHANGED_DEWFORD_TREND set")
|
|
check(not Dewford.trySetTrendyPhrase(phrase, session), "an already-saved phrase is rejected")
|
|
local second = { word("EC_WORD_CAMERA"), word("EC_WORD_COOL") }
|
|
Dewford.trySetTrendyPhrase(second, session)
|
|
check(Dewford.isPhraseSaved(second, session), "a later submission is always saved in the list")
|
|
eq(#session.dewfordTrends, 5, "still five saved trends")
|
|
local NativesDewford = require("src.core.game3.scripting.natives_dewford")
|
|
specials[0x8004] = 0
|
|
NativesDewford.BY_NAME.BufferTrendyPhraseString(ctx)
|
|
eq(ctx.stringVars[1], Town.word(phrase[1]) .. " " .. Town.word(phrase[2]), "BufferTrendyPhraseString")
|
|
local _, idx = NativesDewford.BY_NAME.GetDewfordHallPaintingNameIndex(ctx)
|
|
eq(idx, (phrase[1] + phrase[2]) % 8, "painting index = (w0 + w1) & 7")
|
|
|
|
print("[test] Lottery corner (pokeemerald/src/lottery_corner.c)")
|
|
local Lottery = require("src.core.game3.rse.lottery")
|
|
eq(Lottery.matchingDigits(12345, 12345), 5, "5 digits")
|
|
eq(Lottery.matchingDigits(12345, 62345), 4, "last 4 digits")
|
|
eq(Lottery.matchingDigits(12345, 12346), 0, "first digit differs")
|
|
Lottery.setNumber(0x0001D431, session)
|
|
eq(Rse.var("VAR_POKELOT_RND1"), 0xD431, "RND1 = low 16")
|
|
eq(Rse.var("VAR_POKELOT_RND2"), 0x0001, "RND2 = high 16")
|
|
eq(Lottery.getNumber(session), 0x0001D431, "GetLotteryNumber recombines")
|
|
session.party = { { species = 280, otId = 54321 }, { species = 281, otId = 11345, nickname = "RALTS" } }
|
|
local r = Lottery.pickTicket(12345, session)
|
|
eq(r.tier, 2, "3 matching digits -> prize tier 2")
|
|
eq(r.prize, C:require("items", "ITEM_EXP_SHARE"), "tier 2 prize is EXP. SHARE")
|
|
eq(r.where, "party", "match found in the party")
|
|
eq(Town.data().lotteryPrizes[4], C:require("items", "ITEM_MASTER_BALL"), "sLotteryPrizes[3] = MASTER BALL")
|
|
eq(Lottery.ticketString(345), "00345", "ticket zero padded")
|
|
Rng.SeedRng(7)
|
|
local v = Rng.Random()
|
|
Rng.SeedRng(7)
|
|
Lottery.setRandomNumber(2, session)
|
|
eq(Lottery.getNumber(session), Lottery.isoRandomize2(Lottery.isoRandomize2(v)), "two days = two ISO_RANDOMIZE2 steps")
|
|
|
|
print("[test] Mauville old man (pokeemerald/src/mauville_old_man.c)")
|
|
local OldMan = require("src.core.game3.rse.old_man")
|
|
local expect = { [0] = OldMan.BARD, OldMan.BARD, OldMan.HIPSTER, OldMan.HIPSTER, OldMan.TRADER, OldMan.TRADER,
|
|
OldMan.STORYTELLER, OldMan.STORYTELLER, OldMan.GIDDY, OldMan.GIDDY }
|
|
for digit = 0, 9 do
|
|
session.trainerId = 40 + digit
|
|
OldMan.set(session)
|
|
eq(session.oldMan.id, expect[digit], "trainer id last digit " .. digit)
|
|
end
|
|
session.trainerId = 10
|
|
OldMan.set(session)
|
|
eq(#session.oldMan.songLyrics, 6, "bard has 6 lyrics")
|
|
eq(session.oldMan.songLyrics[1], word("EC_WORD_SHAKE"), "default song starts SHAKE")
|
|
local Bard = require("src.core.game3.rse.bard_music")
|
|
local sim = Bard.simulate(session.oldMan.songLyrics)
|
|
check(sim.count > 200, "song lasts more than 200 frames (" .. sim.count .. ")")
|
|
eq(sim.frames[sim.count - 1], "THE DIET\nDANCE", "second paragraph reads THE DIET / DANCE")
|
|
local sang = 0
|
|
for f = 1, sim.count do
|
|
for _, e in ipairs(sim.events[f]) do if e.op == "start" then sang = sang + 1 end end
|
|
end
|
|
check(sang >= 6, "at least one phoneme per word (" .. sang .. ")")
|
|
local tpl = Bard.templatesFor(word("EC_WORD_SHAKE"))
|
|
check(tpl[1].songId < C:require("songs", "NUM_PHONEME_SONGS"), "SHAKE has a phoneme")
|
|
eq(Bard.pitchTableIndex(word("EC_WORD_SHAKE")), (word("EC_WORD_SHAKE") % 4) + math.floor(word("EC_WORD_SHAKE") / 8) % 2,
|
|
"WORD_TO_PITCH_TABLE_INDEX")
|
|
local okBake, pcm = pcall(Bard.bake, sim)
|
|
check(okBake, "bard song bakes: " .. tostring(not okBake and pcm or ""))
|
|
if okBake then
|
|
check(pcm.samples > 44100 * 3, "rendered more than 3 s of audio (" .. pcm.samples .. ")")
|
|
check(pcm.peak > 0.01, "rendered audio is not silent (peak " .. string.format("%.4f", pcm.peak) .. ")")
|
|
end
|
|
|
|
session.trainerId = 18
|
|
OldMan.set(session)
|
|
Rng.SeedRng(99)
|
|
local lines, n = {}, 0
|
|
while OldMan.giddyShouldTellAnotherTale(session) and n < 20 do
|
|
n = n + 1
|
|
lines[n] = OldMan.generateGiddyLine(session)
|
|
end
|
|
check(n >= 1 and n <= 10, "giddy tells 1..10 tales (" .. n .. ")")
|
|
check(lines[1] ~= nil and lines[1] ~= "", "giddy line text")
|
|
|
|
session.trainerId = 16
|
|
OldMan.set(session)
|
|
eq(OldMan.freeStorySlot(session), 0, "storyteller starts empty")
|
|
session.gameStats = { [9] = 42 }
|
|
local ok, value = OldMan.initializeRandomStat(session)
|
|
check(ok, "a stat >= minVal is recorded")
|
|
eq(value, 42, "recorded trainer battles value")
|
|
eq(OldMan.freeStorySlot(session), 1, "one tale known")
|
|
session.gameStats[9] = 43
|
|
OldMan.selectedStory = 0
|
|
check((OldMan.updateStat(session)), "stat increase re-records the tale")
|
|
|
|
session.trainerId = 12
|
|
OldMan.set(session)
|
|
local w = OldMan.unlockRandomTrendySaying(session)
|
|
eq(math.floor(w / 512), Town.EC_GROUP.TRENDY_SAYING, "hipster teaches a trendy saying")
|
|
check(OldMan.isTrendySayingUnlocked(w % 512, session), "saying unlocked")
|
|
|
|
print("[test] Lilycove lady (pokeemerald/src/lilycove_lady.c)")
|
|
local Lady = require("src.core.game3.rse.lilycove_lady")
|
|
for tid = 0, 5 do
|
|
session.trainerId = tid
|
|
Lady.init(session)
|
|
eq(session.lilycoveLady.id, math.floor(tid / 2), "lady from trainer id " .. tid)
|
|
end
|
|
session.trainerId = 0
|
|
Lady.init(session)
|
|
local q = Lady.quiz(session)
|
|
eq(#q.question, 9, "quiz has 9 question words")
|
|
eq(q.correctAnswer, Town.data().lady.quizAnswers[q.questionId + 1], "answer from sQuizLadyQuizAnswers")
|
|
q.playerAnswer = q.correctAnswer
|
|
check(Lady.isAnswerCorrect(session), "correct answer accepted")
|
|
q.playerAnswer = word("EC_WORD_COOL")
|
|
check(not Lady.isAnswerCorrect(session), "wrong answer rejected")
|
|
session.trainerId = 2
|
|
Lady.init(session)
|
|
local f = Lady.favor(session)
|
|
local liked = Town.data().lady.favorAccepted[f.favorId + 1][1]
|
|
check(Lady.doesFavorLadyLikeItem(liked, session), "accepted item liked")
|
|
eq(Lady.favorState(session), Lady.STATE_COMPLETED, "favor state completed")
|
|
Lady.doesFavorLadyLikeItem(f.bestItem, session)
|
|
check(Lady.favorThresholdMet(session), "best item jumps to the gift threshold")
|
|
session.trainerId = 4
|
|
Lady.init(session)
|
|
local c = Lady.contest(session)
|
|
c.category = 0
|
|
check(Lady.givePokeblock({ spicy = 20 }, session), "cool lady likes spicy")
|
|
check(not Lady.givePokeblock({ dry = 20 }, session), "cool lady dislikes dry")
|
|
eq(c.maxSheen, 20, "max sheen recorded")
|
|
local tvData = Lady.contestLadyTvData(session)
|
|
eq(tvData.contestCategory, 0, "TV data carries the category")
|
|
eq(tvData.pokeblockState, Lady.CONTEST_LADY_NORMAL, "one good pokeblock -> CONTEST_LADY_NORMAL")
|
|
check(tvData.nickname ~= "", "TV data carries the contest mon name")
|
|
local gfx, mon = Lady.gfx(session)
|
|
eq(gfx, C:require("event_objects", "OBJ_EVENT_GFX_GIRL_2"), "contest lady gfx")
|
|
eq(mon, C:require("event_objects", "OBJ_EVENT_GFX_ZIGZAGOON_1"), "cool contest mon gfx")
|
|
|
|
print("[test] Daily events (pokeemerald/src/clock.c:36)")
|
|
require("src.core.game3.rse.daily_events")
|
|
local TimeEvents = require("src.core.game3.time_events")
|
|
local perDay = TimeEvents.handlers()
|
|
for _, name in ipairs({ "UpdateDewfordTrendPerDay", "UpdateFrontierManiac", "UpdateFrontierGambler", "SetRandomLotteryNumber" }) do
|
|
check(type(perDay[name]) == "function", name .. " registered")
|
|
end
|
|
Rse.setVar("VAR_FRONTIER_MANIAC_FACILITY", 8)
|
|
require("src.core.game3.rse.daily_events").updateFrontierManiac(5, session)
|
|
eq(Rse.var("VAR_FRONTIER_MANIAC_FACILITY"), 3, "maniac facility wraps at 10")
|
|
|
|
GameVersion.set(before)
|
|
T.finish()
|