#!/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()