-- Parity test, Workstream I+M. -- I: Surf/Cut are chosen from the party menu's per-mon field-move submenu -- (start_sub_menus.asm .outOfBattleMovePointers), never from an -- overworld A-press. M: Strength gates boulder pushing on a session -- "activated" flag (push_boulder.asm TryPushingBoulder reads -- BIT_STRENGTH_ACTIVE) set only by the party-menu STRENGTH action and -- cleared on every real map load (home/overworld.asm EnterMap -> -- ResetUsingStrengthOutOfBattleBit). package.path = "./?.lua;./?/init.lua;" .. package.path if not _G.love then _G.love = require("tests.love_stub") end local Data = require("src.core.Data") if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end local S = require("tests.harness").suite("parity I_M") local check, eq = S.check, S.eq -- === harness === require("src.render.Font").load(Data) local Game = require("src.core.Game") local Input = require("src.core.Input") local StateStack = require("src.core.StateStack") local Renderer = require("src.render.Renderer") local SaveData = require("src.core.SaveData") local Pokemon = require("src.pokemon.Pokemon") local PartyMenu = require("src.ui.PartyMenu") local TextBox = require("src.render.TextBox") local OW = require("src.world.OverworldController") Game.data = Data Game.input = Input; Input:init() Game.renderer = Renderer; Renderer:init() Game.stack = StateStack; StateStack:init() Game.save = SaveData.newGame() -- Spy on the TextBox constructor so we can assert which message a field -- move surfaced (the raw text passed in, before {PLAYER}/token expansion). local realTextBoxNew = TextBox.new local captured = {} TextBox.new = function(game, text, onDone, opts) captured[#captured + 1] = text or "" return realTextBoxNew(game, text, onDone, opts) end local function clearCaptured() captured = {} end local function sawText(sub) for _, t in ipairs(captured) do if t:find(sub, 1, true) then return true end end return false end -- one input frame: btns is a list of GB button names held this frame. -- Sets both the edge (wasPressed) and held (isDown) states so TextBoxes -- type at the held-button fast speed while menus still see the edge. local function frame(btns) Input.pressed = {} for _, b in ipairs(btns or {}) do Input.pressed[b] = true; Input.state[b] = true end StateStack:update(1 / 60) for _, b in ipairs(btns or {}) do Input.state[b] = false end end local function popAll() while Game.stack:top() do Game.stack:pop() end end local function popToOW() while Game.stack:top() and Game.stack:top() ~= OW do Game.stack:pop() end end local function pushOW(mapId, x, y, facing) popAll() Game.stack:push(OW, mapId, x, y, facing) return Game.stack:top() end -- drain any TextBox(es) sitting on top of the overworld (OW has no .pages) local function drainText() local guard = 0 while guard < 400 do guard = guard + 1 local top = Game.stack:top() if not top or top.pages == nil then break end frame({ "a" }) end end local function onStack(s) for _, st in ipairs(Game.stack.states) do if st == s then return true end end return false end local function submenuActions(pm) local s = {} for _, it in ipairs(pm.subItems or {}) do s[it.action] = true end return s end -- a party mon that knows exactly the given field move(s) local function mkMon(species, ...) local m = Pokemon.new(Data, species, 20) m.moves = {} for _, id in ipairs({ ... }) do m.moves[#m.moves + 1] = { id = id, pp = 15 } end return m end -- open the field-move submenu and select the entry at row `idx` local function selectSubItem(pm, idx) Game.stack:push(pm) frame({ "a" }) -- A on the mon builds the submenu for _ = 2, idx do frame({ "down" }) end frame({ "a" }) -- A on the target row dispatches end -- =========================================================================== -- M: STRENGTH activation + boulder gate (SEAFOAM_ISLANDS_1F boulder @18,10) -- =========================================================================== Game.save.party = { mkMon("MACHOP", "STRENGTH") } Game.save.inventory = { RAINBOWBADGE = true } local ow = pushOW("SEAFOAM_ISLANDS_1F", 17, 10, "right") eq(ow.strengthActive, false, "setMap default: strengthActive is false") local boulder = ow:npcAtCell(18, 10) check(boulder ~= nil and boulder.def.sprite == "SPRITE_BOULDER", "boulder NPC present at (18,10)") check(ow:partyKnows("STRENGTH") ~= nil, "party knows STRENGTH + holds RAINBOWBADGE") -- Gap under test: knowing STRENGTH + badge is NOT enough; without an -- activation the push routine bails at the BIT_STRENGTH_ACTIVE gate. eq(ow:checkBoulderPush("right"), false, "no push before activation (bump 1)") eq(ow:checkBoulderPush("right"), false, "no push before activation (bump 2)") eq(boulder.cellX, 18, "boulder unmoved while STRENGTH is inactive") -- activate via the party menu STRENGTH action (submenu {STATS,SWITCH,STRENGTH}) clearCaptured() local pmStr = PartyMenu.new(Game) selectSubItem(pmStr, 3) eq(Game.overworld.strengthActive, true, "party-menu STRENGTH sets strengthActive") check(onStack(pmStr), "party menu stays under the STRENGTH texts (#385)") check(sawText("used") and sawText("STRENGTH"), "_UsedStrengthText shown") drainText() check(sawText("move boulders"), "_CanMoveBouldersText shown after it") check(not onStack(pmStr), "party menu closes with the blink after the texts") -- now the same two bumps push the boulder (gate passes -> arm -> move) eq(ow:checkBoulderPush("right"), false, "first bump arms the push after activation") eq(ow:checkBoulderPush("right"), true, "boulder pushes after activation") -- every real map load clears the flag (ResetUsingStrengthOutOfBattleBit) ow:setMap("SEAFOAM_ISLANDS_1F", 17, 10, "right") eq(ow.strengthActive, false, "setMap re-entry resets strengthActive") eq(ow:checkBoulderPush("right"), false, "no push after map reload until STRENGTH is reselected") -- =========================================================================== -- I: SURF from the party menu (PALLET_TOWN water @4,14, stand @4,13) -- =========================================================================== Game.save.party = { mkMon("SQUIRTLE", "SURF") } Game.save.inventory = { SOULBADGE = true } ow = pushOW("PALLET_TOWN", 4, 13, "down") ow.player.surfing = false eq(ow:useSurfFieldMove(), "ok", "useSurfFieldMove ok when facing water") ow.player.facing = "up" eq(ow:useSurfFieldMove(), "no_water", "useSurfFieldMove no_water when facing land") ow.player.facing = "down" Game.save.inventory.SOULBADGE = nil eq(ow:useSurfFieldMove(), "no_badge", "useSurfFieldMove no_badge without SOULBADGE") Game.save.inventory.SOULBADGE = true -- failure path: selecting SURF while not facing water shows -- _NoSurfingHereText and loops back (submenu stays open, no mount) ow.player.facing = "up"; ow.player.surfing = false clearCaptured() local pmSurfFail = PartyMenu.new(Game) selectSubItem(pmSurfFail, 3) check(sawText("No SURFing"), "_NoSurfingHereText when not facing water") check(pmSurfFail.submenu == true, "party menu stays open after a failed SURF") eq(ow.player.surfing, false, "no mount when SURF fails") popToOW() -- success path: facing water -> mount + _SurfingGotOnText, menu closes ow.player.facing = "down"; ow.player.surfing = false clearCaptured() local pmSurf = PartyMenu.new(Game) selectSubItem(pmSurf, 3) -- the got-on text prints over the menu (#385); dismissing it closes the -- menu and mounts, and the blink that follows carries the step check(onStack(pmSurf), "party menu stays under the got-on text") Game.stack:pop().onDone() -- a TextBox pops itself before firing onDone eq(ow.player.surfing, true, "SURF from the party menu sets player.surfing") check(not onStack(pmSurf), "party menu closes after a successful SURF") check(sawText("got on"), "_SurfingGotOnText shown on a successful SURF") -- =========================================================================== -- I: list-time badge filter, CUT/SURF/STRENGTH absent without the badge -- =========================================================================== Game.save.party = { mkMon("SQUIRTLE", "CUT", "SURF", "STRENGTH") } Game.save.inventory = {} ow = pushOW("PALLET_TOWN", 4, 13, "down") local pmNoBadge = PartyMenu.new(Game) Game.stack:push(pmNoBadge) frame({ "a" }) local actsOff = submenuActions(pmNoBadge) check(not actsOff.cut and not actsOff.surf and not actsOff.strength, "no CUT/SURF/STRENGTH submenu entries without the required badges") popToOW() Game.save.inventory = { CASCADEBADGE = true, SOULBADGE = true, RAINBOWBADGE = true } local pmBadge = PartyMenu.new(Game) Game.stack:push(pmBadge) frame({ "a" }) local actsOn = submenuActions(pmBadge) check(actsOn.cut and actsOn.surf and actsOn.strength, "CUT/SURF/STRENGTH submenu entries appear once the badges are held") -- =========================================================================== -- I: CUT from the party menu. The Cerulean tree BLOCK (50, at block 9,14) -- spans two unwalkable cells: the fence half at (18,28) (tile $50) and the -- tree itself at (19,28) (tile $3d). UsedCut (engine/overworld/cut.asm) -- checks the facing TILE, so only the tree cell cuts -- stand above it at -- (19,27) facing down, the way a vanilla player does. -- =========================================================================== Game.save.party = { mkMon("BULBASAUR", "CUT") } Game.save.inventory = { CASCADEBADGE = true } ow = pushOW("CERULEAN_CITY", 19, 27, "down") check(ow.map:blockAt(9, 14) == 50, "cut tree block (50) present before CUT") eq(ow:useCutFieldMove(), "ok", "useCutFieldMove ok when facing a cut tree") ow.player.facing = "up" eq(ow:useCutFieldMove(), "nothing", "useCutFieldMove nothing when not facing a tree") ow.player.facing = "down" Game.save.inventory.CASCADEBADGE = nil eq(ow:useCutFieldMove(), "no_badge", "useCutFieldMove no_badge without CASCADEBADGE") Game.save.inventory.CASCADEBADGE = true -- the fence half of the same tree block is NOT cuttable (tile $50 on -- OVERWORLD, not the $3d tree tile) popToOW() ow = pushOW("CERULEAN_CITY", 17, 28, "right") eq(ow:useCutFieldMove(), "nothing", "useCutFieldMove nothing when facing the tree block's fence cell") popToOW() ow = pushOW("CERULEAN_CITY", 19, 27, "down") -- success path: facing the tree -> _UsedCutText, menu closes, tree replaced clearCaptured() local pmCut = PartyMenu.new(Game) selectSubItem(pmCut, 3) check(not onStack(pmCut), "party menu closes after a successful CUT") check(sawText("CUT"), "_UsedCutText shown on a successful CUT") drainText() -- the tree swap is deferred until the message is dismissed eq(ow.map:blockAt(9, 14), 109, "CUT replaces the tree block (50 -> 109)") -- failure path: not facing a tree -> _NothingToCutText, submenu stays open popToOW() ow.player.facing = "up" clearCaptured() local pmCutFail = PartyMenu.new(Game) selectSubItem(pmCutFail, 3) check(sawText("anything to CUT"), "_NothingToCutText when not facing a tree") check(pmCutFail.submenu == true, "party menu stays open after a failed CUT") ow.player.facing = "right" -- =========================================================================== -- I: the overworld A-press shortcut for Surf/Cut is gone (interact()) -- =========================================================================== Game.save.party = { mkMon("SQUIRTLE", "SURF") } Game.save.inventory = { SOULBADGE = true } ow = pushOW("PALLET_TOWN", 4, 13, "down") ow.player.surfing = false ow:interact() eq(ow.player.surfing, false, "interact() facing water no longer starts Surf") Game.save.party = { mkMon("BULBASAUR", "CUT") } Game.save.inventory = { CASCADEBADGE = true } ow = pushOW("CERULEAN_CITY", 17, 28, "right") local cutBlk0 = ow.map:blockAt(9, 14) ow:interact() eq(cutBlk0, 50, "cut tree still present before interact()") eq(ow.map:blockAt(9, 14), 50, "interact() facing a cut tree no longer starts Cut") -- =========================================================================== -- I: IsSurfingAllowed (engine/overworld/field_move_messages.asm) -- the -- Cycling Road and Seafoam B4F current refusals -- =========================================================================== Game.save.party = { mkMon("SQUIRTLE", "SURF") } Game.save.inventory = { SOULBADGE = true } ow = pushOW("PALLET_TOWN", 4, 13, "down") ow.player.surfing = false -- BIT_ALWAYS_ON_BIKE set -> refuse with _CyclingIsFunText, submenu stays Game.save.forcedBike = true eq(ow:useSurfFieldMove(), "forced_bike", "forced bike refuses SURF (even facing water)") clearCaptured() local pmBike = PartyMenu.new(Game) selectSubItem(pmBike, 3) check(sawText("Cycling is fun!\nForget SURFing!"), "_CyclingIsFunText verbatim") check(pmBike.submenu == true, "party menu stays open (.loop) after the bike refusal") eq(ow.player.surfing, false, "no mount on the Cycling Road") Game.save.forcedBike = nil popToOW() -- the flag's lifecycle: armed by the forced-bike tiles -- (CheckForceBikeOrSurf), cleared by the Route 16/18 gate scripts and the -- fly/dungeon/blackout warps (HandleFlyWarpOrDungeonWarp) Game.save.inventory.BICYCLE = 1 Game.save.onBike = false ow = pushOW("ROUTE_16", 17, 10, "down") ow:checkForcedMovement() eq(Game.save.onBike, true, "forced-bike tile mounts the BICYCLE") eq(Game.save.forcedBike, true, "forced-bike tile arms BIT_ALWAYS_ON_BIKE") drainText() ow:setMap("ROUTE_16_GATE_1F", 4, 8, "down") eq(Game.save.forcedBike, nil, "Route 16 gate clears BIT_ALWAYS_ON_BIKE") Game.save.forcedBike = true ow:setMap("ROUTE_18_GATE_1F", 4, 8, "down") eq(Game.save.forcedBike, nil, "Route 18 gate clears BIT_ALWAYS_ON_BIKE") Game.save.forcedBike = true ow = pushOW("ROUTE_17", 4, 10, "down") ow:flyTo("PALLET_TOWN") eq(Game.save.forcedBike, nil, "Fly clears BIT_ALWAYS_ON_BIKE") ow.flyAnim, ow.flyDest, ow.player.inputLocked = nil, nil, false -- undo flyTo Game.save.forcedBike = true ow:warpToHealPoint() eq(Game.save.forcedBike, nil, "blackout/escape warps clear BIT_ALWAYS_ON_BIKE") ow.transitioning = false -- undo the queued warp transition Game.save.onBike = false Game.save.inventory.BICYCLE = nil -- Seafoam B4F: only the stairs square (dbmapcoord 7,11) refuses, and only -- until BOTH boulders are down (CheckBothEventsSet) Game.save.flags["EVENT_SEAFOAM4_BOULDER1_DOWN_HOLE"] = nil Game.save.flags["EVENT_SEAFOAM4_BOULDER2_DOWN_HOLE"] = nil ow = pushOW("SEAFOAM_ISLANDS_B4F", 7, 11, "down") ow.player.surfing = false check(ow.map:isWaterCell(7, 12), "water south of the B4F stairs square") eq(ow:useSurfFieldMove(), "current", "B4F stairs square refuses SURF pre-boulders") clearCaptured() local pmCur = PartyMenu.new(Game) selectSubItem(pmCur, 3) check(sawText("The current is\nmuch too fast!"), "_CurrentTooFastText verbatim") check(pmCur.submenu == true, "party menu stays open (.loop) after the current refusal") eq(ow.player.surfing, false, "no mount against the current") popToOW() ow.player.cellX, ow.player.cellY = 7, 10 eq(ow:useSurfFieldMove(), "no_water", "one square north the gate doesn't fire") ow.player.cellX, ow.player.cellY = 7, 11 Game.save.flags["EVENT_SEAFOAM4_BOULDER1_DOWN_HOLE"] = true eq(ow:useSurfFieldMove(), "current", "one boulder down still refuses (both required)") Game.save.flags["EVENT_SEAFOAM4_BOULDER2_DOWN_HOLE"] = true eq(ow:useSurfFieldMove(), "ok", "both boulders down: SURF allowed from the stairs") Game.save.flags["EVENT_SEAFOAM4_BOULDER1_DOWN_HOLE"] = nil Game.save.flags["EVENT_SEAFOAM4_BOULDER2_DOWN_HOLE"] = nil -- =========================================================================== -- I: SURF re-selected while surfing (ItemUseSurfboard .tryToStopSurfing) -- =========================================================================== Game.save.party = { mkMon("SQUIRTLE", "SURF") } Game.save.inventory = { SOULBADGE = true } ow = pushOW("PALLET_TOWN", 4, 14, "up") ow.player.surfing = true eq(ow:useSurfFieldMove(), "dismount", "surfing + facing land tries to get off") ow.player.facing = "down" eq(ow:useSurfFieldMove(), "no_place", "surfing + facing open water: no place") ow.player.facing = "up" table.insert(ow.entities, { cellX = 4, cellY = 13 }) eq(ow:useSurfFieldMove(), "no_place", "a sprite on the landing square blocks it (IsSpriteInFrontOfPlayer2)") table.remove(ow.entities) -- menu-driven dismount: NO text, the menu closes behind the white blink -- (.stopSurfing never prints; wActionResult stays 1 -> whiteout + -- .goBackToMap) and the simulated pad press steps the player ashore clearCaptured() local pmOff = PartyMenu.new(Game) selectSubItem(pmOff, 3) check(not onStack(pmOff), "party menu closes on dismount") eq(ow.player.surfing, false, ".stopSurfing returns to walking before the step") eq(#captured, 0, "no message on a successful dismount") local offFlash = Game.stack:top() check(offFlash ~= nil and offFlash ~= ow and offFlash.pages == nil, "the GBPalWhiteOut blink covers the menu close") for _ = 1, 60 do frame({}) end -- blink pops, the queued step walks out eq(Game.stack:top(), ow, "back on the map after the blink") eq(ow.player.cellY, 13, "the player stepped forward onto land") -- "no place to get off": the text shows, and the menu STILL closes -- (.cannotStopSurfing leaves wActionResultOrTookBattleTurn at 1) ow.player.surfing = true ow.player.cellX, ow.player.cellY = 4, 15 ow.player.px, ow.player.py = 4 * 16, 15 * 16 ow.player.facing = "down" clearCaptured() local pmNoOff = PartyMenu.new(Game) selectSubItem(pmNoOff, 3) check(sawText("There's no place\nto get off!"), "_SurfingNoPlaceToGetOffText verbatim") check(onStack(pmNoOff), "the menu stays under the message (#385)") eq(ow.player.surfing, true, "still surfing after a blocked dismount") drainText() check(not onStack(pmNoOff), "the menu closes after the message (result stays 1)") ow.player.surfing = false popToOW() -- =========================================================================== -- M: STRENGTH pages -- no prompt on _UsedStrengthText (text_asm cry + -- Delay3 auto-advance), `prompt` on _CanMoveBouldersText, then the -- GBPalWhiteOutWithDelay3 blink (start_sub_menus.asm .strength) -- =========================================================================== Game.save.party = { mkMon("MACHOP", "STRENGTH") } Game.save.inventory = { RAINBOWBADGE = true } ow = pushOW("SEAFOAM_ISLANDS_1F", 17, 10, "right") clearCaptured() local pmStr2 = PartyMenu.new(Game) selectSubItem(pmStr2, 3) local page1 = Game.stack:top() check(page1 ~= nil and page1.pages ~= nil and page1.auto ~= nil, "_UsedStrengthText box is a no-prompt (auto) page") local guard = 0 while Game.stack:top() == page1 and guard < 240 do guard = guard + 1; frame({}) end check(Game.stack:top() ~= page1, "page 1 advanced without an A press") check(sawText("can\nmove boulders."), "_CanMoveBouldersText follows") local page2 = Game.stack:top() check(page2 ~= nil and page2.pages ~= nil and page2.auto == nil, "_CanMoveBouldersText is a normal `prompt` page") for _ = 1, 150 do frame({}) end -- types out, then waits check(Game.stack:top() == page2, "page 2 waits for A/B") frame({ "a" }) local strFlash = Game.stack:top() check(strFlash ~= page2 and strFlash ~= ow and strFlash.pages == nil, "the white blink follows the A press (GBPalWhiteOutWithDelay3)") for _ = 1, 10 do frame({}) end eq(Game.stack:top(), ow, "back on the map after the blink") -- =========================================================================== -- #107: fainted Pokémon can still use field moves (party submenu + partyKnows) -- =========================================================================== local fainted = mkMon("PIDGEOTTO", "FLY", "CUT", "STRENGTH", "SURF") fainted.hp = 0 Game.save.party = { fainted } Game.save.inventory = { THUNDERBADGE = true, CASCADEBADGE = true, RAINBOWBADGE = true, SOULBADGE = true, } ow = pushOW("PALLET_TOWN", 4, 13, "down") check(ow:partyKnows("SURF") == fainted, "partyKnows finds SURF on a fainted mon") check(ow:partyKnows("CUT") == fainted, "partyKnows finds CUT on a fainted mon") local pmFaint = PartyMenu.new(Game) Game.stack:push(pmFaint) frame({ "a" }) local actsFaint = submenuActions(pmFaint) -- FLY needs OVERWORLD tileset (Pallet); CUT/SURF/STRENGTH need badges only check(actsFaint.fly and actsFaint.cut and actsFaint.surf and actsFaint.strength, "fainted mon still lists FLY/CUT/SURF/STRENGTH in the party submenu") popToOW() -- =========================================================================== -- #83: FLY/TELEPORT use CheckIfInOutsideMap (OVERWORLD + PLATEAU), so the -- outdoor strip between Victory Road and the Elite Four / Indigo Plateau -- building allows Fly like any other outdoor overworld map. -- =========================================================================== Game.save.party = { mkMon("AERODACTYL", "FLY", "TELEPORT") } Game.save.inventory = { THUNDERBADGE = true } ow = pushOW("INDIGO_PLATEAU", 10, 5, "down") eq(ow.map.def.tileset, "PLATEAU", "Indigo Plateau outdoor uses PLATEAU tileset") local pmIndigo = PartyMenu.new(Game) Game.stack:push(pmIndigo) frame({ "a" }) local actsIndigo = submenuActions(pmIndigo) check(actsIndigo.fly, "FLY listed on Indigo Plateau outdoor (PLATEAU)") check(actsIndigo.escape, "TELEPORT listed on Indigo Plateau outdoor (PLATEAU)") popToOW() ow = pushOW("ROUTE_23", 10, 6, "down") eq(ow.map.def.tileset, "PLATEAU", "Route 23 uses PLATEAU tileset") local pmR23 = PartyMenu.new(Game) Game.stack:push(pmR23) frame({ "a" }) check(submenuActions(pmR23).fly, "FLY listed on Route 23 (PLATEAU)") popToOW() -- Indoors at the lobby still blocks FLY/TELEPORT (MART tileset) ow = pushOW("INDIGO_PLATEAU_LOBBY", 7, 8, "down") check(ow.map.def.tileset ~= "OVERWORLD" and ow.map.def.tileset ~= "PLATEAU", "Indigo lobby is not an outside tileset") local pmLobby = PartyMenu.new(Game) Game.stack:push(pmLobby) frame({ "a" }) local actsLobby = submenuActions(pmLobby) check(not actsLobby.fly, "FLY omitted inside Indigo Plateau lobby") check(not actsLobby.escape, "TELEPORT omitted inside Indigo Plateau lobby") popToOW() -- restore fainted field-move mon for the STRENGTH/SURF cases below Game.save.party = { fainted } Game.save.inventory = { THUNDERBADGE = true, CASCADEBADGE = true, RAINBOWBADGE = true, SOULBADGE = true, } -- STRENGTH activation from a fainted user (name text + strengthActive) ow = pushOW("SEAFOAM_ISLANDS_1F", 17, 10, "right") clearCaptured() local pmFaintStr = PartyMenu.new(Game) -- Seafoam is not OVERWORLD, so FLY is omitted: STATS, SWITCH, CUT, STRENGTH, SURF selectSubItem(pmFaintStr, 4) eq(Game.overworld.strengthActive, true, "fainted mon can activate STRENGTH from the party menu") check(sawText("used") and sawText("STRENGTH"), "fainted STRENGTH still prints _UsedStrengthText") drainText() -- SURF mount from a fainted user when facing water Game.save.party = { fainted } Game.save.inventory.SOULBADGE = true ow = pushOW("PALLET_TOWN", 4, 13, "down") ow.player.surfing = false eq(ow:useSurfFieldMove(), "ok", "useSurfFieldMove ok with only a fainted SURF mon") clearCaptured() -- submenu order: STATS, SWITCH, FLY, CUT, STRENGTH, SURF (move order on mon) local pmFaintSurf = PartyMenu.new(Game) selectSubItem(pmFaintSurf, 6) Game.stack:pop().onDone() -- dismiss the text: menu closes, mount (#320, #385) eq(ow.player.surfing, true, "fainted mon can SURF from the party menu") check(not onStack(pmFaintSurf), "party menu closes after fainted SURF") -- restore the spied constructor so later dofile'd suites are unaffected TextBox.new = realTextBoxNew popAll() S.finish()