mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 03:35:56 +02:00
big bug squash (#261)
This commit is contained in:
+17
-7
@@ -214,11 +214,10 @@ local function useOn(game, battle, id, target, list, moveIndex)
|
||||
and ow.map.id ~= "AGATHAS_ROOM" then
|
||||
list:close()
|
||||
consume(game, id)
|
||||
require("src.core.Sound").play(game.data, "Teleport_Exit1")
|
||||
ow.player.surfing = false
|
||||
-- EnterMapAnim on arrival (BIT_ESCAPE_WARP / special warp path);
|
||||
-- blackouts omit arrive="teleport" (HandleBlackOut has no LeaveMapAnim)
|
||||
ow:warpToHealPoint(nil, { arrive = "teleport" })
|
||||
-- LeaveMapAnim spin-up + SFX_TELEPORT_EXIT_1, a fade, then land OUTSIDE
|
||||
-- the last Pokémon Center town door like Fly (#196), via the shared
|
||||
-- departure helper -- the same path Dig/Teleport take from the party menu
|
||||
ow:beginTeleportOut()
|
||||
else
|
||||
showMessages(game, { "OAK: " .. game.save.player.name
|
||||
.. "!\nThis isn't the\ntime to use that!" })
|
||||
@@ -303,7 +302,8 @@ local function pickTargetAndUse(game, battle, id, list)
|
||||
-- the ETHERs and PP UP open the move menu after picking a mon
|
||||
-- (ItemUsePPRestore / ItemUsePPUp); the ELIXERs hit every move
|
||||
local wantsMove = id == "ETHER" or id == "MAX_ETHER" or id == "PP_UP"
|
||||
require("src.ui.Screens").push(game, "PartyMenu", {
|
||||
local def = game.data.items[id]
|
||||
local opts = {
|
||||
pickOnly = true,
|
||||
onSwitch = function(mon)
|
||||
if not wantsMove then
|
||||
@@ -326,7 +326,17 @@ local function pickTargetAndUse(game, battle, id, list)
|
||||
end,
|
||||
}))
|
||||
end,
|
||||
})
|
||||
}
|
||||
-- TM/HM: open the party menu in Gen 1's TM/HM display mode so each mon
|
||||
-- shows ABLE / NOT ABLE from its learnset and the prompt reads "Use TM on
|
||||
-- which POKeMON?" (engine/items/item_effects.asm ItemUseTMHM ->
|
||||
-- party_menu.asm TM/HM type). Stones and other pickOnly items keep the
|
||||
-- plain HP layout (Gen 1 shows no ABLE/NOT ABLE for them), so gate
|
||||
-- strictly on def.machine. #210
|
||||
if def and def.machine then
|
||||
opts.tmhm = { move = def.machine.move, kind = def.machine.kind }
|
||||
end
|
||||
require("src.ui.Screens").push(game, "PartyMenu", opts)
|
||||
end
|
||||
|
||||
local function useItem(game, battle, id, list)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
-- The evolution movie (engine/movie/evolution.asm): the mon's pic
|
||||
-- flashes back and forth with the evolved form, speeding up, then the
|
||||
-- new form appears with its cry and the congratulations text.
|
||||
-- B during the flash cancels ("Huh? ... stopped evolving!"? -- Gen 1
|
||||
-- has no cancel; the flash always completes).
|
||||
-- pokered engine/pokemon/evos_moves.asm (EvolveMon) polls hJoyHeld during
|
||||
-- the flash: holding B aborts the evolution -- the mon keeps its species
|
||||
-- and _StoppedEvolvingText ("Huh? MON stopped evolving!") prints. The
|
||||
-- lone exception is trade evolutions (wLinkState == LINK_STATE_TRADING),
|
||||
-- which skip that poll and cannot be cancelled (#213).
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local Music = require("src.core.Music")
|
||||
@@ -14,7 +17,10 @@ EvolutionState.isOpaque = true
|
||||
-- SGB: SetPal_PokemonWholeScreen for the mon on display
|
||||
function EvolutionState:sgbPalettes(game)
|
||||
local P = require("src.render.PaletteFX")
|
||||
local species = self.done and self.newSpecies or self.mon.species
|
||||
-- a cancelled evolution keeps the old species (never applied), so only
|
||||
-- colorize with the new form once it has actually evolved
|
||||
local species = (self.done and not self.canceled) and self.newSpecies
|
||||
or self.mon.species
|
||||
local c = P.monPal(game.data, species)
|
||||
if c then return { P.whole(c) } end
|
||||
return P.wholeNamed(game.data, "MEWMON")
|
||||
@@ -29,17 +35,22 @@ local function frontSprite(game, species)
|
||||
return ok and img or nil
|
||||
end
|
||||
|
||||
function EvolutionState.new(game, mon, newSpecies, onDone)
|
||||
function EvolutionState.new(game, mon, newSpecies, onDone, via)
|
||||
local self = setmetatable({}, EvolutionState)
|
||||
self.game = game
|
||||
self.mon = mon
|
||||
self.newSpecies = newSpecies
|
||||
self.onDone = onDone
|
||||
self.via = via
|
||||
-- evos_moves.asm: only trade evolutions (LINK_STATE_TRADING) skip the
|
||||
-- B-cancel poll; level-up, stone and rare-candy evos are all cancelable.
|
||||
self.cancelable = (via ~= "TRADE")
|
||||
self.oldName = mon.nickname or game.data.pokemon[mon.species].name
|
||||
self.oldSprite = frontSprite(game, mon.species)
|
||||
self.newSprite = frontSprite(game, newSpecies)
|
||||
self.t = 0
|
||||
self.done = false
|
||||
self.canceled = false
|
||||
Music.play(game.data, Music.special(game.data, "evolution"))
|
||||
return self
|
||||
end
|
||||
@@ -47,11 +58,28 @@ end
|
||||
function EvolutionState:update(dt)
|
||||
self.t = self.t + 1
|
||||
if self.done then return end
|
||||
local game = self.game
|
||||
-- evos_moves.asm EvolveMon: each flash iteration polls hJoyHeld and, for
|
||||
-- a cancelable evolution, aborts when B is held -- the mon keeps its
|
||||
-- species (Evolution.apply never runs) and _StoppedEvolvingText prints.
|
||||
if self.cancelable and game.input:isDown("b") then
|
||||
self.done = true
|
||||
self.canceled = true
|
||||
local TextBox = require("src.render.TextBox")
|
||||
-- mirrors data/generated/text.lua _StoppedEvolvingText
|
||||
game.stack:push(TextBox.new(game,
|
||||
("Huh? %s\nstopped evolving!"):format(self.oldName),
|
||||
function()
|
||||
Music.restoreMap(game.data)
|
||||
game.stack:pop() -- the evolution screen itself
|
||||
if self.onDone then self.onDone() end
|
||||
end))
|
||||
return
|
||||
end
|
||||
if self.t >= FLASH_FRAMES then
|
||||
self.done = true
|
||||
local game = self.game
|
||||
local Evolution = require("src.pokemon.Evolution")
|
||||
Evolution.apply(game, self.mon, self.newSpecies)
|
||||
Evolution.apply(game, self.mon, self.newSpecies, self.via)
|
||||
require("src.core.Sound").playCry(game.data, self.newSpecies)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local newName = game.data.pokemon[self.newSpecies].name
|
||||
@@ -61,7 +89,12 @@ function EvolutionState:update(dt)
|
||||
function()
|
||||
Music.restoreMap(game.data)
|
||||
game.stack:pop() -- the evolution screen itself
|
||||
if self.onDone then self.onDone() end
|
||||
-- Gen1 re-runs the level-up learn check on the evolved species
|
||||
-- after the "evolved into" text (evos_moves.asm EvolveMon ->
|
||||
-- learn_move.asm LearnMoveFromLevelUp, #12). Pop the evo screen
|
||||
-- first so the "learned MOVE!" text / forget prompt push onto the
|
||||
-- overworld / battle-return, not this state.
|
||||
Evolution.learnEvolutionMoves(game, self.mon, self.onDone)
|
||||
end))
|
||||
end
|
||||
end
|
||||
@@ -73,7 +106,8 @@ function EvolutionState:draw()
|
||||
-- accelerating flash between the two forms
|
||||
local sprite
|
||||
if self.done then
|
||||
sprite = self.newSprite
|
||||
-- a cancelled evolution settles back on the original form
|
||||
sprite = self.canceled and self.oldSprite or self.newSprite
|
||||
else
|
||||
local period = math.max(4, 28 - math.floor(self.t / 40) * 6)
|
||||
local showNew = math.floor(self.t / period) % 2 == 1
|
||||
|
||||
+6
-2
@@ -11,9 +11,13 @@ function FlyMenu.new(game)
|
||||
local visited = game.save.visited or {}
|
||||
local seen = {}
|
||||
for _, mapId in ipairs(game.data.field.flyOrder or {}) do
|
||||
-- towns only (dungeon escape spots share the table), each listed once
|
||||
-- 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).
|
||||
local def = game.data.maps[mapId]
|
||||
if visited[mapId] and def and Map.isOutdoor(def) and not seen[mapId] then
|
||||
if visited[mapId] and def and not seen[mapId]
|
||||
and (Map.isOutdoor(def) or def.tileset == "PLATEAU") then
|
||||
seen[mapId] = true
|
||||
table.insert(items, {
|
||||
value = mapId,
|
||||
|
||||
+96
-23
@@ -132,6 +132,10 @@ function PartyMenu.new(game, opts)
|
||||
self.onSwitch = opts.onSwitch
|
||||
self.onCancel = opts.onCancel
|
||||
self.pickOnly = opts.pickOnly
|
||||
-- TM/HM teaching: opts.tmhm = { move, kind } switches the list to Gen 1's
|
||||
-- TM/HM display (ABLE / NOT ABLE per mon instead of the HP bar, and the
|
||||
-- "Use TM on which POKeMON?" prompt). Set by BagMenu.pickTargetAndUse. #210
|
||||
self.tmhm = opts.tmhm
|
||||
self.forceSwitch = opts.forceSwitch
|
||||
self.battle = opts.battle
|
||||
self.party = opts.party -- link battles pass their clamped copies
|
||||
@@ -178,8 +182,15 @@ function PartyMenu:update(dt)
|
||||
elseif action == "switch" then
|
||||
self.swapFrom = self.index
|
||||
elseif action == "fly" then
|
||||
-- FLY opens the TOWN MAP with a cursor over the visited fly towns,
|
||||
-- not a plain text list (engine/menus/town_map.asm LoadTownMap_Fly).
|
||||
-- flyTo (OverworldController) validates the fly-warp + runs the
|
||||
-- departure/warp, so we just hand it the chosen mapId (#195).
|
||||
local ow = self.game.overworld
|
||||
self.game.stack:pop() -- close the party menu
|
||||
Screens.push(self.game, "FlyMenu")
|
||||
Screens.push(self.game, "TownMap", { fly = true, onFly = function(mapId)
|
||||
if ow then ow:flyTo(mapId) end
|
||||
end })
|
||||
return
|
||||
elseif action == "flash" then -- FLASH lights dark tunnels
|
||||
-- start_sub_menus.asm .flash: PrintText _FlashLightsAreaText, then
|
||||
@@ -304,21 +315,15 @@ function PartyMenu:update(dt)
|
||||
-- 1/5 of the user's max HP to a chosen teammate
|
||||
self.softboiledFrom = self.index
|
||||
elseif action == "escape" then
|
||||
-- DIG / TELEPORT both warp to the last Pokémon Center town
|
||||
-- (wLastBlackoutMap, special_warps.asm escape warp); .dig/.teleport
|
||||
-- end with GBPalWhiteOutWithDelay3 + jp .goBackToMap
|
||||
-- DIG / TELEPORT warp to the last Pokémon Center TOWN (wLastBlackoutMap,
|
||||
-- special_warps.asm escape warp). pokered's .dig/.teleport spin the
|
||||
-- player up (LeaveMapAnim), white/fade out, then land it; this port
|
||||
-- lands OUTSIDE the town PC door like Fly (#196). beginTeleportOut
|
||||
-- centralizes the spin -> fade -> warp so BagMenu's ESCAPE ROPE shares
|
||||
-- the exact departure; the fade + warp fire when the spin ends.
|
||||
local ow = self.game.overworld
|
||||
local heal = self.game.save.lastHeal
|
||||
local Transition = require("src.render.Transition")
|
||||
self.game.stack:pop()
|
||||
if ow and heal then
|
||||
self.game.stack:push(Transition.whiteFlash(self.game, nil, function()
|
||||
require("src.core.Sound").play(self.game.data, "Teleport_Exit1")
|
||||
-- EnterMapAnim on arrival (HandleFlyWarpOrDungeonWarp sets
|
||||
-- BIT_FLY_WARP); blackouts must not pass arrive="teleport"
|
||||
ow:warpToHealPoint(nil, { arrive = "teleport" })
|
||||
end))
|
||||
end
|
||||
if ow then ow:beginTeleportOut() end
|
||||
return
|
||||
end
|
||||
self.submenu = nil
|
||||
@@ -436,6 +441,30 @@ function PartyMenu:update(dt)
|
||||
end
|
||||
end
|
||||
|
||||
-- The bottom-of-screen context message for the current menu state
|
||||
-- (pokered engine/menus/party_menu.asm PartyMenuMessage / RedrawPartyMenu_):
|
||||
-- the party menu always prints a message in the bottom text box. With the
|
||||
-- normal message id that is PartyMenuBattleText ("Bring out which POKéMON?")
|
||||
-- when IsInBattle else PartyMenuNormalText ("Choose a POKéMON."); the swap /
|
||||
-- item / TM-HM ids print their own strings, which draw() handles inline.
|
||||
-- Pure (no side effects) so drivers can assert it. #147
|
||||
function PartyMenu:bottomMessage()
|
||||
if self.swapFrom then
|
||||
return "Move to where?"
|
||||
elseif self.softboiledFrom or self.pickOnly then
|
||||
return "Use on which one?"
|
||||
elseif self.tmhm then
|
||||
return self.game.data.text._PartyMenuUseTMText
|
||||
or "Use TM on which\nPOKéMON?"
|
||||
elseif self.battle then
|
||||
return self.game.data.text._PartyMenuBattleText
|
||||
or "Bring out which\nPOKéMON?"
|
||||
else
|
||||
return self.game.data.text._PartyMenuNormalText
|
||||
or "Choose a POKéMON."
|
||||
end
|
||||
end
|
||||
|
||||
function PartyMenu:draw()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 144)
|
||||
@@ -462,16 +491,34 @@ function PartyMenu:draw()
|
||||
-- PrintLevel overwrites the <LV> tile with the third digit
|
||||
Font.draw(tostring(mon.level), 104, y)
|
||||
end
|
||||
if mon.hp <= 0 then
|
||||
Font.draw("FNT", 136, y)
|
||||
elseif mon.status then
|
||||
Font.draw(mon.status, 136, y)
|
||||
if self.tmhm then
|
||||
-- TM/HM teaching menu (engine/menus/party_menu.asm PrintPartyMenu):
|
||||
-- the second row shows the inline "ABLE" / "NOT ABLE" learnability
|
||||
-- strings in place of the HP bar and status, decided by CanLearnTM.
|
||||
-- The learnset scan mirrors ItemEffects.use so the display can never
|
||||
-- disagree with the actual teach. #210
|
||||
local can = false
|
||||
for _, m in ipairs(def.tmhm or {}) do
|
||||
if m == self.tmhm.move then can = true break end
|
||||
end
|
||||
-- right-aligned so the shorter "ABLE" shares "NOT ABLE"'s right edge
|
||||
if can then
|
||||
Font.draw("ABLE", 120, y + 8)
|
||||
else
|
||||
Font.draw("NOT ABLE", 88, y + 8)
|
||||
end
|
||||
else
|
||||
if mon.hp <= 0 then
|
||||
Font.draw("FNT", 136, y)
|
||||
elseif mon.status then
|
||||
Font.draw(mon.status, 136, y)
|
||||
end
|
||||
-- the colored tile HP bar (DrawHP2 + SetPartyMenuHPBarColor)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
HudTiles.drawHPBar(self.game.data, 5, (y + 8) / 8, mon)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(("%3d/%3d"):format(mon.hp, mon.stats.hp), 104, y + 8)
|
||||
end
|
||||
-- the colored tile HP bar (DrawHP2 + SetPartyMenuHPBarColor)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
HudTiles.drawHPBar(self.game.data, 5, (y + 8) / 8, mon)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(("%3d/%3d"):format(mon.hp, mon.stats.hp), 104, y + 8)
|
||||
if i == self.index then
|
||||
Font.drawCode(Theme.cursor, 0, y)
|
||||
end
|
||||
@@ -483,8 +530,34 @@ function PartyMenu:draw()
|
||||
Font.draw("Move to where?", 8, 136)
|
||||
elseif self.softboiledFrom then
|
||||
Font.draw("Use on which one?", 8, 136)
|
||||
elseif self.tmhm then
|
||||
-- "Use TM on which\nPOKeMON?" in the standard bottom text box
|
||||
-- (party_menu.asm keeps the message box for the TM/HM menu); box + line
|
||||
-- geometry match TextBox's default (rows 12-17, text on rows 14/16). #210
|
||||
Font.drawBox(0, 12, 20, 6)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
local prompt = self.game.data.text._PartyMenuUseTMText
|
||||
or "Use TM on which\nPOKéMON?"
|
||||
local ly = 112
|
||||
for line in (prompt .. "\n"):gmatch("([^\n]*)\n") do
|
||||
Font.draw(line, 8, ly)
|
||||
ly = ly + 16
|
||||
end
|
||||
elseif self.pickOnly then
|
||||
Font.draw("Use on which one?", 8, 136)
|
||||
else
|
||||
-- default field party menu (StartMenu) and the battle voluntary-switch
|
||||
-- (BattleState:openParty): Gen1 prints PartyMenuNormalText / PartyMenuBattleText
|
||||
-- in the standard bottom text box (party_menu.asm PartyMenuMessage), not
|
||||
-- plain bottom-row text. Box + line geometry match the #210 TM/HM case and
|
||||
-- TextBox's default (rows 12-17, text on rows 14/16). #147
|
||||
Font.drawBox(0, 12, 20, 6)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
local ly = 112
|
||||
for line in (self:bottomMessage() .. "\n"):gmatch("([^\n]*)\n") do
|
||||
Font.draw(line, 8, ly)
|
||||
ly = ly + 16
|
||||
end
|
||||
end
|
||||
if self.submenu then
|
||||
local n = #self.subItems
|
||||
|
||||
+5
-2
@@ -106,8 +106,11 @@ local function sell(game)
|
||||
onChoose = function(item)
|
||||
local def = game.data.items[item.value]
|
||||
-- only key items and HMs are unsellable (pokemart.asm IsKeyItem /
|
||||
-- IsItemHM); zero-price items like ETHER sell for ¥0
|
||||
if (def and def.keyItem) or item.value:find("^HM_") then
|
||||
-- IsItemHM); zero-price items like ETHER sell for ¥0. An unknown id
|
||||
-- (nil def) has no price, so treat it as unsellable too rather than
|
||||
-- indexing nil below -- guards saves that already picked up a bogus
|
||||
-- ITEM_NONE "0" from Blue's House before that pickup was fixed (#11).
|
||||
if not def or def.keyItem or item.value:find("^HM_") then
|
||||
list.footer = txt(game, "_PokemartUnsellableItemText",
|
||||
"I can't put a\nprice on that.")
|
||||
return
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
-- A on page 2) closes.
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
-- status_screen.asm PrintMonType prints the type's DISPLAY name from the
|
||||
-- TypeNames table, not the constant: species types are stored as pokered
|
||||
-- constants (RomExtractor:typesById) and PSYCHIC's is "PSYCHIC_TYPE" (so it
|
||||
-- won't collide with the PSYCHIC move), which would overflow the TYPE field.
|
||||
-- TypeChart.displayName maps it back to "PSYCHIC", like HallOfFame and the
|
||||
-- battle move-type box already do (#214).
|
||||
local TypeChart = require("src.battle.TypeChart")
|
||||
|
||||
local SummaryMenu = {}
|
||||
SummaryMenu.__index = SummaryMenu
|
||||
@@ -99,10 +106,10 @@ function SummaryMenu:draw()
|
||||
-- TYPE1/TYPE2/IDNo/OT column (10,9) with values indented (11,10)
|
||||
drawLineBox(19, 9, 8, 6)
|
||||
Font.draw("TYPE1/", 80, 72)
|
||||
Font.draw(def.types[1] or "", 88, 80)
|
||||
Font.draw(def.types[1] and TypeChart.displayName(def.types[1]) or "", 88, 80)
|
||||
if def.types[2] then
|
||||
Font.draw("TYPE2/", 80, 88)
|
||||
Font.draw(def.types[2], 88, 96)
|
||||
Font.draw(TypeChart.displayName(def.types[2]), 88, 96)
|
||||
end
|
||||
Font.draw("IDNo/", 80, 104)
|
||||
-- the trainer ID is rolled at new game (SaveData.newGame) and
|
||||
|
||||
+101
-10
@@ -7,6 +7,11 @@
|
||||
-- the selected name in a banner up top, and the player's current
|
||||
-- location blinking. List mode (townMap data missing): up/down through
|
||||
-- an ordered list of fly towns instead. B closes.
|
||||
--
|
||||
-- Fly mode (opts.fly + opts.onFly, LoadTownMap_Fly): the same Kanto map,
|
||||
-- but the cursor cycles ONLY the visited fly destinations (Up/Down, in fly
|
||||
-- order), the banner reads "To <NAME>", and A calls onFly(mapId) to depart.
|
||||
-- This is what the party-menu FLY field move opens (#195).
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local Sound = require("src.core.Sound")
|
||||
@@ -80,7 +85,10 @@ local function buildLocations(game)
|
||||
local seen = {}
|
||||
for _, mapId in ipairs(field.flyOrder or {}) do
|
||||
local def = game.data.maps and game.data.maps[mapId]
|
||||
if not seen[mapId] and def and Map.isOutdoor(def) then
|
||||
-- accept the PLATEAU tileset too so Indigo Plateau shows on the
|
||||
-- stale-asset list fallback, matching the fly-list filter (#203)
|
||||
if not seen[mapId] and def
|
||||
and (Map.isOutdoor(def) or def.tileset == "PLATEAU") then
|
||||
seen[mapId] = true
|
||||
local loc = { name = mapId:gsub("_", " ") }
|
||||
table.insert(locs, loc)
|
||||
@@ -119,6 +127,42 @@ local function markerXY(loc)
|
||||
return loc.x * 8 + 16, loc.y * 8 + 8
|
||||
end
|
||||
|
||||
-- the row-0 name banner; fly mode prefixes "To " like LoadTownMap_Fly
|
||||
-- (engine/menus/town_map.asm prints the destination as "To <NAME>")
|
||||
function TownMap:bannerText(loc)
|
||||
return (self.fly and "To " or "") .. loc.name
|
||||
end
|
||||
|
||||
-- Fly mode selection set (engine/menus/town_map.asm LoadTownMap_Fly): the
|
||||
-- cursor cycles ONLY the visited fly destinations, in fly order, each landing
|
||||
-- on its town square. Built from field.flyOrder filtered to visited outdoor
|
||||
-- towns that have a fly-warp spot, deduped, reusing the grid loc so the cursor
|
||||
-- lands on the town and its name shows in the banner.
|
||||
local function buildFlyList(game, byMap)
|
||||
local field = game.data.field or {}
|
||||
local visited = game.save.visited or {}
|
||||
local flyWarps = field.flyWarps or {}
|
||||
local Map = require("src.world.Map")
|
||||
local flyLocs, flyMapIds, seen = {}, {}, {}
|
||||
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).
|
||||
if not seen[mapId] and visited[mapId] and flyWarps[mapId]
|
||||
and def and (Map.isOutdoor(def) or def.tileset == "PLATEAU") then
|
||||
seen[mapId] = true
|
||||
local loc = byMap[mapId] or { name = mapId:gsub("_", " ") }
|
||||
table.insert(flyLocs, loc)
|
||||
flyMapIds[#flyLocs] = mapId
|
||||
end
|
||||
end
|
||||
return flyLocs, flyMapIds
|
||||
end
|
||||
|
||||
-- opts.nestSpecies: the Pokédex AREA screen (LoadTownMap_Nest) --
|
||||
-- blink a nest icon on every map whose wild slots hold the species
|
||||
function TownMap.new(game, opts)
|
||||
@@ -152,6 +196,26 @@ function TownMap.new(game, opts)
|
||||
or "assets/generated/townmap/nest.png")
|
||||
self.nestIcon = ok and img or nil
|
||||
end
|
||||
if opts.fly then
|
||||
-- FLY picker (LoadTownMap_Fly): restrict the selectable set to the
|
||||
-- visited fly towns so Up/Down cycle only those and A knows the mapId.
|
||||
local flyLocs, flyMapIds = buildFlyList(game, self.byMap)
|
||||
if #flyLocs > 0 then
|
||||
self.fly = true
|
||||
self.onFly = opts.onFly
|
||||
self.locs = flyLocs
|
||||
self.flyMapIds = flyMapIds
|
||||
-- grid rendering needs coords on every entry; without them fall back to
|
||||
-- the name list so the fly screen still works on stale asset builds
|
||||
if self.mode == "grid" then
|
||||
for _, loc in ipairs(flyLocs) do
|
||||
if not (loc.x and loc.y) then self.mode = "list" break end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- with nothing visited yet there is nowhere to fly: leave self.fly unset
|
||||
-- so the screen degrades to a plain viewer (B closes)
|
||||
end
|
||||
-- the player's current location (guard: overworld may not be running)
|
||||
local mapId = game.overworld and game.overworld.map and game.overworld.map.id
|
||||
self.playerLoc = mapId and self.byMap[mapId] or nil
|
||||
@@ -199,7 +263,19 @@ function TownMap:update(dt)
|
||||
self.game.stack:pop()
|
||||
return
|
||||
end
|
||||
if self.nestSpecies then
|
||||
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.
|
||||
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)
|
||||
end
|
||||
elseif self.nestSpecies then
|
||||
if input:wasPressed("a") then
|
||||
Sound.play(self.game.data, "Press_AB")
|
||||
self.game.stack:pop()
|
||||
@@ -260,18 +336,28 @@ function TownMap:draw()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
-- the player's current location blinks (slow phase)
|
||||
-- the player's current location blinks (slow phase). Paint it with a
|
||||
-- palette-safe DARK shade (red 0), not red: this screen composites through
|
||||
-- the TOWNMAP SGB shade-remap shader (PaletteFX.shader), which keys ONLY on
|
||||
-- the red channel, and a red-0.75 dot lands in the c1 bucket = TOWNMAP
|
||||
-- {165,214,255}, the exact light-blue used for the water and the town-square
|
||||
-- fill, so the marker was drawn but recolored invisible (#152). Red 0 -> c3
|
||||
-- {25,16,16} = a solid dark "you are here" dot, visible on land and water.
|
||||
if self.playerLoc and self.blink < 20 then
|
||||
local x, y = markerXY(self.playerLoc)
|
||||
love.graphics.setColor(0.75, 0.1, 0.1, 1)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("fill", x + 2, y + 2, 4, 4)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
-- blinking cursor on the selected location
|
||||
-- blinking cursor on the selected location. markerXY is the 8x8 cell's
|
||||
-- top-left; the cursor asset is a 16x16 hollow frame centered on its own
|
||||
-- (8,8), so draw it -4,-4 to enclose the cell (engine/menus/town_map.asm
|
||||
-- draws the box cursor CENTERED on the selected location). Drawing it at
|
||||
-- the cell top-left put the square in the frame's top-left quadrant (#152).
|
||||
if selected and self.blink % 16 < 10 then
|
||||
local x, y = markerXY(selected)
|
||||
if self.bg.cursor then
|
||||
love.graphics.draw(self.bg.cursor, x, y)
|
||||
love.graphics.draw(self.bg.cursor, x - 4, y - 4)
|
||||
else
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("line", x + 0.5, y + 0.5, 7, 7)
|
||||
@@ -281,7 +367,7 @@ function TownMap:draw()
|
||||
-- the name strip on row 0 (DisplayTownMap: ClearScreenArea + name)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 8)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
if selected then Font.draw(selected.name, 8, 0) end
|
||||
if selected then Font.draw(self:bannerText(selected), 8, 0) end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
@@ -294,7 +380,9 @@ function TownMap:draw()
|
||||
drawSquare(loc)
|
||||
end
|
||||
if self.playerLoc and self.blink < 20 then
|
||||
love.graphics.setColor(0.75, 0.1, 0.1, 1)
|
||||
-- palette-safe dark, same red-channel shade-remap reason as the primary
|
||||
-- grid path above (#152); stale-asset builds hit this fallback square
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("fill", self.playerLoc.x * 8 + 2,
|
||||
self.playerLoc.y * 8 + 2, 4, 4)
|
||||
end
|
||||
@@ -317,7 +405,10 @@ function TownMap:draw()
|
||||
end
|
||||
Font.draw(loc.name, 24, y)
|
||||
if loc == self.playerLoc and self.blink < 20 then
|
||||
-- blinking marker on the player's current town
|
||||
-- blinking marker on the player's current town; force the palette-safe
|
||||
-- dark shade explicitly so the red-channel shade-remap keeps it
|
||||
-- visible regardless of Font.draw's leftover color (#152)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("fill", 24 + #loc.name * 8 + 6, y + 2, 4, 4)
|
||||
end
|
||||
end
|
||||
@@ -327,7 +418,7 @@ function TownMap:draw()
|
||||
-- name banner across the top
|
||||
Font.drawBox(0, 0, 20, 3)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
if selected then Font.draw(selected.name, 8, 8) end
|
||||
if selected then Font.draw(self:bannerText(selected), 8, 8) end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user