Merge pull request #1025 from AverageConsumer/feature/map-overview-detail-markers

Mod API: add detailed map overview data and POIs
This commit is contained in:
bryanthaboi
2026-08-10 14:08:58 -04:00
committed by GitHub
3 changed files with 106 additions and 22 deletions
+10
View File
@@ -35,6 +35,16 @@ An edited vanilla map becomes a `mod.content.maps:patch` carrying only the
fields that moved; a new map becomes a `:register`. See
`docs/new-features.md` and the extension's own README.
## Read-only map overviews
`mod.world:mapOverview()` returns collision `rows` at map-cell resolution,
optional visual `tileRows` at 2x resolution, and optional `tileDetailRows` at
4x resolution. Visual rows contain Game Boy shades from `"0"` (lightest) to
`"3"` (darkest); their matching width and height fields describe the grid.
`markers` contains active `{ kind, x, y }` points in map-cell coordinates for
`warp`, visible `item`, and untaken `hidden` locations. All fields are
read-only snapshots; mods choose which layers to render.
## Rendering pipelines
Most registries hand the engine *content*. `render_pipelines` hands it
+53 -15
View File
@@ -18,6 +18,11 @@ local overviewShades = {}
Assets.register(function() overviewShades = {} end)
local function shadeDigit(sum, pixelCount)
return tostring(math.max(0, math.min(3,
math.floor((1 - sum / pixelCount) * 3 + 0.5))))
end
local function mapTileRows(map)
local tileset = map.tileset
if not (tileset and tileset.image and tileset.tilesPerRow) then return nil end
@@ -28,30 +33,39 @@ local function mapTileRows(map)
cached = { pixels = pixels, shades = {} }
overviewShades[tileset.image] = cached
end
local rows, perRow = {}, tileset.tilesPerRow
local rows, detailRows, perRow = {}, {}, tileset.tilesPerRow
for ty = 0, map.heightCells * 2 - 1 do
local row = {}
local row, detailTop, detailBottom = {}, {}, {}
for tx = 0, map.widthCells * 2 - 1 do
local tile = map:tileAt(tx, ty)
local shade = cached.shades[tile]
if shade == nil then
local sum = 0
local shades = cached.shades[tile]
if shades == nil then
local sums = { 0, 0, 0, 0 }
local ox, oy = (tile % perRow) * 8, math.floor(tile / perRow) * 8
for py = 0, 7 do
for px = 0, 7 do
local r, g, b = cached.pixels:getPixel(ox + px, oy + py)
sum = sum + r * 0.2126 + g * 0.7152 + b * 0.0722
local quadrant = math.floor(py / 4) * 2 + math.floor(px / 4) + 1
sums[quadrant] = sums[quadrant]
+ r * 0.2126 + g * 0.7152 + b * 0.0722
end
end
shade = tostring(math.max(0, math.min(3,
math.floor((1 - sum / 64) * 3 + 0.5))))
cached.shades[tile] = shade
shades = {
shadeDigit(sums[1] + sums[2] + sums[3] + sums[4], 64),
shadeDigit(sums[1], 16), shadeDigit(sums[2], 16),
shadeDigit(sums[3], 16), shadeDigit(sums[4], 16),
}
cached.shades[tile] = shades
end
row[#row + 1] = shade
row[#row + 1] = shades[1]
detailTop[#detailTop + 1] = shades[2] .. shades[3]
detailBottom[#detailBottom + 1] = shades[4] .. shades[5]
end
rows[#rows + 1] = table.concat(row)
detailRows[#detailRows + 1] = table.concat(detailTop)
detailRows[#detailRows + 1] = table.concat(detailBottom)
end
return rows
return rows, detailRows
end
function WorldAPI.new(game, modId)
@@ -86,10 +100,12 @@ end
-- A compact, read-only view of the active map for minimaps and companion UIs.
-- `rows` describes collision terrain; optional `tileRows` reduces each real
-- 8x8 map tile to its average Game Boy shade ("0" lightest, "3" darkest).
-- `tileDetailRows` preserves one shade per 4x4 quadrant. Markers identify
-- exits and item spots that are still active without exposing world internals.
function WorldAPI:mapOverview()
local ow = self:overworld()
if not ow or not ow.map then return nil, NO_OVERWORLD end
local map, rows = ow.map, {}
local map, rows, markers = ow.map, {}, {}
for y = 0, map.heightCells - 1 do
local row = {}
for x = 0, map.widthCells - 1 do
@@ -99,11 +115,33 @@ function WorldAPI:mapOverview()
end
rows[#rows + 1] = table.concat(row)
end
local tileRows = mapTileRows(map)
local def = map.def or {}
for _, warp in ipairs(def.warps or {}) do
markers[#markers + 1] = { kind = "warp", x = warp.x, y = warp.y }
end
local game, save = self.game, self.game.save or {}
for _, obj in ipairs(def.objects or {}) do
if obj.item and obj.item ~= "0" and obj.item ~= 0
and ow.objectVisible(save, map.id, obj) then
markers[#markers + 1] = { kind = "item", x = obj.x, y = obj.y }
end
end
local hidden = game.data and game.data.field and game.data.field.hiddenItems
for _, item in ipairs(hidden and hidden[map.id] or {}) do
local key = map.id .. "_" .. item.x .. "_" .. item.y
if not (save.hiddenTaken and save.hiddenTaken[key]) then
markers[#markers + 1] = { kind = "hidden", x = item.x, y = item.y }
end
end
local tileRows, tileDetailRows = mapTileRows(map)
return { mapId = map.id, width = map.widthCells,
height = map.heightCells, rows = rows, tileRows = tileRows,
height = map.heightCells, rows = rows, markers = markers,
tileRows = tileRows,
tileWidth = tileRows and map.widthCells * 2,
tileHeight = tileRows and map.heightCells * 2 }
tileHeight = tileRows and map.heightCells * 2,
tileDetailRows = tileDetailRows,
tileDetailWidth = tileDetailRows and map.widthCells * 4,
tileDetailHeight = tileDetailRows and map.heightCells * 4 }
end
-- opts.arrive = "fly" | "teleport" picks the arrival FX; anything else
+43 -7
View File
@@ -5,8 +5,9 @@ local Assets = require("src.render.Assets")
local WorldAPI = require("src.world.WorldAPI")
Assets.imageData = function()
return { getPixel = function(_, x)
local shade = x < 8 and 1 or 0
return { getPixel = function(_, x, y)
local shade = x >= 8 and 0 or ({ 1, 0, 2 / 3, 1 / 3 })[
math.floor(y / 4) * 2 + math.floor(x / 4) + 1]
return shade, shade, shade, 1
end }
end
@@ -16,15 +17,34 @@ local overview, err = api:mapOverview()
T.eq(overview, nil, "map overview is unavailable outside the overworld")
T.eq(err, "no overworld", "map overview reports why it is unavailable")
local map = { id = "TEST_MAP", widthCells = 2, heightCells = 2 }
local map = {
id = "TEST_MAP", widthCells = 2, heightCells = 2,
def = {
warps = { { x = 1, y = 0 } },
objects = { { index = 1, x = 0, y = 1, item = "POTION" } },
},
}
function map:isWarpTileCell(x, y) return x == 1 and y == 0 end
function map:isWaterCell(x, y) return x == 0 and y == 1 end
function map:isWalkableCell(x, y) return x == 0 and y == 0 end
function map:tileAt(x) return x % 2 end
api = WorldAPI.new({ stack = { states = {
{ isOverworld = true, map = map },
} } }, "tester")
local save = {}
local world = {
isOverworld = true,
map = map,
objectVisible = function(s, mapId, obj)
return not (s.itemsTaken and s.itemsTaken[mapId .. "_obj_" .. obj.index])
end,
}
local game = {
save = save,
data = { field = { hiddenItems = {
TEST_MAP = { { x = 1, y = 1, item = "NUGGET" } },
} } },
stack = { states = { world } },
}
api = WorldAPI.new(game, "tester")
overview = api:mapOverview()
T.eq(overview.mapId, "TEST_MAP", "map overview identifies the active map")
T.eq(overview.width, 2, "map overview reports its width")
@@ -32,11 +52,27 @@ T.eq(overview.height, 2, "map overview reports its height")
T.eq(overview.rows[1], ".+", "walkable land and warps are distinct")
T.eq(overview.rows[2], "~ ", "water and blocked terrain are distinct")
T.eq(overview.tileRows, nil, "tile overview is optional")
T.eq(#overview.markers, 3, "active exits and untaken items are marked")
T.eq(overview.markers[1].kind, "warp", "warp marker is semantic")
T.eq(overview.markers[2].kind, "item", "visible item marker is semantic")
T.eq(overview.markers[3].kind, "hidden", "hidden item marker is semantic")
save.itemsTaken = { TEST_MAP_obj_1 = true }
save.hiddenTaken = { TEST_MAP_1_1 = true }
overview = api:mapOverview()
T.eq(#overview.markers, 1, "collected items disappear from the overview")
T.eq(overview.markers[1].kind, "warp", "exits remain after collecting items")
map.tileset = { image = "test.png", tilesPerRow = 2 }
overview = api:mapOverview()
T.eq(overview.tileWidth, 4, "tile overview reports its width")
T.eq(overview.tileHeight, 4, "tile overview reports its height")
T.eq(overview.tileRows[1], "0303", "tile overview preserves map shading")
T.eq(overview.tileRows[1], "2323", "tile overview preserves average shading")
T.eq(overview.tileDetailWidth, 8, "detail overview reports its width")
T.eq(overview.tileDetailHeight, 8, "detail overview reports its height")
T.eq(overview.tileDetailRows[1], "03330333",
"detail overview preserves top tile quadrants")
T.eq(overview.tileDetailRows[2], "12331233",
"detail overview preserves bottom tile quadrants")
T.finish("world map overview")