Files
gen1recomp/tests/emerald_frontier_f2_test.lua
T
bryanthaboi 690f0cd9a1 TOUCHDOWN
2026-09-27 16:07:31 -04:00

364 lines
18 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
local GameVersion = require("src.core.GameVersion")
local Profile = require("src.core.game3.profile")
Profile.reset()
GameVersion.set("emerald")
local Dataset = require("src.core.game3.dataset")
local cache = Dataset.cache()
local function has(rel)
local src = cache and cache.read and cache:read(rel)
return type(src) == "string"
end
if not (has("data/generated/gba/frontier/trainers.lua") and has("data/generated/gba/rse/frontier_f2/manifest.lua")) then
print("emerald_frontier_f2_test: skipped (no Emerald cache with rse/frontier_f2; set POKEPORT_IDENTITY)")
os.exit(0)
end
require("src.import.gba.versions").select("emerald")
local C = require("src.core.game3.constants").of("emerald")
local Pokemon = require("src.core.game3.pokemon")
Pokemon.install(nil)
local BattleProfile = require("src.core.game3.battle.profile")
BattleProfile.reset()
local Runtime = require("src.core.game3.runtime")
local Rng = require("src.core.game3.rng")
local State = require("src.core.game3.battle.state")
local store = { flags = {}, vars = {} }
local Space = require("src.core.game3.scripting.space")
Space.store = store
local sess = { version = "emerald", name = "BRENDAN", trainerId = 12345, secretId = 4321, money = 1000, party = {},
gender = 0, bag = {}, flags = store.flags, vars = store.vars }
local prevGet = Runtime.getSession
Runtime.getSession = function() return sess end
local Data = require("src.core.game3.rse.frontier.f2_data")
local D = require("src.core.game3.rse.frontier.trainers")
local Util = require("src.core.game3.rse.frontier.util")
local Rse = require("src.core.game3.rse.init")
local Palace = require("src.core.game3.rse.frontier.palace")
local Arena = require("src.core.game3.rse.frontier.arena")
local Dome = require("src.core.game3.rse.frontier.dome")
local FacPalace = require("src.core.game3.battle.facility_palace")
local FacArena = require("src.core.game3.battle.facility_arena")
local FacDome = require("src.core.game3.battle.facility_dome")
local function mv(name) return C:require("moves", name) end
local function sp(name) return C:require("species", name) end
local man = Data.manifest()
local P, A, M = man.palace, man.arena, man.dome
-- pokeemerald/src/battle_script_commands.c:859
eq(table.concat(P.moveGroupLikelihood[1], ","), "61,68,61,68", "Hardy palace likelihoods (battle_script_commands.c:859)")
eq(table.concat(P.moveGroupLikelihood[4], ","), "38,69,70,85", "Adamant likelihoods")
eq(#P.moveTarget, 25, "one palace target preference per nature")
eq(#P.flavorTextId, 25, "one palace flavor text per nature")
eq(#P.earlyPrizes, 6, "six early palace prizes (battle_palace.c:43)")
eq(P.latePrizes[#P.latePrizes], C:require("items", "ITEM_CHOICE_BAND"), "last late palace prize is the Choice Band")
eq(P.inobedientStringIds[5], C:require("battle_string_ids", "STRINGID_PKMNINCAPABLEOFPOWER"),
"gInobedientStringIds[B_MSG_INCAPABLE_OF_POWER]")
eq(A.mindRatings[mv("MOVE_POUND") + 1], 1, "Pound gives 1 mind point (battle_arena.c:59)")
eq(A.mindRatings[mv("MOVE_PROTECT") + 1], -1, "Protect subtracts a mind point")
eq(A.mindRatings[mv("MOVE_FAKE_OUT") + 1], -1, "Fake Out subtracts a mind point")
eq(A.mindRatings[mv("MOVE_COUNTER") + 1], 0, "Counter gives no mind points")
eq(A.mindRatings[mv("MOVE_GROWL") + 1], 0, "status moves give no mind points")
eq(#A.refereeStrings, 9, "nine referee strings (battle_message.c:1409)")
eq(A.windows[23].left, 2, "ARENA_WIN_JUDGMENT_TEXT sits at column 2 (battle_bg.c:584)")
eq(A.textInfo[23].fg, 2, "judgment text uses dark gray")
eq(table.concat(M.treeTrainerIds, ","), "0,8,12,4,7,15,11,3,2,10,14,6,5,13,9,1", "sTourneyTreeTrainerIds")
eq(table.concat(M.idToOpponentId[1], ","), "8,0,4,8", "sIdToOpponentId[0]")
eq(#M.styleMovePoints, 355, "a battle-style row per move")
eq(#M.styleThresholds, 31, "31 battle-style thresholds")
eq(#M.text.stats, 43, "43 opponent stat texts")
eq(#M.text.potential, 17, "potential texts for 16 seeds and Tucker")
eq(#M.lineSections[1][1], 4, "trainer 1 round 1 advancement line has four tiles (battle_dome.c:1686)")
eq(M.lineSections[1][1][1].tile, 0x6021, "the first line tile is LINE_H in palette 6")
eq(#M.typeEffectiveness, 111, "gTypeEffectiveness rows incl. the Foresight separator")
-- pokeemerald/src/battle_gfx_sfx_util.c:296
eq(FacPalace.moveGroup(mv("MOVE_TACKLE")), FacPalace.GROUP.ATTACK, "Tackle is an attack move")
eq(FacPalace.moveGroup(mv("MOVE_GROWL")), FacPalace.GROUP.SUPPORT, "Growl is a support move")
eq(FacPalace.moveGroup(mv("MOVE_SWORDS_DANCE")), FacPalace.GROUP.DEFENSE, "Swords Dance is a defense move")
eq(FacPalace.moveGroup(mv("MOVE_SPIKES")), FacPalace.GROUP.SUPPORT, "Spikes is a support move")
eq(FacPalace.moveGroup(mv("MOVE_EARTHQUAKE")), FacPalace.GROUP.ATTACK, "Earthquake is an attack move")
local function mon(name, level, moves, personality)
local m = D.createMon(sp(name), level, 20, personality or 0, 99)
local list = {}
for i, n in ipairs(moves) do list[i] = mv(n) end
D.setMoves(m, list)
return m
end
local draws, seq = 0, {}
local function rng(lo, hi)
draws = draws + 1
local v = seq[draws] or 0
if hi == nil then return v end
return lo + (v % (hi - lo + 1))
end
local ADAMANT = 3
local function palaceState(moves, foeMoves)
local st = State.new({
playerParty = { mon("SPECIES_SWAMPERT", 50, moves, ADAMANT) },
foeParty = { mon("SPECIES_METAGROSS", 50, foeMoves or { "MOVE_TACKLE" }, ADAMANT) },
})
st.session = sess
st.aiFlags = 7
st.trainerItems = { 0, 0, 0, 0 }
st.rng = rng
return st
end
local fac = FacPalace.new()
local st = palaceState({ "MOVE_TACKLE", "MOVE_SWORDS_DANCE", "MOVE_GROWL", "MOVE_PROTECT" })
fac:start(st)
draws, seq = 0, { 10 }
local slot = fac:choose(st, 0, nil)
eq(slot, 1, "Adamant with percent 10 < 38 picks the attack group, only Tackle qualifies")
check(not fac.unable[0], "a mon with a move in the chosen group is able to act")
draws, seq = 0, { 50 }
slot = fac:choose(st, 0, nil)
check(slot == 2 or slot == 4, "percent 50 lands in Adamant's defense range 38..68 (slot " .. tostring(slot) .. ")")
draws, seq = 0, { 80 }
slot = fac:choose(st, 0, nil)
eq(slot, 3, "percent 80 >= 69 picks support, only Growl qualifies")
fac.flags[0] = true
draws, seq = 0, { 60 }
slot = fac:choose(st, 0, nil)
eq(slot, 1, "at half HP Adamant uses the second likelihood pair: 60 < 70 is attack")
fac.flags[0] = nil
local st2 = palaceState({ "MOVE_TACKLE", "MOVE_EARTHQUAKE" })
fac:start(st2)
draws, seq = 0, { 90, 5, 99 }
slot = fac:choose(st2, 0, nil)
check(fac.unable[0] == true, "no move in the chosen group and a failed 50% roll leaves the mon incapable")
fac.unable = {}
draws, seq = 0, { 90, 5, 10 }
slot = fac:choose(st2, 0, nil)
check(not fac.unable[0] and (slot == 1 or slot == 2), "the 50% roll can still let the mon use a random move")
-- pokeemerald/src/battle_script_commands.c:6389
local Ui = require("src.core.game3.battle.ui")
Ui.reset({ headless = true })
Ui.bindState(st, sess)
fac:start(st)
st.turn = 0
fac:turnStart(st, nil, true)
st.player.mon.hp = math.floor(st.player.mon.maxHp / 2)
st.turn = 1
fac:turnStart(st, nil, true)
check(fac.flags[0] == true, "a mon at half HP is flagged at the turn boundary")
local log = Ui.log()
local last = log[#log]
check(type(last) == "string" and last ~= "", "the palace flavor text is printed (" .. tostring(last) .. ")")
local before = #Ui.log()
st.turn = 2
fac:turnStart(st, nil, true)
eq(#Ui.log(), before, "the flavor text prints once per mon")
-- pokeemerald/src/battle_gfx_sfx_util.c:325
local std = State.new({ double = true,
playerParty = { mon("SPECIES_SWAMPERT", 50, { "MOVE_TACKLE" }, 0), mon("SPECIES_MANECTRIC", 50, { "MOVE_TACKLE" }, 0) },
foeParty = { mon("SPECIES_METAGROSS", 50, { "MOVE_TACKLE" }), mon("SPECIES_SALAMENCE", 50, { "MOVE_TACKLE" }) } })
std.session = sess
std.rng = rng
std.battlers[1].mon.hp = 10
local pref = P.moveTarget[1]
local want = (pref == FacPalace.TARGET.STRONGER) and 3 or ((pref == FacPalace.TARGET.WEAKER) and 1 or nil)
if want then
eq(fac:target(std, 0), want, "Hardy's doubles target preference follows gBattlePalaceNatureToMoveTarget")
else
local t = fac:target(std, 0)
check(t == 1 or t == 3, "random preference targets a foe")
end
-- pokeemerald/src/battle_palace.c:168
Util.newGame(sess)
local f = Util.frontier(sess)
Rse.setVar("VAR_FRONTIER_BATTLE_MODE", D.MODE.SINGLES, sess)
Rse.setVar("VAR_FRONTIER_FACILITY", D.FACILITY.PALACE, sess)
f.lvlMode = D.LVL.OPEN
Palace.incrementStreak(sess)
eq(Util.get2(f.palaceWinStreaks, 0, 1), 1, "palace streak increments")
eq(Util.get2(f.palaceRecordWinStreaks, 0, 1), 1, "open level records the streak")
f.lvlMode = D.LVL.L50
Util.set2(f.palaceRecordWinStreaks, 0, 0, 10)
Palace.incrementStreak(sess)
eq(Util.get2(f.palaceRecordWinStreaks, 0, 0), 1,
"IncrementPalaceStreak overwrites a higher record with the current streak (battle_palace.c:177)")
local ctx = { specialVars = {} }
Palace.commentId(ctx, sess)
check((Rse.specialVar(ctx, Util.VAR_RESULT)) < 3, "short palace streak comments are random 0..2")
Util.set2(f.palaceWinStreaks, 0, 0, 60)
Palace.commentId(ctx, sess)
eq(Rse.specialVar(ctx, Util.VAR_RESULT), 3, "50..98 palace streak comment is 3")
Palace.setPrize(sess)
local late = false
for _, it in ipairs(P.latePrizes) do if it == f.palacePrize then late = true end end
check(late, "a 60 streak palace prize comes from the late list")
-- pokeemerald/src/battle_arena.c:470
local afac = FacArena.new()
local ast = State.new({ playerParty = { mon("SPECIES_SWAMPERT", 50, { "MOVE_TACKLE" }) },
foeParty = { mon("SPECIES_METAGROSS", 50, { "MOVE_TACKLE" }) } })
ast.session = sess
afac:start(ast)
ast.turn = 0
afac:turnStart(ast, nil, true)
eq(afac.counter, 0, "the arena counter starts at 0 for the first turn")
for t = 1, 2 do
ast.turn = t
afac:turnStart(ast, nil, true)
end
eq(afac.counter, 2, "after two turn boundaries the arena counter is 2 (battle_main.c:3998)")
afac:initPoints(ast)
afac:addMind(0, mv("MOVE_TACKLE"))
afac:addMind(1, mv("MOVE_PROTECT"))
eq(afac.mind[0], 1, "Tackle adds a mind point")
eq(afac.mind[1], -1, "Protect removes a mind point")
afac:addSkill(0, { obeys = true, super = true })
afac:addSkill(1, { obeys = true, noEffect = true })
eq(afac.skill[0], 2, "a super effective hit adds 2 skill points")
eq(afac.skill[1], -2, "a move with no effect costs 2 skill points")
afac:addSkill(1, { obeys = true, noEffect = true, protectedMiss = true })
eq(afac.skill[1], -2, "a miss into Protect costs nothing")
afac:deductSkill(1, "STRINGID_PKMNSXMADEYUSELESS")
eq(afac.skill[1], -5, "an ability blocking the move costs 3 skill points")
ast.enemy.mon.hp = math.floor(ast.enemy.mon.hp / 2)
local result, rows, total = afac:judge(ast)
eq(result, FacArena.RESULT.PLAYER_WON, "the player wins mind, skill and body")
eq(total[0], 6, "three circles are six points")
eq(rows[0].icons[0], FacArena.ANIM.CIRCLE, "mind circle for the player")
eq(rows[0].icons[1], FacArena.ANIM.X, "mind cross for the opponent")
local flags = FacArena.moveFlags({ { kind = "move", attackerId = 0 }, { kind = "msg", id = "STRINGID_NOTVERYEFFECTIVE" } }, 0)
check(flags.obeys and flags.notVery, "move events are read into skill flags")
Ui.reset({ headless = true })
Ui.bindState(ast, sess)
afac:endTurn(ast, nil, true)
eq(ast.enemy.mon.hp, 0, "the judged loser's mon is knocked out (battle_script_commands.c:6410)")
eq(ast.player.mon.hp > 0, true, "the judged winner keeps its HP")
-- pokeemerald/src/battle_arena.c:731
Rse.setVar("VAR_FRONTIER_FACILITY", D.FACILITY.ARENA, sess)
f.lvlMode = D.LVL.L50
Util.set1(f.arenaWinStreaks, 0, 7)
f.winStreakActiveFlags = 0
Arena.init(sess)
eq(Util.get1(f.arenaWinStreaks, 0), 0, "InitArenaChallenge clears an inactive streak")
Arena.setPrize(sess)
local short = false
for _, it in ipairs(A.shortPrizes) do if it == f.arenaPrize then short = true end end
check(short, "a fresh arena challenge sets a short-streak prize")
-- pokeemerald/src/battle_dome.c:2801
local E = Dome.EFFECTIVENESS
eq(Dome.typeEffectivenessPoints(mv("MOVE_WATER_GUN"), sp("SPECIES_CHARMANDER"), E.GOOD, sess), 4, "x2 is 4 good points")
eq(Dome.typeEffectivenessPoints(mv("MOVE_WATER_GUN"), sp("SPECIES_CHARMANDER"), E.BAD, sess), -2, "x2 is -2 bad points")
eq(Dome.typeEffectivenessPoints(mv("MOVE_WATER_GUN"), sp("SPECIES_CHARMANDER"), E.AI_VS_AI, sess), 12, "x2 is 12 AI points")
eq(Dome.typeEffectivenessPoints(mv("MOVE_EARTHQUAKE"), sp("SPECIES_GASTLY"), E.BAD, sess), 0,
"Levitate ground in bad mode falls through to 0 (battle_dome.c:2821)")
eq(Dome.typeEffectivenessPoints(mv("MOVE_EARTHQUAKE"), sp("SPECIES_GASTLY"), E.GOOD, sess), 2,
"Levitate ground in good mode counts as neutral")
eq(Dome.typeEffectivenessPoints(mv("MOVE_GROWL"), sp("SPECIES_GASTLY"), E.GOOD, sess), 0, "status moves score 0")
eq(Dome.typeEffectivenessPoints(mv("MOVE_THUNDERBOLT"), sp("SPECIES_SHEDINJA"), E.GOOD, sess), 2,
"Wonder Guard never multiplies (battle_dome.c:2839)")
local stats = Dome.calcMonStats(sp("SPECIES_METAGROSS"), 50, 31, 0x3F, 0)
local base = Pokemon.stats(sp("SPECIES_METAGROSS"))
eq(stats[0], math.floor((2 * base.hp + 31) * 50 / 100) + 60, "CalcDomeMonStats HP ignores EVs (battle_dome.c:2531)")
eq(stats[1], (math.floor((2 * base.atk + 31) * 50 / 100) + 5) % 256, "CalcDomeMonStats Attack")
local flags2 = Dome.aiTypeCalc(mv("MOVE_THUNDERBOLT"), sp("SPECIES_GYARADOS"), 0, sess)
eq(flags2, 2, "Thunderbolt on Gyarados is super effective x4")
Rse.setVar("VAR_FRONTIER_FACILITY", D.FACILITY.DOME, sess)
Rse.setVar("VAR_FRONTIER_BATTLE_MODE", D.MODE.SINGLES, sess)
f.lvlMode = D.LVL.L50
sess.party = {
mon("SPECIES_SWAMPERT", 50, { "MOVE_SURF", "MOVE_EARTHQUAKE", "MOVE_ICE_BEAM", "MOVE_PROTECT" }),
mon("SPECIES_METAGROSS", 50, { "MOVE_METEOR_MASH", "MOVE_EARTHQUAKE", "MOVE_PSYCHIC", "MOVE_EXPLOSION" }),
mon("SPECIES_SALAMENCE", 50, { "MOVE_DRAGON_CLAW", "MOVE_FLAMETHROWER", "MOVE_AERIAL_ACE", "MOVE_ROCK_SLIDE" }),
}
f.selectedPartyMons = { 1, 2, 3, 0 }
Rng.SeedRng(0xBEEF)
Dome.init(sess)
local scores = Dome.initTrainers(sess)
local fd = Dome.frontier(sess)
local ids, dup = {}, false
for i = 1, 16 do
local tid = fd.domeTrainers[i].trainerId
if ids[tid] then dup = true end
ids[tid] = true
end
check(not dup, "sixteen distinct tourney trainers")
check(ids[D.TRAINER_PLAYER] == true, "the player is seeded into the tourney")
local sorted = true
for i = 0, 14 do if scores[i] < scores[i + 1] then sorted = false end end
check(sorted, "tourney seeds are sorted by ranking score (battle_dome.c:2466)")
local ptid = Dome.tournamentId(sess, D.TRAINER_PLAYER)
local opp = Dome.opponentTournamentId(sess, 0, D.TRAINER_PLAYER)
eq(opp, M.idToOpponentId[ptid + 1][1], "round 1 opponent comes from sIdToOpponentId")
sess.frontierOpponentA = Dome.playerOpponentTrainerId(sess)
eq(Dome.tournamentId(sess, sess.frontierOpponentA), opp, "dome_setopponent picks the round 1 opponent")
local party = Dome.initOpponentParty(sess)
eq(#party, 2, "the dome opponent brings two of three mons (battle_dome.c:2615)")
eq(party[1].level, 50, "dome opponents are level 50 in the lv50 tourney")
local ivs = party[1].ivs.atk
eq(ivs, 3, "CreateDomeOpponentMon passes the tourney id to GetDomeTrainerMonIvs (battle_dome.c:2590)")
sess.frontierDomeLastMoves = { player = mv("MOVE_SURF"), opponent = 0 }
Dome.resolveWinners({ specialVars = {} }, sess, Dome.PLAYER_WON_MATCH)
local out = 0
for i = 1, 16 do if fd.domeTrainers[i].isEliminated then out = out + 1 end end
eq(out, 8, "after round 1 eight trainers are out (battle_dome.c:5191)")
eq(fd.domeWinningMoves[opp + 1], mv("MOVE_SURF"), "the player's last move is the winning move")
check(not fd.domeTrainers[ptid + 1].isEliminated, "the player advances")
local match = M.idToMatchNumber[M.pairedTrainerIds[ptid + 1] + 1][1]
local ws = Dome.winString(sess, match)
eq(ws.id, Dome.TEXT.WON_USING_MOVE, "a decided round-1 match reports the winning move")
eq(ws.var1, sess.name, "the player is named as the winner")
fd.curChallengeBattleNum = 1
local nxt = Dome.playerOpponentTrainerId(sess)
check(nxt ~= 0 and nxt ~= D.TRAINER_PLAYER, "round 2 has an opponent for the player")
for tid = 0, 15 do
local card = Dome.trainerCard(sess, tid)
check(card.style >= 0 and card.style <= 31 and card.statText ~= nil, "info card style/stat for tourney id " .. tid)
end
fd.domePlayerPartyData = {}
for i = 1, 3 do fd.domePlayerPartyData[i] = { moves = { 0, 0, 0, 0 }, evs = { 6, 252, 0, 252, 0, 0 }, nature = 0 } end
eq(Dome.statTextId(sess, ptid), 6, "Atk+Speed EVs pick the atk-and-speed text (battle_dome.c:4686)")
local ctx2 = { specialVars = {} }
Dome.compareSeeds(ctx2, sess)
check(Rse.specialVar(ctx2, Util.VAR_RESULT) == 1 or Rse.specialVar(ctx2, Util.VAR_RESULT) == 2, "dome_compareseeds returns 1 or 2")
-- pokeemerald/data/battle_scripts_1.s:2958
local dfac = FacDome.new()
local dst = State.new({ playerParty = { mon("SPECIES_SWAMPERT", 50, { "MOVE_TACKLE" }) },
foeParty = { mon("SPECIES_METAGROSS", 50, { "MOVE_TACKLE" }) } })
dst.session = sess
dst.playerParty[1].hp, dst.foeParty[1].hp = 0, 0
eq(dfac:finalResult(dst, "lose"), "draw", "both sides wiped in the dome is a draw")
dst.foeParty[1].hp = 5
eq(dfac:finalResult(dst, "lose"), "lose", "a normal loss stays a loss")
local Natives = require("src.core.game3.scripting.natives")
Natives.bind("emerald")
for _, name in ipairs({ "CallBattleDomeFunction", "CallBattlePalaceFunction", "CallBattleArenaFunction", "DoDomeConfetti" }) do
local id = C.specials.byName[name]
check(id ~= nil and Natives.ALLOW["special:" .. id] ~= nil, name .. " is bound on Emerald")
end
check(Rse.system("palace") == Palace and Rse.system("arena") == Arena and Rse.system("dome") == Dome,
"the facilities register their doSpecialBattle handlers")
Natives.bind("firered")
check(Natives.handlerFor("CallBattleDomeFunction") == nil, "FireRed never binds the dome")
Runtime.getSession = prevGet
GameVersion.set("firered")
T.finish("emerald_frontier_f2_test")