-- CheckRepelEffect, the last gate in TryWildEncounter. -- -- luajit tests/gen2_repel_test.lua -- -- The port had the whole REPEL step chain -- World:useRepel arming -- save.repelSteps, StepEvents.repelStep ticking it down and printing the -- wear-off -- and nothing in the encounter path ever READ it, so a Max Repel -- was consumed, messaged and counted while changing nothing at all. -- -- CheckRepelEffect (engine/overworld/wildmons.asm) runs AFTER -- ChooseWildEncounter has already picked the mon, which is the fact that -- decides every case below: the filter is on the LEVEL that was rolled, and -- the mon it is measured against is the first party member that is not -- fainted. `ld a, [wCurPartyLevel] / cp [hl] / jr nc, .encounter` takes the -- encounter on greater-or-equal, so a lead standing at the wilds' own level -- repels nothing. -- -- CheckEncounterRoamMon writes wCurPartyLevel too, so a beast goes through the -- same filter; SweetScentEncounter never reaches the routine at all. package.path = "./?.lua;./?/init.lua;" .. package.path local S = require("tests.harness").suite("gen2 repel") local check, eq = S.check, S.eq local World = require("src.world.gen2.World") local Roamers = require("src.core.gen2.Roamers") local COLL_TALL_GRASS = 0x18 local DATA = { items = { REPEL = { id = "REPEL", name = "REPEL", pocket = "ITEM", index = 0x14 }, MAX_REPEL = { id = "MAX_REPEL", name = "MAX REPEL", pocket = "ITEM", index = 0x2b }, }, moves = { TACKLE = { name = "TACKLE", pp = 35 } }, pokemon = { HOOTHOOT = { name = "HOOTHOOT", index = 163, types = { "NORMAL", "FLYING" }, baseStats = { hp = 60, attack = 30, defense = 30, speed = 50, specialAttack = 36, specialDefense = 56 }, growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } }, RAIKOU = { name = "RAIKOU", index = 243, types = { "ELECTRIC", "ELECTRIC" }, baseStats = { hp = 90, attack = 85, defense = 75, speed = 115, specialAttack = 115, specialDefense = 100 }, growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } }, QUAGSIRE = { name = "QUAGSIRE", index = 195, types = { "WATER", "GROUND" }, baseStats = { hp = 95, attack = 85, defense = 85, speed = 35, specialAttack = 65, specialDefense = 65 }, growthRate = 0, levelMoves = { { level = 1, move = "TACKLE" } } }, }, } -- An always-passing rate and all seven slots filled with the same row, so -- nothing below is about a roll: every step that is not repelled starts a -- battle with a level 15 HOOTHOOT. local function sevenSlots() local list = {} for i = 1, 7 do list[i] = { species = "HOOTHOOT", level = 15 } end return list end local ENCOUNTERS = { grass = { ROUTE_42 = { map = "ROUTE_42", rates = { MORN = 256, DAY = 256, NITE = 256 }, slots = { MORN = sevenSlots(), DAY = sevenSlots(), NITE = sevenSlots() }, }, }, } local function repelWorld(party) local game = { data = DATA, save = { player = { name = "GOLD", badges = {} }, party = party or {}, inventory = {} }, } local world = World.new(game) game.world = world world.maps = { ROUTE_42 = { id = "ROUTE_42", group = 3, map = 1, width = 2, height = 2, blocks = { 1, 2, 3, 4 }, objects = {}, warps = {} } } world.map = { id = "ROUTE_42", def = world.maps.ROUTE_42, width = 2, height = 2, cellCollision = function() return COLL_TALL_GRASS end, } world.map.def.environment = "ROUTE" world.encounters = ENCOUNTERS world.player = { cellX = 0, cellY = 0 } world.daytime = "DAY" world.started = nil world.startBattle = function(self, opts) self.started = opts return true end return world, game end local function mon(species, level) return { species = species, level = level, hp = 20, maxHp = 20 } end -- ---- the plain grass filter ------------------------------------------------ do local world, game = repelWorld({ mon("QUAGSIRE", 40) }) check(world:tryWildEncounter(), "with no REPEL the level 15 wild fires") eq(world.started.wild.species, "HOOTHOOT", "and it is the map's own slot") world.started = nil game.save.pokedex = { seen = {}, caught = {} } game.save.repelSteps = 250 check(not world:tryWildEncounter(), "a ticking REPEL drops a wild BELOW the lead's level") check(world.started == nil, "so no battle starts") eq(game.save.repelSteps, 250, "and the filter never touches the counter -- DoRepelStep owns it") check(not game.save.pokedex.seen.HOOTHOOT, "a repelled encounter is not even SEEN") end -- `jr nc, .encounter`: equal levels still fight. This is the difference -- between "repels weaker mons" and "repels everything", and it is one branch. do local world, game = repelWorld({ mon("QUAGSIRE", 15) }) game.save.repelSteps = 250 check(world:tryWildEncounter(), "a wild at exactly the lead's level is NOT repelled") eq(world.started.wild.level, 15, "and fights at its own level") end do local world, game = repelWorld({ mon("QUAGSIRE", 16) }) game.save.repelSteps = 100 check(not world:tryWildEncounter(), "one level under the lead is repelled") end -- The walk starts at wPartyMon1HP and skips every slot whose HP is zero, so -- the mon a repel measures against is the first LIVE one -- and an egg (HP -- zeroed by DayCare_GiveEgg) is never it. do local world, game = repelWorld({ { species = "EGG", isEgg = true, hp = 0, maxHp = 0, level = 40 }, { species = "QUAGSIRE", level = 5, hp = 0, maxHp = 20 }, mon("QUAGSIRE", 40), }) game.save.repelSteps = 250 check(not world:tryWildEncounter(), "the egg and the fainted mon are skipped; the level 40 lead repels") world.started = nil game.save.party = { { species = "QUAGSIRE", level = 5, hp = 12, maxHp = 20 }, mon("QUAGSIRE", 40) } check(world:tryWildEncounter(), "a live level 5 in front of it lets the level 15 wild through") end -- No repel armed at all is the routine's own first line: `and a / jr z`. do local world, game = repelWorld({ mon("QUAGSIRE", 40) }) game.save.repelSteps = 0 check(world:tryWildEncounter(), "a spent REPEL suppresses nothing") end -- ---- the beast ------------------------------------------------------------ -- -- CheckEncounterRoamMon stages wTempWildMonSpecies AND wCurPartyLevel before -- returning carry, so the level a repel measures is the beast's 40. A level -- 41 lead really does repel Raikou on the cart. do local world, game = repelWorld({ mon("QUAGSIRE", 41) }) Roamers.init(game.save, { force = true }) game.save.roamers[1].map = "ROUTE_42" world.roamerRandom = function() return 1 end -- slot 1 past both gates game.save.repelSteps = 250 check(not world:tryWildEncounter(), "a level 41 lead repels the beast itself") check(world.started == nil, "no roaming battle starts") check(game.save.roamers[1].hp == 0, "and .InitRoamHP never runs, so the beast keeps its unrolled struct") game.save.party = { mon("QUAGSIRE", 40) } check(world:tryWildEncounter(), "a level 40 lead does not") eq(world.started.roaming, 1, "and it is the roaming battle") end -- ---- SWEET SCENT ---------------------------------------------------------- -- -- SweetScentEncounter (engine/events/sweet_scent.asm) is CanEncounterWildMon, -- GetMapEncounterRate and ChooseWildEncounter -- and then straight to -- wScriptVar. CheckRepelEffect is not in it, so the move works through a -- Max Repel exactly as the cart lets it. do local world, game = repelWorld({ mon("QUAGSIRE", 40) }) game.save.repelSteps = 250 check(world:sweetScentEncounter(), "SWEET SCENT ignores a ticking REPEL") eq(world.started.wild.species, "HOOTHOOT", "and turns up the map's own slot") end S.finish()