Files
gen1recomp/tests/gen2_swarm_test.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

198 lines
8.1 KiB
Lua

-- Pokemon swarms, WIRED.
--
-- luajit tests/gen2_swarm_test.lua
--
-- src/core/gen2/Roamers.lua has carried the whole swarm model for a while --
-- StoreSwarmMapIndices / SetSwarmFlag / CheckSwarmFlag / ActivateFishingSwarm
-- and the _SwarmWildmonCheck lookup -- with three dead links: the daily clear
-- had no call site, the encounter roll never consulted the tables, and the
-- cache carried no tables to consult. This suite is those three.
--
-- CheckSwarmFlag second row of CheckTimeEvents' `.do_daily`
-- (engine/overworld/events.asm)
-- _SwarmWildmonCheck the first thing LoadWildMonDataPointer does, for
-- the grass list and the water one alike
-- SwarmGrassWildMons / data/wild/swarm_grass.asm, swarm_water.asm,
-- SwarmWaterWildMons emitted by RomExtractorGen2 as
-- encounters.swarmGrass / .swarmWater
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 swarm")
local check, eq = S.check, S.eq
local World = require("src.world.gen2.World")
local Roamers = require("src.core.gen2.Roamers")
local Swarm = Roamers.Swarm
local COLL_TALL_GRASS = 0x18
local DATA = {
items = {},
moves = { TACKLE = { name = "TACKLE", pp = 35 } },
pokemon = {
NIDORAN_M = { name = "NIDORAN", index = 32, types = { "POISON", "POISON" },
baseStats = { hp = 46, attack = 57, defense = 40, speed = 50,
specialAttack = 40, specialDefense = 40 },
growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } },
YANMA = { name = "YANMA", index = 193, types = { "BUG", "FLYING" },
baseStats = { hp = 65, attack = 65, defense = 45, speed = 95,
specialAttack = 75, specialDefense = 45 },
growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } },
},
}
local function slots(species, level)
local list = {}
for i = 1, 7 do list[i] = { species = species, level = level } end
return list
end
-- ROUTE_35's own list against the Yanma swarm's, both always-hit so the only
-- question a step asks is WHICH table was searched.
local ENCOUNTERS = {
grass = {
ROUTE_35 = { map = "ROUTE_35", rates = { MORN = 256, DAY = 256, NITE = 256 },
slots = { MORN = slots("NIDORAN_M", 12), DAY = slots("NIDORAN_M", 12),
NITE = slots("NIDORAN_M", 12) } },
},
swarmGrass = {
ROUTE_35 = { map = "ROUTE_35", rates = { MORN = 256, DAY = 256, NITE = 256 },
slots = { MORN = slots("YANMA", 14), DAY = slots("YANMA", 14),
NITE = slots("YANMA", 14) } },
},
}
local function swarmWorld()
local game = {
data = DATA,
save = { player = { name = "GOLD", badges = {} },
party = { { species = "YANMA", level = 5, hp = 20, maxHp = 20 } },
inventory = {} },
}
local world = World.new(game)
game.world = world
world.maps = { ROUTE_35 = { id = "ROUTE_35", group = 2, map = 5,
width = 2, height = 2, blocks = { 1, 2, 3, 4 }, objects = {}, warps = {},
environment = "ROUTE" },
ROUTE_36 = { id = "ROUTE_36", group = 2, map = 6, width = 2, height = 2,
blocks = { 1, 2, 3, 4 }, objects = {}, warps = {}, environment = "ROUTE" } }
world.map = { id = "ROUTE_35", def = world.maps.ROUTE_35,
width = 2, height = 2,
cellCollision = function() return COLL_TALL_GRASS end }
world.encounters = ENCOUNTERS
world.player = { cellX = 0, cellY = 0 }
world.daytime = "DAY"
world.startBattle = function(self, opts) self.started = opts return true end
return world, game
end
-- ---- the lookup arm --------------------------------------------------------
do
local world, game = swarmWorld()
check(world:tryWildEncounter(), "an ordinary step on Route 35 fights")
eq(world.started.wild.species, "NIDORAN_M", "the map's own list")
-- Script_swarm -> StoreSwarmMapIndices, which is World:setSwarm.
world:setSwarm(2, 5)
eq(game.save.swarmMap, "ROUTE_35", "the swarm command stores the map pair")
check(game.save.dailyFlags.swarm, "and falls through into SetSwarmFlag")
world.started = nil
check(world:tryWildEncounter(), "the next step still fights")
eq(world.started.wild.species, "YANMA",
"but out of the swarm table, which _SwarmWildmonCheck searches first")
eq(world.started.wild.level, 14, "at the swarm row's own level")
-- The check is `cp d / cp e` against the CURRENT map: a swarm on Route 35
-- does nothing to Route 36.
world.map = { id = "ROUTE_36", def = world.maps.ROUTE_36,
width = 2, height = 2,
cellCollision = function() return COLL_TALL_GRASS end }
world.started = nil
check(not world:tryWildEncounter(),
"a map the swarm is not on falls through to its own (absent) table")
check(world.started == nil, "so nothing is fought there")
end
-- SWEET SCENT reaches the same ChooseWildEncounter, so it sees the swarm too.
do
local world = swarmWorld()
world:setSwarm(2, 5)
check(world:sweetScentEncounter(), "SWEET SCENT turns something up")
eq(world.started.wild.species, "YANMA", "and it is the swarm's mon")
end
-- ---- the daily clear -------------------------------------------------------
--
-- CheckTimeEvents `.do_daily` is CheckDailyResetTimer, CheckSwarmFlag,
-- CheckPokerusTick, CheckPhoneCall in that order. The reset takes
-- DAILYFLAGS1_SWARM down; CheckSwarmFlag is what notices and clears the map
-- pair with it, and it is the ONLY thing that ever ends a swarm.
do
local world, game = swarmWorld()
world:setSwarm(2, 5)
-- A day already banked, so the first poll is not the one that starts the
-- timer (Apricorns.checkDailyResetTimer arms it on a save that has none).
world:checkTimeEvents()
check(game.save.swarmMap == "ROUTE_35",
"a poll on the same day leaves the swarm up")
check(Swarm.active(game.save), "the flag is still set")
-- Roll the clock over the way the daily reset timer measures it.
game.save.dailyReset.day = (game.save.dailyReset.day or 0) - 2
world:checkTimeEvents()
check(not Swarm.active(game.save), "the daily reset drops the flag")
check(game.save.swarmMap == nil, "and CheckSwarmFlag clears the map pair")
world.started = nil
check(world:tryWildEncounter(), "the step after that still fights")
eq(world.started.wild.species, "NIDORAN_M", "out of the map's own list again")
end
-- ActivateFishingSwarm rides the same flag and does NOT touch the map pair,
-- so the clear has to take both down together.
do
local world, game = swarmWorld()
world:setSwarm(2, 5)
Swarm.setFishing(game.save, Swarm.FISH_QWILFISH)
eq(Swarm.fishing(game.save), Swarm.FISH_QWILFISH, "the fishing swarm is up")
world:checkTimeEvents() -- arms the timer, the way a save's first step does
game.save.dailyReset.day = (game.save.dailyReset.day or 0) - 2
world:checkTimeEvents()
eq(Swarm.fishing(game.save), Swarm.FISH_NONE,
"and goes down on the same daily tick")
end
-- ---- the extracted tables --------------------------------------------------
--
-- data/wild/swarm_grass.asm has four rows and swarm_water.asm one; the Yanma
-- swarm on Route 35 is the one the Dunsparce/Yanma phone calls arm.
do
local cache = os.getenv("GOLD_CACHE")
local encounters
if cache then
local chunk = loadfile(cache .. "/data/generated/encounters.lua")
encounters = chunk and chunk()
end
if not (encounters and encounters.swarmGrass) then
check(true, "no gold cache: swarm table shape (SKIP)")
else
local grass = encounters.swarmGrass
check(grass.ROUTE_35 ~= nil, "SwarmGrassWildMons carries ROUTE_35")
check(grass.ROUTE_38 ~= nil, "and ROUTE_38")
check(grass.DARK_CAVE_VIOLET_ENTRANCE ~= nil,
"and DARK_CAVE_VIOLET_ENTRANCE, which is the Dunsparce swarm")
check(grass.MOUNT_MORTAR_1F_OUTSIDE ~= nil, "and MOUNT_MORTAR_1F_OUTSIDE")
eq(grass.ROUTE_35.rates.DAY, 25, "Route 35's swarm rate is 10 percent")
eq(grass.ROUTE_35.slots.DAY[3].species, "YANMA",
"and slot 3 of its day list is the YANMA the swarm exists for")
local water = encounters.swarmWater or {}
check(water.MOUNT_MORTAR_1F_OUTSIDE ~= nil,
"SwarmWaterWildMons carries the one Marill row")
eq(water.MOUNT_MORTAR_1F_OUTSIDE.slots[2].species, "MARILL",
"with MARILL in its middle slot")
end
end
S.finish()