-- 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()