Files
gen1recomp/tests/game3_growth_evolution_test.lua

237 lines
11 KiB
Lua

#!/usr/bin/env luajit
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 eq(a, b, msg)
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
end
local function finish()
if failed > 0 then
print("[test] FAILED " .. failed)
os.exit(1)
end
print("[test] all passed")
os.exit(0)
end
local Cache = require("tests.game3_cache")
local cacheRoot = Cache.mount("pokemon/evolutions.lua")
if not cacheRoot then
print("[skip] game3_growth_evolution_test: " .. tostring(Cache.reason))
finish()
end
print("[info] FireRed cache at " .. cacheRoot)
local Pokemon = require("src.core.game3.pokemon")
Pokemon.install(nil)
local ItemsData = require("src.core.game3.items_data")
ItemsData.ensureLoaded()
local Evolution = require("src.core.game3.evolution")
local Schema = require("src.core.game3.save_schema_firered")
local Storage = require("src.core.game3.storage")
local EEVEE, ESPEON, UMBREON, VAPOREON = 133, 196, 197, 134
local WURMPLE, SILCOON, CASCOON = 290, 291, 293
local NINCADA, NINJASK = 301, 302
local GOLBAT, CROBAT = 42, 169
local SWAMPERT, ARON, KYOGRE = 285, 382, 404
local OLD_UNOWN_J = 260
local ITEM_WATER_STONE = 97
local national = { national_dex_unlocked = true }
local kantoOnly = { national_dex_unlocked = false }
local function mon(t)
local m = {
species = t.species,
speciesId = t.species,
level = t.level or 50,
friendship = t.friendship or 70,
personality = t.personality or 0,
beauty = t.beauty or 0,
item = t.item or 0,
}
if t.numbering then m.speciesNumbering = t.numbering end
return m
end
print("[test] 1. EVO_FRIENDSHIP_DAY / EVO_FRIENDSHIP_NIGHT are dead on this cart")
local eeveeRows = Pokemon.evolutions(EEVEE)
local hasDay, hasNight = false, false
for _, row in ipairs(eeveeRows) do
local m = tonumber(row.method or row[1]) or 0
if m == Evolution.EVO_FRIENDSHIP_DAY then hasDay = true end
if m == Evolution.EVO_FRIENDSHIP_NIGHT then hasNight = true end
end
check(hasDay, "the pack carries EEVEE's EVO_FRIENDSHIP_DAY row (method 2, ESPEON)")
check(hasNight, "the pack carries EEVEE's EVO_FRIENDSHIP_NIGHT row (method 3, UMBREON)")
local realDate, realTime = os.date, os.time
local function atHour(hour, fn)
os.date = function(fmt, when)
if fmt == "*t" or fmt == "!*t" then
local t = realDate("*t", when)
t.hour = hour
return t
end
if type(fmt) == "string" and (fmt == "%H" or fmt == "!%H") then
return string.format("%02d", hour)
end
return realDate(fmt, when)
end
os.time = function(t) return realTime(t) end
local ok, res = pcall(fn)
os.date, os.time = realDate, realTime
if not ok then error(res, 0) end
return res
end
for _, hour in ipairs({ 13, 1 }) do
local lit = (hour == 13) and "13:00" or "01:00"
local target = atHour(hour, function()
return Evolution.levelTarget(mon({ species = EEVEE, friendship = 255 }), national)
end)
eq(target, nil, "at " .. lit .. " a friendship 255 EEVEE still does not evolve")
check(target ~= ESPEON and target ~= UMBREON,
"at " .. lit .. " neither ESPEON nor UMBREON is reachable")
end
eq(Evolution.itemTarget(mon({ species = EEVEE }), ITEM_WATER_STONE, national), VAPOREON,
"EEVEE's live EVO_ITEM arm still gives VAPOREON")
eq(Evolution.levelTarget(mon({ species = GOLBAT, friendship = 220 }), national), CROBAT,
"plain EVO_FRIENDSHIP still evolves GOLBAT at 220")
eq(Evolution.levelTarget(mon({ species = GOLBAT, friendship = 219 }), national), nil,
"and not at 219")
print("[test] 2. a bare species id reads as the internal SPECIES it is")
eq(Pokemon.speciesOf(mon({ species = WURMPLE })), WURMPLE,
"untagged 290 is WURMPLE, not national 290 NINCADA")
eq(Pokemon.name(Pokemon.speciesOf(mon({ species = WURMPLE }))), "WURMPLE", "and it is named WURMPLE")
eq(Pokemon.speciesOf(mon({ species = ARON })), ARON,
"untagged 382 is ARON, not national 382 KYOGRE")
eq(Pokemon.speciesOf(mon({ species = EEVEE })), EEVEE, "Kanto ids are unchanged")
eq(Pokemon.speciesOf(mon({ species = 251 })), 251, "and so is the Johto end of the range")
print("[test] 3. the numbering tag is what disambiguates")
eq(Pokemon.speciesOf(mon({ species = WURMPLE, numbering = Pokemon.NUMBERING_INTERNAL })), WURMPLE,
"internal 290 stays WURMPLE")
eq(Pokemon.speciesOf(mon({ species = WURMPLE, numbering = Pokemon.NUMBERING_NATIONAL })), NINCADA,
"national 290 resolves to internal NINCADA")
eq(Pokemon.speciesOf(mon({ species = ARON, numbering = Pokemon.NUMBERING_NATIONAL })), KYOGRE,
"national 382 resolves to internal KYOGRE")
eq(Pokemon.speciesOf(mon({ species = EEVEE, numbering = Pokemon.NUMBERING_NATIONAL })), EEVEE,
"national 133 is internal 133")
check(Pokemon.isInternalSpecies(WURMPLE), "290 is a named internal species")
check(not Pokemon.isInternalSpecies(OLD_UNOWN_J),
"260 is an OLD_UNOWN placeholder, not a species")
eq(Pokemon.speciesOf(mon({ species = OLD_UNOWN_J })), SWAMPERT,
"so an untagged 260 falls through to national 260 SWAMPERT")
eq(Pokemon.numberingOf(mon({ species = WURMPLE })), nil, "an untagged mon reports no numbering")
eq(Pokemon.numberingOf(Pokemon.tagNumbering(mon({ species = WURMPLE }))), Pokemon.NUMBERING_INTERNAL,
"tagNumbering defaults to internal")
print("[test] 4. the Hoenn evolution methods now reach the right rows")
eq(Evolution.levelTarget(mon({ species = WURMPLE, level = 7, personality = 0 }), national), SILCOON,
"a level 7 WURMPLE with upper % 10 <= 4 gives SILCOON")
eq(Evolution.levelTarget(mon({ species = WURMPLE, level = 7, personality = 5 * 65536 }), national),
CASCOON, "and with upper % 10 > 4 it gives CASCOON")
eq(Evolution.levelTarget(mon({ species = WURMPLE, level = 6, personality = 0 }), national), nil,
"below level 7 nothing matches")
eq(Evolution.levelTarget(
mon({ species = WURMPLE, level = 20, numbering = Pokemon.NUMBERING_NATIONAL }), national),
NINJASK, "the same id tagged national is NINCADA and gives NINJASK at 20")
print("[test] 5. levelTarget still gates on the national dex flag")
eq(Evolution.levelTarget(mon({ species = GOLBAT, friendship = 220 }), kantoOnly), nil,
"CROBAT is blocked while the national dex is locked")
eq(Evolution.levelTarget(mon({ species = WURMPLE, level = 7 }), kantoOnly), nil,
"SILCOON is blocked too")
eq(Evolution.levelTarget(mon({ species = WURMPLE, level = 7 }), national), SILCOON,
"and both come back once it is unlocked")
print("[test] 6. an old save's mons are stamped on load")
local legacy = {
schemaVersion = 1,
engine = "game3",
version = "firered",
name = "RED",
party = { { species = WURMPLE, speciesId = WURMPLE, level = 7, hp = 10, maxHp = 10 } },
storage = Storage.serialize(Storage.new()),
}
legacy.storage.boxes[1] = {
name = "BOX 1",
wallpaper = 1,
mons = { [1] = { species = ARON, speciesId = ARON, level = 5, hp = 10, maxHp = 10 } },
}
local loaded = Schema.fromSaveTable(legacy)
eq(loaded.party[1].speciesNumbering, Pokemon.NUMBERING_INTERNAL, "the party mon is tagged internal")
eq(Pokemon.speciesOf(loaded.party[1]), WURMPLE, "and it is still WURMPLE")
local boxed = loaded.storage.boxes[1].mons[1]
eq(boxed.speciesNumbering, Pokemon.NUMBERING_INTERNAL, "the boxed mon is tagged internal")
eq(Pokemon.speciesOf(boxed), ARON, "and it is still ARON")
local again = Schema.fromSaveTable(Schema.toSaveTable(loaded))
eq(again.party[1].speciesNumbering, Pokemon.NUMBERING_INTERNAL, "the tag survives a save round trip")
local hostExp = require("src.core.game3.summary_data").expForLevel(Pokemon.growthRate(WURMPLE), 8) - 1
local hostSave = {
schemaVersion = 1,
party = { { species = "WURMPLE", level = 7, hp = 10, maxHp = 10, exp = hostExp, personality = 0 } },
}
local hostLoaded = Schema.fromSaveTable(hostSave)
eq(hostLoaded.party[1].speciesNumbering, Pokemon.NUMBERING_INTERNAL, "a named host mon is normalized to internal numbering")
eq(hostLoaded.party[1].species, WURMPLE, "the persisted species is numeric internal WURMPLE")
eq(hostLoaded.party[1].speciesId, WURMPLE, "the species alias agrees with WURMPLE")
eq(hostLoaded.party[1].exp, hostExp, "normalization preserves earned EXP")
eq(hostLoaded.party[1].hp, 10, "normalization preserves damaged HP")
eq(Evolution.levelTarget(hostLoaded.party[1], national), SILCOON, "normalization preserves the WURMPLE evolution branch")
local SaveData = require("src.core.SaveData")
local hostRoundtrip = Schema.fromSaveTable(SaveData.decode(SaveData.encode(Schema.toSaveTable(hostLoaded))))
eq(Pokemon.speciesOf(hostRoundtrip.party[1]), WURMPLE, "serialized roundtrip preserves WURMPLE rather than national NINCADA")
eq(hostRoundtrip.party[1].exp, hostExp, "serialized roundtrip preserves earned EXP")
local rewards = require("src.core.game3.battle.experience").awardFoe({
playerParty = hostRoundtrip.party, player = { mon = hostRoundtrip.party[1], partyIndex = 1 },
wild = true, session = hostRoundtrip,
}, { species = 113, level = 10, mon = { species = 113, level = 10 } },
{ partyIndices = { 1 }, getOpts = function() return {} end })
eq(#rewards, 1, "normalized named mon remains an actual battle EXP recipient")
check(hostRoundtrip.party[1].exp > hostExp and hostRoundtrip.party[1].level >= 8,
"battle EXP crosses the normalized WURMPLE growth threshold")
print("[test] 7. a mon handed over by a script is stamped when it is built")
local Party = require("src.core.game3.party")
local session = Schema.newGame({ name = "RED" })
local ok, code, given = Party.giveMon(session, WURMPLE, 7)
check(ok, "giveMon accepted WURMPLE")
eq(code, Party.MON_GIVEN_TO_PARTY, "it went to the party")
eq(given.speciesNumbering, Pokemon.NUMBERING_INTERNAL, "and it carries the internal tag")
eq(Pokemon.speciesOf(given), WURMPLE, "so it reads back as WURMPLE")
print("[test] 8. an evolved mon carries an internal id, whatever it carried before")
local natMon = mon({ species = 265, level = 7, numbering = Pokemon.NUMBERING_NATIONAL })
natMon.hp, natMon.maxHp = 20, 20
eq(Pokemon.speciesOf(natMon), WURMPLE, "national 265 is internal WURMPLE")
Evolution.apply(natMon, SILCOON, { party = { natMon } }, nil, "levelup")
eq(natMon.speciesNumbering, Pokemon.NUMBERING_INTERNAL, "applying an evolution retags it internal")
eq(Pokemon.speciesOf(natMon), SILCOON, "so it reads as SILCOON, not national 291")
eq(Pokemon.name(Pokemon.speciesOf(natMon)), "SILCOON", "and it is named SILCOON")
local NINCADA_L20 = mon({ species = NINCADA, level = 20, numbering = Pokemon.NUMBERING_INTERNAL })
NINCADA_L20.hp, NINCADA_L20.maxHp = 25, 25
local shedParty = { NINCADA_L20 }
Evolution.apply(NINCADA_L20, NINJASK, { party = shedParty }, nil, "levelup")
eq(#shedParty, 2, "the SHEDINJA copy was added")
eq(shedParty[2].speciesNumbering, Pokemon.NUMBERING_INTERNAL, "the copy is tagged internal too")
eq(Pokemon.name(Pokemon.speciesOf(shedParty[2])), "SHEDINJA", "and it is SHEDINJA")
finish()