Files
2026-07-17 20:30:02 -04:00

258 lines
8.4 KiB
Lua

-- Map browser: view any map, follow its warps, and set the save's spawn
-- point / remembered outdoor or heal spot by clicking cells on the
-- rendered map. Reuses the game's own MapLoader/TileRenderer/Warp so the
-- editor's view matches what the player would actually see.
local MapLoader = require("src.world.MapLoader")
local Warp = require("src.world.Warp")
local MapBrowser = {}
local LIST_W, LIST_H, ROW_H = 200, 300, 20
local MAX_ROWS = math.floor(LIST_H / ROW_H)
local VIEW_W, VIEW_H = 480, 432
local function clampZoom(z)
if z < 1 then return 1 end
if z > 4 then return 4 end
return z
end
-- Point the camera so (cx,cy) lands in the middle of the viewport.
local function centerOn(S, cx, cy)
S.mapCamX = cx * 16 - VIEW_W / (2 * S.mapZoom)
S.mapCamY = cy * 16 - VIEW_H / (2 * S.mapZoom)
end
-- Detect outdoor the same way the game treats LAST_MAP sources:
-- OVERWORLD/PLATEAU tilesets, or maps with connections / visited fly spots.
local function isOutdoor(S, map)
if map.def.tileset == "OVERWORLD" or map.def.tileset == "PLATEAU" then
return true
end
if next(map.def.connections or {}) ~= nil then return true end
return (S.save.visited and S.save.visited[map.id]) or false
end
local function sortedMapIds(data)
local ids = {}
for id in pairs(data.maps) do table.insert(ids, id) end
table.sort(ids)
return ids
end
-- LAST_MAP warps resolve against the remembered outdoor spot; skip with
-- a status message if the save has none (fresh games, or old saves).
-- Mirrors the game: leaving an OVERWORLD/PLATEAU map via a warp updates
-- lastOutdoor so building exits (Indigo lobby, Route 22 Gate, …) return
-- to the map you entered from.
local OUTSIDE_TILESETS = { OVERWORLD = true, PLATEAU = true }
local function goToWarp(S, warp)
local def = warp.def
local fromMap = S.data.maps[S.mapId]
if fromMap and OUTSIDE_TILESETS[fromMap.tileset]
and def.destMap ~= "LAST_MAP" and def.destMap ~= S.mapId then
S.save.lastOutdoor = { id = S.mapId, x = def.x, y = def.y }
end
if def.destMap == "LAST_MAP" and not S.save.lastOutdoor then
S.status = "Can't follow warp: no remembered outdoor map (lastOutdoor unset)"
return
end
local ok, destMap, dx, dy = pcall(Warp.destination, S.data, def, S.save.lastOutdoor)
if not ok then
S.status = "Warp failed: " .. tostring(destMap)
return
end
S.mapId = destMap
S.mapClickCell = nil
centerOn(S, dx, dy)
S.status = "Followed warp to " .. destMap
end
-- Screen-space point inside the viewport -> map cell, or nil if the
-- point is outside the viewport or off the edge of the map.
local function cellAtScreen(S, map, Kit, vx, vy)
if Kit.mouseX < vx or Kit.mouseX >= vx + VIEW_W
or Kit.mouseY < vy or Kit.mouseY >= vy + VIEW_H then
return nil
end
local wx = (Kit.mouseX - vx) / S.mapZoom + S.mapCamX
local wy = (Kit.mouseY - vy) / S.mapZoom + S.mapCamY
local cx, cy = math.floor(wx / 16), math.floor(wy / 16)
if not map:inBounds(cx, cy) then return nil end
return cx, cy
end
-- Wired from App.wheelmoved while the Map tab is active.
function MapBrowser.wheelmoved(S, dy)
S.mapZoom = clampZoom((S.mapZoom or 2) + (dy > 0 and 0.25 or -0.25))
end
local PAN_KEYS = {
up = { 0, -16 }, w = { 0, -16 },
down = { 0, 16 }, s = { 0, 16 },
left = { -16, 0 }, a = { -16, 0 },
right = { 16, 0 }, d = { 16, 0 },
}
-- Wired from App.keypressed while the Map tab is active.
function MapBrowser.keypressed(S, key)
local d = PAN_KEYS[key]
if not d then return end
S.mapCamX = (S.mapCamX or 0) + d[1]
S.mapCamY = (S.mapCamY or 0) + d[2]
end
function MapBrowser.draw(S, Kit, x, y)
Kit.label(x, y, "Map: " .. tostring(S.mapId))
-- ---- map id list (paginated; ~220 maps is too many for one page) ----
local ids = sortedMapIds(S.data)
S.mapListScroll = S.mapListScroll or 0
local pageIds, selectedIdx = {}, nil
for i = 1, MAX_ROWS do
local id = ids[S.mapListScroll + i]
if id then
pageIds[#pageIds + 1] = id
if id == S.mapId then selectedIdx = #pageIds end
end
end
local clickIdx = Kit.list(x, y + 24, LIST_W, MAX_ROWS * ROW_H, pageIds, selectedIdx, ROW_H)
if clickIdx and pageIds[clickIdx] then
S.mapId = pageIds[clickIdx]
S.mapClickCell = nil
if S.save.player.map == S.mapId then
centerOn(S, S.save.player.x, S.save.player.y)
else
S.mapCamX, S.mapCamY = 0, 0
end
end
local listBottom = y + 24 + MAX_ROWS * ROW_H + 8
if Kit.button(x, listBottom, 60, 24, "Prev") then
S.mapListScroll = math.max(0, S.mapListScroll - MAX_ROWS)
end
if Kit.button(x + 64, listBottom, 60, 24, "Next") then
if S.mapListScroll + MAX_ROWS < #ids then
S.mapListScroll = S.mapListScroll + MAX_ROWS
end
end
if Kit.button(x, listBottom + 30, LIST_W, 24, "Go to save location") then
S.mapId = S.save.player.map
S.mapClickCell = nil
centerOn(S, S.save.player.x, S.save.player.y)
end
-- ---- map viewport ----
local vx, vy = x + LIST_W + 20, y + 24
local ok, map = pcall(MapLoader.load, S.data, S.mapId)
if not ok then
Kit.label(vx, vy, "Failed to load map: " .. tostring(map))
return
end
-- love_stub (headless tests) lacks push/pop/scale/scissor; skip the
-- actual render there but keep all click/button logic below running.
if love.graphics.push then
love.graphics.setScissor(vx, vy, VIEW_W, VIEW_H)
love.graphics.push()
love.graphics.translate(vx, vy)
love.graphics.scale(S.mapZoom, S.mapZoom)
map.renderer:draw(S.mapCamX, S.mapCamY)
if S.save.player.map == S.mapId then
love.graphics.setColor(1, 0.2, 0.2)
love.graphics.rectangle("fill",
S.save.player.x * 16 - S.mapCamX,
S.save.player.y * 16 - S.mapCamY, 16, 16)
end
love.graphics.setColor(0.2, 0.8, 1, 0.5)
for _, w in ipairs(map.def.warps) do
love.graphics.rectangle("line",
w.x * 16 - S.mapCamX, w.y * 16 - S.mapCamY, 16, 16)
end
if S.mapClickCell then
love.graphics.setColor(1, 1, 0.2, 0.9)
love.graphics.rectangle("line",
S.mapClickCell.cx * 16 - S.mapCamX, S.mapClickCell.cy * 16 - S.mapCamY, 16, 16)
end
love.graphics.setColor(1, 1, 1, 1)
love.graphics.pop()
love.graphics.setScissor()
end
-- ---- click handling: warp cells jump the view, others select ----
if Kit.mouseClicked then
local cx, cy = cellAtScreen(S, map, Kit, vx, vy)
if cx then
local warp = map:warpAtCell(cx, cy)
if warp then
goToWarp(S, warp)
else
S.mapClickCell = { cx = cx, cy = cy }
S.status = string.format("Selected cell (%d,%d) on %s", cx, cy, S.mapId)
end
end
end
-- ---- info + set-location buttons ----
local by = vy + VIEW_H + 8
Kit.label(vx, by, S.mapClickCell
and string.format("Selected: (%d,%d) zoom %.2fx", S.mapClickCell.cx, S.mapClickCell.cy, S.mapZoom)
or string.format("Click a cell to select it zoom %.2fx", S.mapZoom))
if Kit.button(vx, by + 22, 140, 26, "Set player here") then
if S.mapClickCell then
S.save.player.map = S.mapId
S.save.player.x = S.mapClickCell.cx
S.save.player.y = S.mapClickCell.cy
S.dirty = true
S.status = string.format("Player set to %s (%d,%d)", S.mapId, S.mapClickCell.cx, S.mapClickCell.cy)
else
S.status = "Click a cell first"
end
end
if Kit.button(vx + 150, by + 22, 160, 26, "Set lastOutdoor here") then
if not S.mapClickCell then
S.status = "Click a cell first"
elseif not isOutdoor(S, map) then
S.status = S.mapId .. " doesn't look outdoor (no connections, not visited)"
else
S.save.lastOutdoor = { id = S.mapId, x = S.mapClickCell.cx, y = S.mapClickCell.cy }
S.dirty = true
S.status = "lastOutdoor set to " .. S.mapId
end
end
if Kit.button(vx + 320, by + 22, 140, 26, "Set lastHeal here") then
if S.mapClickCell then
S.save.lastHeal = { map = S.mapId, x = S.mapClickCell.cx, y = S.mapClickCell.cy }
S.dirty = true
S.status = "lastHeal set to " .. S.mapId
else
S.status = "Click a cell first"
end
end
if Kit.button(vx, by + 56, 120, 24, "Center on player") then
if S.save.player.map == S.mapId then
centerOn(S, S.save.player.x, S.save.player.y)
else
S.status = "Player isn't on this map"
end
end
if Kit.button(vx + 130, by + 56, 60, 24, "Zoom -") then
S.mapZoom = clampZoom((S.mapZoom or 2) - 0.5)
end
if Kit.button(vx + 196, by + 56, 60, 24, "Zoom +") then
S.mapZoom = clampZoom((S.mapZoom or 2) + 0.5)
end
end
return MapBrowser