mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 16:31:05 +02:00
516 lines
23 KiB
Lua
516 lines
23 KiB
Lua
-- 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 {STRENGTH,STATS,SWITCH})
|
|
clearCaptured()
|
|
local pmStr = PartyMenu.new(Game)
|
|
selectSubItem(pmStr, 1)
|
|
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, 1)
|
|
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, 1)
|
|
-- 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, 1)
|
|
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, 1)
|
|
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, 1)
|
|
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, 1)
|
|
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, 1)
|
|
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, 1)
|
|
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, 1)
|
|
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: CUT, STRENGTH, SURF, STATS, SWITCH
|
|
selectSubItem(pmFaintStr, 2)
|
|
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: FLY, CUT, STRENGTH, SURF (move order on mon), then STATS, SWITCH
|
|
local pmFaintSurf = PartyMenu.new(Game)
|
|
selectSubItem(pmFaintSurf, 4)
|
|
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()
|