diff --git a/src/ui/TownMap.lua b/src/ui/TownMap.lua index 32d52012..ec6f305c 100644 --- a/src/ui/TownMap.lua +++ b/src/ui/TownMap.lua @@ -221,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 @@ -267,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 diff --git a/tests/parity_fly_cursor_order.lua b/tests/parity_fly_cursor_order.lua new file mode 100644 index 00000000..1a1bb518 --- /dev/null +++ b/tests/parity_fly_cursor_order.lua @@ -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()