mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Merge pull request #815 from johnjohto/fix-fly-menu-788-795
This commit is contained in:
+4
-5
@@ -12,12 +12,11 @@ function FlyMenu.new(game)
|
||||
local seen = {}
|
||||
for _, mapId in ipairs(game.data.field.flyOrder or {}) do
|
||||
-- towns only (dungeon escape spots share the table), each listed once.
|
||||
-- Indigo Plateau (tileset PLATEAU) is a valid Fly destination too, so allow
|
||||
-- it past the OVERWORLD-only isOutdoor gate while the CAVERN/FACILITY escape
|
||||
-- spots stay excluded (LoadTownMap_Fly cycles it like any town, #203).
|
||||
-- Map.isFlyTown is the BuildFlyLocationsList gate: map ids 0..10, so
|
||||
-- INDIGO_PLATEAU cycles like any town (#203) while the ROUTE_4/ROUTE_10
|
||||
-- Pokemon Centers, fly warps but not towns, stay out (#788).
|
||||
local def = game.data.maps[mapId]
|
||||
if visited[mapId] and def and not seen[mapId]
|
||||
and (Map.isOutdoor(def) or def.tileset == "PLATEAU") then
|
||||
if visited[mapId] and def and not seen[mapId] and Map.isFlyTown(def) then
|
||||
seen[mapId] = true
|
||||
table.insert(items, {
|
||||
value = mapId,
|
||||
|
||||
+19
-10
@@ -147,13 +147,14 @@ local function buildFlyList(game, byMap)
|
||||
for _, mapId in ipairs(field.flyOrder or {}) do
|
||||
local def = game.data.maps and game.data.maps[mapId]
|
||||
-- INDIGO_PLATEAU is a normal Fly spot (engine/menus/town_map.asm
|
||||
-- LoadTownMap_Fly cycles it like any town), but its map uses tileset
|
||||
-- "PLATEAU" not OVERWORLD, so Map.isOutdoor() alone dropped it from the
|
||||
-- cursor even though it is visited and has a fly warp. Allow PLATEAU here
|
||||
-- while the CAVERN/FACILITY dungeon escape spots that share flyOrder still
|
||||
-- fail the gate and stay out (#203).
|
||||
-- LoadTownMap_Fly cycles it like any town): its map id sits inside
|
||||
-- BuildFlyLocationsList's 0..NUM_CITY_MAPS-1 walk, which is what
|
||||
-- Map.isFlyTown checks, so it passes even though its tileset is
|
||||
-- "PLATEAU" not OVERWORLD (#203). The ROUTE_4/ROUTE_10 Pokemon Centers
|
||||
-- carry fly warps but are not towns, so they stay out (#788), as do the
|
||||
-- CAVERN/FACILITY dungeon escape spots that share flyOrder.
|
||||
if not seen[mapId] and visited[mapId] and flyWarps[mapId]
|
||||
and def and (Map.isOutdoor(def) or def.tileset == "PLATEAU") then
|
||||
and def and Map.isFlyTown(def) then
|
||||
seen[mapId] = true
|
||||
local loc = byMap[mapId] or { name = mapId:gsub("_", " ") }
|
||||
table.insert(flyLocs, loc)
|
||||
@@ -220,8 +221,13 @@ function TownMap.new(game, opts)
|
||||
local mapId = game.overworld and game.overworld.map and game.overworld.map.id
|
||||
self.playerLoc = mapId and self.byMap[mapId] or nil
|
||||
self.sel = 1
|
||||
for i, loc in ipairs(self.locs) do
|
||||
if loc == self.playerLoc then self.sel = i break end
|
||||
-- LoadTownMap_Fly always opens with hl on wFlyLocationsList[0], the FIRST
|
||||
-- fly destination (PALLET_TOWN), never the player's current town (#795).
|
||||
-- Only the plain viewer snaps the cursor to where the player stands.
|
||||
if not self.fly then
|
||||
for i, loc in ipairs(self.locs) do
|
||||
if loc == self.playerLoc then self.sel = i break end
|
||||
end
|
||||
end
|
||||
self.blink = 0
|
||||
return self
|
||||
@@ -266,14 +272,17 @@ function TownMap:update(dt)
|
||||
if self.fly then
|
||||
-- LoadTownMap_Fly: Up/Down cycle the visited destinations, A flies there,
|
||||
-- B cancels (handled above). moveList walks self.locs, now the fly list.
|
||||
-- Up steps FORWARD through the towns (.pressedUp does inc hl: PALLET ->
|
||||
-- VIRIDIAN -> PEWTER -> ...), Down steps back and wraps to the last
|
||||
-- visited town from the top; the port had the two swapped (#795).
|
||||
if input:wasPressed("a") then
|
||||
Sound.play(self.game.data, "Press_AB")
|
||||
local mapId = self.flyMapIds[self.sel]
|
||||
self.game.stack:pop()
|
||||
if mapId and self.onFly then self.onFly(mapId) end
|
||||
return
|
||||
elseif input:wasPressed("up") then self:moveList(-1)
|
||||
elseif input:wasPressed("down") then self:moveList(1)
|
||||
elseif input:wasPressed("up") then self:moveList(1)
|
||||
elseif input:wasPressed("down") then self:moveList(-1)
|
||||
end
|
||||
elseif self.nestSpecies then
|
||||
if input:wasPressed("a") then
|
||||
|
||||
@@ -24,6 +24,13 @@ local NO_SHORE_TILESETS = { SHIP_PORT = true }
|
||||
-- what counts as "outside" for the wLastMap memory (CheckIfInOutsideMap)
|
||||
local OUTSIDE_TILESETS = { "OVERWORLD", "PLATEAU" }
|
||||
|
||||
-- pokered's fly destination gate: BuildFlyLocationsList
|
||||
-- (engine/items/town_map.asm) walks map ids 0..NUM_CITY_MAPS-1, the eleven
|
||||
-- towns PALLET_TOWN..SAFFRON_CITY, so routes never appear even though
|
||||
-- ROUTE_4/ROUTE_10 carry fly-warp landing spots (those exist for the
|
||||
-- dungeon-escape/heal tables, special_warps.asm FlyWarpDataPtr)
|
||||
local NUM_CITY_MAPS = 11
|
||||
|
||||
-- warp pads and fall-through holes (data/tilesets/warp_pad_hole_tile_ids
|
||||
-- .asm WarpPadAndHoleData); a tileset record carrying warpPadTiles
|
||||
-- ({ [tileId] = "pad"|"hole" }) wins over these vanilla rows
|
||||
@@ -153,6 +160,17 @@ function Map.isOutside(def, tilesets)
|
||||
return false
|
||||
end
|
||||
|
||||
-- FLY destination (LoadTownMap_Fly / BuildFlyLocationsList): the eleven
|
||||
-- towns, map indices 0..NUM_CITY_MAPS-1. ROUTE_4 and ROUTE_10 are outdoor
|
||||
-- and have fly warps but are not towns, so the outdoor test alone offered
|
||||
-- their Pokemon Centers as fly targets (#788). Maps without a vanilla
|
||||
-- index (mod-authored) keep the old outdoor/PLATEAU surface test, which is
|
||||
-- how a mod adds its own fly town.
|
||||
function Map.isFlyTown(def)
|
||||
if def.index ~= nil then return def.index < NUM_CITY_MAPS end
|
||||
return Map.isOutdoor(def) or def.tileset == "PLATEAU"
|
||||
end
|
||||
|
||||
-- region groups maps a rule applies to without naming them; the id prefix
|
||||
-- is the fallback for caches that predate the property
|
||||
function Map.inRegion(def, region, prefix)
|
||||
|
||||
@@ -364,7 +364,12 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
|
||||
Game.save.flashLit = nil
|
||||
self:setDark(false)
|
||||
end
|
||||
if Game.data.field.flyWarps[mapId] then
|
||||
-- MarkTownVisitedAndLoadToggleableObjects marks towns only (cp
|
||||
-- FIRST_ROUTE_MAP): the fly-warp table also carries the ROUTE_4/ROUTE_10
|
||||
-- Pokemon Centers and the dungeon escape spots, and entering those never
|
||||
-- sets a wTownVisitedFlag bit, so it must not set save.visited either (#788)
|
||||
local mapDef = Game.data.maps[mapId]
|
||||
if Game.data.field.flyWarps[mapId] and mapDef and Map.isFlyTown(mapDef) then
|
||||
Game.save.visited = Game.save.visited or {}
|
||||
Game.save.visited[mapId] = true
|
||||
end
|
||||
|
||||
@@ -29,8 +29,8 @@ return function(game)
|
||||
}
|
||||
|
||||
-- Exercise the real visited-marking path (OverworldController marks any
|
||||
-- flyWarps map visited on entry) by standing on the Plateau exterior first,
|
||||
-- then hop back to Pallet for a clean starting point.
|
||||
-- flyWarps TOWN visited on entry, Map.isFlyTown) by standing on the Plateau
|
||||
-- exterior first, then hop back to Pallet for a clean starting point.
|
||||
U.teleport(game, "INDIGO_PLATEAU", 9, 6, "down")
|
||||
U.wait(5)
|
||||
assert(game.save.visited.INDIGO_PLATEAU,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
-- Parity test: the FLY town map opens on PALLET_TOWN and Up walks the
|
||||
-- towns forward, Down backward (#795).
|
||||
--
|
||||
-- pokered's LoadTownMap_Fly (engine/items/town_map.asm) enters its loop
|
||||
-- with hl on wFlyLocationsList[0] -- the cursor ALWAYS starts on the first
|
||||
-- fly destination, PALLET_TOWN, never the town the player is standing in.
|
||||
-- .pressedUp does `inc hl` (next town, skipping NOT_VISITED entries,
|
||||
-- wrapping at the $ff terminator back to the start) and .pressedDown does
|
||||
-- `dec hl` (previous town, wrapping off the top to the last visited town).
|
||||
-- The port started the cursor on the player's current town and had Up and
|
||||
-- Down swapped, so the menu felt like it cycled in a random order.
|
||||
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 fly cursor order")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local TownMap = require("src.ui.TownMap")
|
||||
|
||||
local function makeGame(visited, currentMap)
|
||||
local pressed = {}
|
||||
local game = {
|
||||
data = Data,
|
||||
save = { visited = visited },
|
||||
overworld = currentMap and { map = { id = currentMap } } or nil,
|
||||
input = { wasPressed = function(_, name)
|
||||
local p = pressed[name]
|
||||
pressed[name] = nil
|
||||
return p
|
||||
end },
|
||||
stack = { pop = function() end },
|
||||
}
|
||||
return game, function(name) pressed[name] = true end
|
||||
end
|
||||
|
||||
local function selection(tm)
|
||||
return tm.flyMapIds[tm.sel]
|
||||
end
|
||||
|
||||
local ALL = {
|
||||
PALLET_TOWN = true, VIRIDIAN_CITY = true, PEWTER_CITY = true,
|
||||
CERULEAN_CITY = true, LAVENDER_TOWN = true, VERMILION_CITY = true,
|
||||
CELADON_CITY = true, FUCHSIA_CITY = true, CINNABAR_ISLAND = true,
|
||||
INDIGO_PLATEAU = true, SAFFRON_CITY = true,
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- 1) the cursor opens on PALLET_TOWN, not the player's town
|
||||
-- ------------------------------------------------------------------
|
||||
local game, tap = makeGame(ALL, "CELADON_CITY")
|
||||
local tm = TownMap.new(game, { fly = true })
|
||||
check(tm.fly == true, "the town map opens in fly mode")
|
||||
eq(selection(tm), "PALLET_TOWN",
|
||||
"the fly cursor opens on PALLET_TOWN while the player stands in Celadon")
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- 2) Up walks the towns forward, Down backward, both wrapping
|
||||
-- ------------------------------------------------------------------
|
||||
tap("up") tm:update(0)
|
||||
eq(selection(tm), "VIRIDIAN_CITY", "Up from PALLET_TOWN selects VIRIDIAN_CITY")
|
||||
tap("up") tm:update(0)
|
||||
eq(selection(tm), "PEWTER_CITY", "Up again selects PEWTER_CITY")
|
||||
tap("down") tm:update(0)
|
||||
eq(selection(tm), "VIRIDIAN_CITY", "Down steps back to VIRIDIAN_CITY")
|
||||
tap("down") tm:update(0)
|
||||
eq(selection(tm), "PALLET_TOWN", "Down again returns to PALLET_TOWN")
|
||||
tap("down") tm:update(0)
|
||||
eq(selection(tm), "SAFFRON_CITY",
|
||||
"Down off the top wraps to the last visited town (SAFFRON_CITY)")
|
||||
tap("up") tm:update(0)
|
||||
eq(selection(tm), "PALLET_TOWN", "Up off the bottom wraps back to PALLET_TOWN")
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- 3) unvisited towns are skipped, in list order
|
||||
-- ------------------------------------------------------------------
|
||||
local PARTIAL = { PALLET_TOWN = true, VIRIDIAN_CITY = true, CELADON_CITY = true }
|
||||
local game2, tap2 = makeGame(PARTIAL, "VIRIDIAN_CITY")
|
||||
local tm2 = TownMap.new(game2, { fly = true })
|
||||
eq(selection(tm2), "PALLET_TOWN",
|
||||
"the fly cursor opens on PALLET_TOWN on a partial save too")
|
||||
tap2("up") tm2:update(0)
|
||||
eq(selection(tm2), "VIRIDIAN_CITY", "Up selects VIRIDIAN_CITY")
|
||||
tap2("up") tm2:update(0)
|
||||
eq(selection(tm2), "CELADON_CITY", "Up skips every unvisited town to CELADON")
|
||||
tap2("up") tm2:update(0)
|
||||
eq(selection(tm2), "PALLET_TOWN", "Up off the end wraps to PALLET_TOWN")
|
||||
tap2("down") tm2:update(0)
|
||||
eq(selection(tm2), "CELADON_CITY",
|
||||
"Down off the top wraps to CELADON, the last visited town")
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- 4) the plain town map viewer still opens on the player's town
|
||||
-- ------------------------------------------------------------------
|
||||
local game3 = makeGame(ALL, "CERULEAN_CITY")
|
||||
local viewer = TownMap.new(game3)
|
||||
check(not viewer.fly, "the plain viewer is not in fly mode")
|
||||
check(viewer.locs[viewer.sel] == viewer.playerLoc,
|
||||
"the plain viewer still opens on the player's current location")
|
||||
|
||||
S.finish()
|
||||
@@ -0,0 +1,77 @@
|
||||
-- Parity test: the Route 4 / Route 10 Pokemon Centers are not FLY
|
||||
-- destinations (#788).
|
||||
--
|
||||
-- pokered keeps fly-warp landing spots for ROUTE_4 and ROUTE_10 in
|
||||
-- data/maps/special_warps.asm FlyWarpDataPtr, but the fly picker never
|
||||
-- offers them: BuildFlyLocationsList (engine/items/town_map.asm) walks map
|
||||
-- ids 0..NUM_CITY_MAPS-1 only, and MarkTownVisitedAndLoadToggleableObjects
|
||||
-- (engine/overworld/toggleable_objects.asm) sets a wTownVisitedFlag bit
|
||||
-- only for maps below FIRST_ROUTE_MAP. The port walked the whole fly-warp
|
||||
-- table and gated on "outdoor", so both route centers showed up as fly
|
||||
-- targets the moment the player had set foot on those routes.
|
||||
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 fly route centers")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local Map = require("src.world.Map")
|
||||
local FlyMenu = require("src.ui.FlyMenu")
|
||||
local TownMap = require("src.ui.TownMap")
|
||||
|
||||
local TOWNS = {
|
||||
"PALLET_TOWN", "VIRIDIAN_CITY", "PEWTER_CITY", "CERULEAN_CITY",
|
||||
"LAVENDER_TOWN", "VERMILION_CITY", "CELADON_CITY", "FUCHSIA_CITY",
|
||||
"CINNABAR_ISLAND", "INDIGO_PLATEAU", "SAFFRON_CITY",
|
||||
}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- 1) the gate itself, straight off generated map records
|
||||
-- ------------------------------------------------------------------
|
||||
-- map indices 0..10 are exactly the eleven towns (data/generated/maps.lua
|
||||
-- carries pokered's map constant order), and Map.isFlyTown keys off them
|
||||
check(Map.isFlyTown(Data.maps.PALLET_TOWN), "PALLET_TOWN is a fly town")
|
||||
check(Map.isFlyTown(Data.maps.INDIGO_PLATEAU),
|
||||
"INDIGO_PLATEAU is a fly town (index 9, past the PLATEAU tileset)")
|
||||
check(not Map.isFlyTown(Data.maps.ROUTE_4), "ROUTE_4 is not a fly town")
|
||||
check(not Map.isFlyTown(Data.maps.ROUTE_10), "ROUTE_10 is not a fly town")
|
||||
check(not Map.isFlyTown(Data.maps.POKEMON_MANSION_1F),
|
||||
"POKEMON_MANSION_1F (dungeon escape spot) is not a fly town")
|
||||
-- a mod-authored town has no vanilla index and keeps the outdoor test
|
||||
check(Map.isFlyTown({ tileset = "OVERWORLD" }),
|
||||
"an index-less outdoor map (mod town) is a fly town")
|
||||
check(not Map.isFlyTown({ tileset = "CAVERN" }),
|
||||
"an index-less cave map is not a fly town")
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- 2) both pickers exclude the route centers, even on a polluted save
|
||||
-- ------------------------------------------------------------------
|
||||
-- Saves written before this fix already carry visited.ROUTE_4 /
|
||||
-- visited.ROUTE_10 (any map with a fly warp was marked on entry), so the
|
||||
-- menu side has to filter, not just the writer.
|
||||
local visited = { ROUTE_4 = true, ROUTE_10 = true }
|
||||
for _, id in ipairs(TOWNS) do visited[id] = true end
|
||||
local save = { visited = visited }
|
||||
|
||||
local menu = FlyMenu.new({ data = Data, save = save })
|
||||
local menuIds = {}
|
||||
for _, item in ipairs(menu.items or {}) do menuIds[#menuIds + 1] = item.value end
|
||||
eq(#menuIds, #TOWNS, "FLY lists exactly the eleven towns")
|
||||
for i, id in ipairs(TOWNS) do
|
||||
eq(menuIds[i], id, ("FLY entry %d is %s"):format(i, id))
|
||||
end
|
||||
|
||||
local tm = TownMap.new({ data = Data, save = save }, { fly = true })
|
||||
check(tm.fly == true, "the town map opens in fly mode")
|
||||
local listed = {}
|
||||
for _, id in ipairs(tm.flyMapIds or {}) do listed[id] = true end
|
||||
eq(#tm.flyMapIds, #TOWNS, "the fly town map cycles exactly the eleven towns")
|
||||
check(not listed.ROUTE_4, "ROUTE_4 is not on the fly town map")
|
||||
check(not listed.ROUTE_10, "ROUTE_10 is not on the fly town map")
|
||||
for _, id in ipairs(TOWNS) do
|
||||
check(listed[id], id .. " is on the fly town map")
|
||||
end
|
||||
|
||||
S.finish()
|
||||
Reference in New Issue
Block a user