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

227 lines
9.3 KiB
Lua

-- The fishing half of a swarm (engine/events/fish.asm).
--
-- luajit tests/gen2_fishing_swarm_test.lua (ROM-free; the cache facts SKIP
-- without a gold cache)
--
-- Fish calls GetFishGroupIndex before it indexes FishGroups, and that routine
-- is the ONLY reader of wFishingSwarmFlag: a map whose MAP_FISHGROUP is
-- FISHGROUP_QWILFISH rolls FISHGROUP_QWILFISH_SWARM instead while the flag says
-- FISHSWARM_QWILFISH, and the same for Remoraid. Nothing about the map header
-- changes, which is why the phone call that starts a swarm is one setval and
-- one special.
--
-- The port wired swarms into the grass and water tables (_SwarmWildmonCheck,
-- src/core/gen2/Roamers.lua Swarm.tables) and left fishing reading the header
-- alone, so `special ActivateFishingSwarm` had no reader at all: the Route 32
-- Qwilfish swarm and the Route 44 Remoraid swarm never reached a rod.
--
-- Everything below goes through World:rollFishing, which is what World:useRod
-- and World:tryFishing both call, rather than through Encounter directly.
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 fishing swarm")
local check, eq = S.check, S.eq
love = require("tests.love_stub")
local World = require("src.world.gen2.World")
local Encounter = require("src.battle.gen2.Encounter")
local Roamers = require("src.core.gen2.Roamers")
local Permissions = require("src.world.gen2.Permissions")
local COLL_FLOOR, COLL_WATER = 0x00, 0x29
local MAP_W, MAP_H = 10, 10
-- constants/script_constants.asm.
eq(Encounter.FISHSWARM_NONE, 0, "FISHSWARM_NONE is 0")
eq(Encounter.FISHSWARM_QWILFISH, 1, "FISHSWARM_QWILFISH is 1")
eq(Encounter.FISHSWARM_REMORAID, 2, "FISHSWARM_REMORAID is 2")
eq(Roamers.Swarm.FISH_QWILFISH, Encounter.FISHSWARM_QWILFISH,
"and the save-side names agree with them")
eq(Roamers.Swarm.FISH_REMORAID, Encounter.FISHSWARM_REMORAID, "for both")
-- One always-hit row per list, so the ROLL is never what these assert: whatever
-- comes back names the group it was rolled from.
local function only(species)
local list = { { chance = 256, species = species, level = 10 } }
return { old = list, good = list, super = list }
end
local ENCOUNTERS = {
fishGroups = {
FISHGROUP_POND = only("MAGIKARP"),
FISHGROUP_QWILFISH = only("MAGIKARP"),
FISHGROUP_QWILFISH_SWARM = only("QWILFISH"),
FISHGROUP_QWILFISH_NO_SWARM = only("MAGIKARP"),
FISHGROUP_REMORAID = only("GOLDEEN"),
FISHGROUP_REMORAID_SWARM = only("REMORAID"),
},
}
local function mon(name)
return {
name = name, types = { "WATER", "WATER" },
baseStats = { hp = 20, attack = 10, defense = 55, speed = 80,
specialAttack = 15, specialDefense = 20 },
levelMoves = { { level = 1, move = "SPLASH" } },
}
end
local DATA = {
items = {},
moves = { SPLASH = { name = "SPLASH", pp = 40 } },
pokemon = {
MAGIKARP = mon("MAGIKARP"), QWILFISH = mon("QWILFISH"),
GOLDEEN = mon("GOLDEEN"), REMORAID = mon("REMORAID"),
},
}
-- A world with one water tile at (5,4) and the player facing it from (5,5),
-- which is the only geometry .TryFish reads.
local function fishWorld(mapId, fishGroup)
local cells = { [4 * 100 + 5] = COLL_WATER }
local map
map = {
id = mapId,
width = MAP_W, height = MAP_H,
def = { bgEvents = {}, objects = {}, width = MAP_W, height = MAP_H,
environment = "ROUTE", fishGroup = fishGroup },
cellCollision = function(_, x, y) return cells[y * 100 + x] or COLL_FLOOR end,
inBounds = function(_, x, y)
return x >= 0 and y >= 0 and x < MAP_W * 2 and y < MAP_H * 2
end,
isWalkable = function(_, x, y)
return Permissions.isWalkable(map:cellCollision(x, y))
end,
warpAt = function() return nil end,
}
local game = {
data = DATA,
save = { player = { name = "GOLD" }, party = {}, inventory = {} },
}
local world = World.new(game)
game.world = world
world.map = map
world.maps = { [mapId] = map.def }
world.encounters = ENCOUNTERS
world.player = {
cellX = 5, cellY = 5, px = 80, py = 80, facing = "up", moving = false,
turnArmed = true, update = function() return false end,
setSprite = function() end,
}
world.pollTimeOfDay = function() end
world.vm = { running = function() return false end, update = function() end }
return world, game
end
local function hooked(world, rod)
local outcome, wild = world:rollFishing(rod or "OLD_ROD")
return outcome, wild and wild.species
end
-- ---- ROUTE_32, the Qwilfish swarm -----------------------------------------
do
local world, game = fishWorld("ROUTE_32", "FISHGROUP_QWILFISH")
local outcome, species = hooked(world)
eq(outcome, "battle", "the rod hooks something off the map's own group")
eq(species, "MAGIKARP", "which is FISHGROUP_QWILFISH with no swarm running")
-- `special ActivateFishingSwarm` with wScriptVar = FISHSWARM_QWILFISH: the
-- save write the port already had, and the ONLY thing that changes here.
Roamers.Swarm.setFishing(game.save, Roamers.Swarm.FISH_QWILFISH)
outcome, species = hooked(world)
eq(outcome, "battle", "the rod still hooks something during the swarm")
eq(species, "QWILFISH", "and it is FISHGROUP_QWILFISH_SWARM's list")
for _, rod in ipairs({ "OLD_ROD", "GOOD_ROD", "SUPER_ROD" }) do
local _, got = hooked(world, rod)
eq(got, "QWILFISH", "the swap is per GROUP, so it covers the " .. rod)
end
-- The Remoraid swarm is a different flag value and must not touch this map.
Roamers.Swarm.setFishing(game.save, Roamers.Swarm.FISH_REMORAID)
local _, other = hooked(world)
eq(other, "MAGIKARP", "a Remoraid swarm leaves Route 32 alone")
-- CheckSwarmFlag is what ends it: the daily reset drops DAILYFLAGS1_SWARM and
-- the next poll clears wFishingSwarmFlag with it.
Roamers.Swarm.setFishing(game.save, Roamers.Swarm.FISH_QWILFISH)
game.save.dailyFlags.swarm = false
eq(Roamers.Swarm.check(game.save), 1, "CheckSwarmFlag answers 1 once it is down")
local _, ended = hooked(world)
eq(ended, "MAGIKARP", "and the rod is back on the map's own group")
end
-- ---- ROUTE_44, the Remoraid swarm -----------------------------------------
do
local world, game = fishWorld("ROUTE_44", "FISHGROUP_REMORAID")
local _, species = hooked(world)
eq(species, "GOLDEEN", "Route 44 fishes FISHGROUP_REMORAID by default")
Roamers.Swarm.setFishing(game.save, Roamers.Swarm.FISH_REMORAID)
local _, swarmed = hooked(world)
eq(swarmed, "REMORAID", "and FISHGROUP_REMORAID_SWARM while the swarm runs")
Roamers.Swarm.setFishing(game.save, Roamers.Swarm.FISH_QWILFISH)
local _, wrong = hooked(world)
eq(wrong, "GOLDEEN", "a Qwilfish swarm leaves Route 44 alone")
end
-- ---- the groups the substitution must NOT touch ---------------------------
do
-- FISHGROUP_QWILFISH_NO_SWARM is a map header value of its own, not a state
-- of FISHGROUP_QWILFISH: GetFishGroupIndex compares against FISHGROUP_QWILFISH
-- and FISHGROUP_REMORAID and nothing else.
local world, game = fishWorld("ROUTE_33", "FISHGROUP_QWILFISH_NO_SWARM")
Roamers.Swarm.setFishing(game.save, Roamers.Swarm.FISH_QWILFISH)
local _, species = hooked(world)
eq(species, "MAGIKARP",
"FISHGROUP_QWILFISH_NO_SWARM never becomes a swarm group")
local pond, pondGame = fishWorld("ROUTE_34", "FISHGROUP_POND")
Roamers.Swarm.setFishing(pondGame.save, Roamers.Swarm.FISH_QWILFISH)
local _, caught = hooked(pond)
eq(caught, "MAGIKARP", "and an ordinary pond is untouched by either swarm")
end
-- ---- a cache with no swarm rows -------------------------------------------
do
local world, game = fishWorld("ROUTE_32", "FISHGROUP_QWILFISH")
world.encounters = { fishGroups = { FISHGROUP_QWILFISH = only("MAGIKARP") } }
Roamers.Swarm.setFishing(game.save, Roamers.Swarm.FISH_QWILFISH)
local outcome, species = hooked(world)
eq(outcome, "battle", "an older cache still bites")
eq(species, "MAGIKARP", "off the map's own group rather than off nothing")
end
-- ---- the group names in the real cache ------------------------------------
do
local cache = os.getenv("GOLD_CACHE")
if not cache then
local home = os.getenv("HOME") or ""
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
end
local encChunk = loadfile(cache .. "/data/generated/encounters.lua")
local mapChunk = loadfile(cache .. "/data/generated/maps.lua")
if not (encChunk and mapChunk) then
check(true, "no gold cache: fish group names (SKIP)")
else
local groups = (encChunk() or {}).fishGroups or {}
local maps = mapChunk()
for _, name in ipairs({ "FISHGROUP_QWILFISH", "FISHGROUP_QWILFISH_SWARM",
"FISHGROUP_REMORAID", "FISHGROUP_REMORAID_SWARM" }) do
check(groups[name] ~= nil, "the cache carries " .. name)
end
eq((maps.ROUTE_32 or {}).fishGroup, "FISHGROUP_QWILFISH",
"ROUTE_32's header names the Qwilfish group")
eq((maps.ROUTE_44 or {}).fishGroup, "FISHGROUP_REMORAID",
"and ROUTE_44's the Remoraid one")
-- The substitution is resolved against the cache's own group table.
eq(Encounter.fishGroupFor({ fishGroups = groups }, "FISHGROUP_QWILFISH",
Encounter.FISHSWARM_QWILFISH), "FISHGROUP_QWILFISH_SWARM",
"so the real Route 32 header swaps")
eq(Encounter.fishGroupFor({ fishGroups = groups }, "FISHGROUP_REMORAID",
Encounter.FISHSWARM_REMORAID), "FISHGROUP_REMORAID_SWARM",
"and so does the real Route 44 one")
end
end
S.finish()