mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 20:50:21 +02:00
feat(mods): expose map overviews in Gold
This commit is contained in:
@@ -474,6 +474,8 @@ Generation-agnostic; nothing to adapt.
|
|||||||
(`src/world/gen2/WorldAPI.lua`). Two differences show through and are
|
(`src/world/gen2/WorldAPI.lua`). Two differences show through and are
|
||||||
documented on the module: Gold's world is not a stack state, and Gen 2 event
|
documented on the module: Gold's world is not a stack state, and Gen 2 event
|
||||||
flags are numeric ids into `wEventFlags` rather than string keys.
|
flags are numeric ids into `wEventFlags` rather than string keys.
|
||||||
|
`mapOverview` returns the same read-only terrain, tile-shading, and marker
|
||||||
|
shape, using Gold's live object masks and event flags to omit collected items.
|
||||||
`spawnNpc` / `removeNpc` append onto the map def's own object list, the way the
|
`spawnNpc` / `removeNpc` append onto the map def's own object list, the way the
|
||||||
Gen 1 arm does, so a spawned actor is pooled, drawn, walked and talked to like
|
Gen 1 arm does, so a spawned actor is pooled, drawn, walked and talked to like
|
||||||
an extracted one and survives a map reload; it is not serialized, so a mod
|
an extracted one and survives a map reload; it is not serialized, so a mod
|
||||||
|
|||||||
+2
-1
@@ -66,7 +66,8 @@ optional visual `tileRows` at 2x resolution, and optional `tileDetailRows` at
|
|||||||
`"3"` (darkest); their matching width and height fields describe the grid.
|
`"3"` (darkest); their matching width and height fields describe the grid.
|
||||||
`markers` contains active `{ kind, x, y }` points in map-cell coordinates for
|
`markers` contains active `{ kind, x, y }` points in map-cell coordinates for
|
||||||
`warp`, visible `item`, and untaken `hidden` locations. All fields are
|
`warp`, visible `item`, and untaken `hidden` locations. All fields are
|
||||||
read-only snapshots; mods choose which layers to render.
|
read-only snapshots; mods choose which layers to render. Red and Gold expose
|
||||||
|
the same contract while applying their own object and event visibility rules.
|
||||||
|
|
||||||
## Party ordering
|
## Party ordering
|
||||||
|
|
||||||
|
|||||||
@@ -736,7 +736,7 @@ COVERAGE["src.pokemon.Boxes"] = {
|
|||||||
|
|
||||||
COVERAGE["src.world.WorldAPI"] = {
|
COVERAGE["src.world.WorldAPI"] = {
|
||||||
kind = "alias", target = "src.world.gen2.WorldAPI",
|
kind = "alias", target = "src.world.gen2.WorldAPI",
|
||||||
backed = "new __index overworld current warpTo toggleObject replaceBlock "
|
backed = "new __index overworld current mapOverview warpTo toggleObject replaceBlock "
|
||||||
.. "spawnNpc removeNpc npc queueScript invalidateMap",
|
.. "spawnNpc removeNpc npc queueScript invalidateMap",
|
||||||
warned = "setFlag getFlag",
|
warned = "setFlag getFlag",
|
||||||
absent = "",
|
absent = "",
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
-- Generation-neutral minimap rasterization. WorldAPI arms own semantic
|
||||||
|
-- markers because object/event visibility differs; terrain and tile shading
|
||||||
|
-- share one output contract.
|
||||||
|
|
||||||
|
local Assets = require("src.render.Assets")
|
||||||
|
|
||||||
|
local MapOverview = {}
|
||||||
|
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 tileRows(map)
|
||||||
|
local tileset = map.tileset
|
||||||
|
if not (tileset and tileset.image and tileset.tilesPerRow) then return nil end
|
||||||
|
local cached = overviewShades[tileset.image]
|
||||||
|
if not cached then
|
||||||
|
local ok, pixels = pcall(Assets.imageData, tileset.image)
|
||||||
|
if not ok then return nil end
|
||||||
|
cached = { pixels = pixels, shades = {} }
|
||||||
|
overviewShades[tileset.image] = cached
|
||||||
|
end
|
||||||
|
local rows, detailRows, perRow = {}, {}, tileset.tilesPerRow
|
||||||
|
for ty = 0, map.heightCells * 2 - 1 do
|
||||||
|
local row, detailTop, detailBottom = {}, {}, {}
|
||||||
|
for tx = 0, map.widthCells * 2 - 1 do
|
||||||
|
local tile = map:tileAt(tx, ty)
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
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] = 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, detailRows
|
||||||
|
end
|
||||||
|
|
||||||
|
function MapOverview.build(map, markers)
|
||||||
|
local rows = {}
|
||||||
|
for y = 0, map.heightCells - 1 do
|
||||||
|
local row = {}
|
||||||
|
for x = 0, map.widthCells - 1 do
|
||||||
|
row[#row + 1] = map:isWarpTileCell(x, y) and "+"
|
||||||
|
or map:isWaterCell(x, y) and "~"
|
||||||
|
or map:isWalkableCell(x, y) and "." or " "
|
||||||
|
end
|
||||||
|
rows[#rows + 1] = table.concat(row)
|
||||||
|
end
|
||||||
|
local tiles, detail = tileRows(map)
|
||||||
|
return { mapId = map.id, width = map.widthCells,
|
||||||
|
height = map.heightCells, rows = rows, markers = markers,
|
||||||
|
tileRows = tiles,
|
||||||
|
tileWidth = tiles and map.widthCells * 2,
|
||||||
|
tileHeight = tiles and map.heightCells * 2,
|
||||||
|
tileDetailRows = detail,
|
||||||
|
tileDetailWidth = detail and map.widthCells * 4,
|
||||||
|
tileDetailHeight = detail and map.heightCells * 4 }
|
||||||
|
end
|
||||||
|
|
||||||
|
return MapOverview
|
||||||
+3
-73
@@ -6,8 +6,8 @@
|
|||||||
-- stays unsupported; anything a mod legitimately needs belongs here.
|
-- stays unsupported; anything a mod legitimately needs belongs here.
|
||||||
|
|
||||||
local Logger = require("src.core.Logger")
|
local Logger = require("src.core.Logger")
|
||||||
local Assets = require("src.render.Assets")
|
|
||||||
local MapLoader = require("src.world.MapLoader")
|
local MapLoader = require("src.world.MapLoader")
|
||||||
|
local MapOverview = require("src.world.MapOverview")
|
||||||
local Party = require("src.pokemon.Party")
|
local Party = require("src.pokemon.Party")
|
||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
|
||||||
@@ -15,59 +15,6 @@ local WorldAPI = {}
|
|||||||
WorldAPI.__index = WorldAPI
|
WorldAPI.__index = WorldAPI
|
||||||
|
|
||||||
local NO_OVERWORLD = "no overworld"
|
local NO_OVERWORLD = "no overworld"
|
||||||
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
|
|
||||||
local cached = overviewShades[tileset.image]
|
|
||||||
if not cached then
|
|
||||||
local ok, pixels = pcall(Assets.imageData, tileset.image)
|
|
||||||
if not ok then return nil end
|
|
||||||
cached = { pixels = pixels, shades = {} }
|
|
||||||
overviewShades[tileset.image] = cached
|
|
||||||
end
|
|
||||||
local rows, detailRows, perRow = {}, {}, tileset.tilesPerRow
|
|
||||||
for ty = 0, map.heightCells * 2 - 1 do
|
|
||||||
local row, detailTop, detailBottom = {}, {}, {}
|
|
||||||
for tx = 0, map.widthCells * 2 - 1 do
|
|
||||||
local tile = map:tileAt(tx, ty)
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
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] = 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, detailRows
|
|
||||||
end
|
|
||||||
|
|
||||||
local function acceptsMenuInput(game, ow)
|
local function acceptsMenuInput(game, ow)
|
||||||
local stack = game and game.stack
|
local stack = game and game.stack
|
||||||
@@ -147,16 +94,7 @@ end
|
|||||||
function WorldAPI:mapOverview()
|
function WorldAPI:mapOverview()
|
||||||
local ow = self:overworld()
|
local ow = self:overworld()
|
||||||
if not ow or not ow.map then return nil, NO_OVERWORLD end
|
if not ow or not ow.map then return nil, NO_OVERWORLD end
|
||||||
local map, rows, markers = ow.map, {}, {}
|
local map, markers = ow.map, {}
|
||||||
for y = 0, map.heightCells - 1 do
|
|
||||||
local row = {}
|
|
||||||
for x = 0, map.widthCells - 1 do
|
|
||||||
row[#row + 1] = map:isWarpTileCell(x, y) and "+"
|
|
||||||
or map:isWaterCell(x, y) and "~"
|
|
||||||
or map:isWalkableCell(x, y) and "." or " "
|
|
||||||
end
|
|
||||||
rows[#rows + 1] = table.concat(row)
|
|
||||||
end
|
|
||||||
local def = map.def or {}
|
local def = map.def or {}
|
||||||
for _, warp in ipairs(def.warps or {}) do
|
for _, warp in ipairs(def.warps or {}) do
|
||||||
markers[#markers + 1] = { kind = "warp", x = warp.x, y = warp.y }
|
markers[#markers + 1] = { kind = "warp", x = warp.x, y = warp.y }
|
||||||
@@ -175,15 +113,7 @@ function WorldAPI:mapOverview()
|
|||||||
markers[#markers + 1] = { kind = "hidden", x = item.x, y = item.y }
|
markers[#markers + 1] = { kind = "hidden", x = item.x, y = item.y }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local tileRows, tileDetailRows = mapTileRows(map)
|
return MapOverview.build(map, markers)
|
||||||
return { mapId = map.id, width = map.widthCells,
|
|
||||||
height = map.heightCells, rows = rows, markers = markers,
|
|
||||||
tileRows = tileRows,
|
|
||||||
tileWidth = tileRows and map.widthCells * 2,
|
|
||||||
tileHeight = tileRows and map.heightCells * 2,
|
|
||||||
tileDetailRows = tileDetailRows,
|
|
||||||
tileDetailWidth = tileDetailRows and map.widthCells * 4,
|
|
||||||
tileDetailHeight = tileDetailRows and map.heightCells * 4 }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- opts.arrive = "fly" | "teleport" picks the arrival FX; anything else
|
-- opts.arrive = "fly" | "teleport" picks the arrival FX; anything else
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
local Logger = require("src.core.Logger")
|
local Logger = require("src.core.Logger")
|
||||||
local Movement = require("src.script.gen2.Movement")
|
local Movement = require("src.script.gen2.Movement")
|
||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
local HiddenItems = require("src.world.gen2.HiddenItems")
|
||||||
|
local MapOverview = require("src.world.MapOverview")
|
||||||
|
|
||||||
local WorldAPI = {}
|
local WorldAPI = {}
|
||||||
WorldAPI.__index = WorldAPI
|
WorldAPI.__index = WorldAPI
|
||||||
@@ -50,6 +52,31 @@ function WorldAPI:current()
|
|||||||
facing = p and p.facing }
|
facing = p and p.facing }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The same read-only minimap contract as Gen 1, with Gold's object/event
|
||||||
|
-- visibility rules supplying the semantic markers.
|
||||||
|
function WorldAPI:mapOverview()
|
||||||
|
local world = self:overworld()
|
||||||
|
if not world or not world.map then return nil, NO_OVERWORLD end
|
||||||
|
local map, def, markers = world.map, world.map.def or {}, {}
|
||||||
|
for _, warp in ipairs(def.warps or {}) do
|
||||||
|
markers[#markers + 1] = { kind = "warp", x = warp.x, y = warp.y }
|
||||||
|
end
|
||||||
|
local visible = {}
|
||||||
|
for _, npc in ipairs(world.npcs or {}) do
|
||||||
|
if npc.def then visible[npc.def] = true end
|
||||||
|
end
|
||||||
|
for _, obj in ipairs(def.objects or {}) do
|
||||||
|
local item = obj.itemball and obj.itemball.item
|
||||||
|
if item and item ~= "0" and item ~= 0 and visible[obj] then
|
||||||
|
markers[#markers + 1] = { kind = "item", x = obj.x, y = obj.y }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, item in ipairs(HiddenItems.unfound(def, world.events)) do
|
||||||
|
markers[#markers + 1] = { kind = "hidden", x = item.x, y = item.y }
|
||||||
|
end
|
||||||
|
return MapOverview.build(map, markers)
|
||||||
|
end
|
||||||
|
|
||||||
-- opts is accepted for signature parity with the Gen 1 arm; Gold's arrival FX
|
-- opts is accepted for signature parity with the Gen 1 arm; Gold's arrival FX
|
||||||
-- come from the map setup method, so opts.arrive has nothing to select yet.
|
-- come from the map setup method, so opts.arrive has nothing to select yet.
|
||||||
function WorldAPI:warpTo(mapId, x, y, facing, opts)
|
function WorldAPI:warpTo(mapId, x, y, facing, opts)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package.path = "./?.lua;./?/init.lua;" .. package.path
|
|||||||
local T = require("tests.harness")
|
local T = require("tests.harness")
|
||||||
local Assets = require("src.render.Assets")
|
local Assets = require("src.render.Assets")
|
||||||
local WorldAPI = require("src.world.WorldAPI")
|
local WorldAPI = require("src.world.WorldAPI")
|
||||||
|
local WorldAPI2 = require("src.world.gen2.WorldAPI")
|
||||||
|
|
||||||
Assets.imageData = function()
|
Assets.imageData = function()
|
||||||
return { getPixel = function(_, x, y)
|
return { getPixel = function(_, x, y)
|
||||||
@@ -17,6 +18,10 @@ local overview, err = api:mapOverview()
|
|||||||
T.eq(overview, nil, "map overview is unavailable outside the overworld")
|
T.eq(overview, nil, "map overview is unavailable outside the overworld")
|
||||||
T.eq(err, "no overworld", "map overview reports why it is unavailable")
|
T.eq(err, "no overworld", "map overview reports why it is unavailable")
|
||||||
|
|
||||||
|
local overview2, err2 = WorldAPI2.new({}, "tester"):mapOverview()
|
||||||
|
T.eq(overview2, nil, "Gen 2 map overview is unavailable outside the overworld")
|
||||||
|
T.eq(err2, "no overworld", "Gen 2 map overview reports why it is unavailable")
|
||||||
|
|
||||||
local map = {
|
local map = {
|
||||||
id = "TEST_MAP", widthCells = 2, heightCells = 2,
|
id = "TEST_MAP", widthCells = 2, heightCells = 2,
|
||||||
def = {
|
def = {
|
||||||
@@ -63,6 +68,48 @@ overview = api:mapOverview()
|
|||||||
T.eq(#overview.markers, 1, "collected items disappear from the overview")
|
T.eq(#overview.markers, 1, "collected items disappear from the overview")
|
||||||
T.eq(overview.markers[1].kind, "warp", "exits remain after collecting items")
|
T.eq(overview.markers[1].kind, "warp", "exits remain after collecting items")
|
||||||
|
|
||||||
|
local ball = { x = 0, y = 1, itemball = { item = 15, quantity = 1 } }
|
||||||
|
local gen2Map = {
|
||||||
|
id = "GEN2_MAP", widthCells = 2, heightCells = 2,
|
||||||
|
tileset = { image = "test.png", tilesPerRow = 2 },
|
||||||
|
def = {
|
||||||
|
warps = { { x = 1, y = 0 } },
|
||||||
|
objects = { ball },
|
||||||
|
bgEvents = { {
|
||||||
|
x = 1, y = 1, kind = 7,
|
||||||
|
hiddenItem = { item = 30, event = 123 },
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
function gen2Map:isWarpTileCell(x, y) return x == 1 and y == 0 end
|
||||||
|
function gen2Map:isWaterCell(x, y) return x == 0 and y == 1 end
|
||||||
|
function gen2Map:isWalkableCell(x, y) return x == 0 and y == 0 end
|
||||||
|
function gen2Map:tileAt(x) return x % 2 end
|
||||||
|
local found = {}
|
||||||
|
local gen2World = {
|
||||||
|
isOverworld = true,
|
||||||
|
map = gen2Map,
|
||||||
|
npcs = { {}, { def = ball } },
|
||||||
|
events = { get = function(_, event) return found[event] end },
|
||||||
|
}
|
||||||
|
api = WorldAPI2.new({ save = {}, data = {}, world = gen2World }, "tester")
|
||||||
|
overview = api:mapOverview()
|
||||||
|
T.eq(overview.rows[1], ".+", "Gen 2 uses the shared terrain contract")
|
||||||
|
T.eq(overview.rows[2], "~ ", "Gen 2 water and blocked terrain are distinct")
|
||||||
|
T.eq(overview.tileDetailRows[1], "03330333",
|
||||||
|
"Gen 2 exposes the same 4x4 tile shading detail")
|
||||||
|
T.eq(#overview.markers, 3,
|
||||||
|
"Gen 2 exits, visible item balls, and hidden items are marked")
|
||||||
|
T.eq(overview.markers[2].kind, "item", "Gen 2 item balls are semantic")
|
||||||
|
T.eq(overview.markers[3].kind, "hidden", "Gen 2 hidden items are semantic")
|
||||||
|
|
||||||
|
gen2World.npcs = {}
|
||||||
|
found[123] = true
|
||||||
|
overview = api:mapOverview()
|
||||||
|
T.eq(#overview.markers, 1,
|
||||||
|
"collected Gen 2 items disappear from the overview")
|
||||||
|
|
||||||
|
api = WorldAPI.new(game, "tester")
|
||||||
map.tileset = { image = "test.png", tilesPerRow = 2 }
|
map.tileset = { image = "test.png", tilesPerRow = 2 }
|
||||||
overview = api:mapOverview()
|
overview = api:mapOverview()
|
||||||
T.eq(overview.tileWidth, 4, "tile overview reports its width")
|
T.eq(overview.tileWidth, 4, "tile overview reports its width")
|
||||||
|
|||||||
Reference in New Issue
Block a user