Compare commits

...

5 Commits

Author SHA1 Message Date
bryanthaboi 8e4d5b6807 Merge pull request #1239 from bryanthaboi/dev
something for avg consumer and something for everyone else
2026-08-13 16:40:04 -04:00
bryanthaboi 26e9e1d597 Merge pull request #1238 from AverageConsumer/codex/gen2-map-overview-parity
feat(mods): expose map overviews in Gold
2026-08-13 16:30:47 -04:00
bryanthaboi f7f32d2786 scroll list 2026-08-13 16:29:59 -04:00
AverageConsumer c5192eaea4 feat(mods): expose map overviews in Gold 2026-08-13 22:08:00 +02:00
github-actions c2906744ce chore(ios): update app-repo.json [skip ci] 2026-08-13 15:53:20 -04:00
10 changed files with 220 additions and 89 deletions
+2
View File
@@ -474,6 +474,8 @@ Generation-agnostic; nothing to adapt.
(`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
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
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
+2 -1
View File
@@ -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.
`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.
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
+7
View File
@@ -12,6 +12,13 @@
"tintColor": "3b5ca8",
"category": "games",
"versions": [
{
"version": "0.1.82",
"date": "2026-08-13",
"size": 11281277,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.82/gen1recomp++-0.1.82-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #914 Immunity steps and no encounter tiles\n- #936 Fly and teleporting allow should block interactions\n- #944 When starting a new game, the character sprite is facing down instead of up\n- #949 Save game import, save game export, \"normal\" save game\n- #964 Menu closing when trying to view Pokemon before picking starter\n- #978 Following up of #847\n- #983 Pikachu incorrectly able to be released\n- #984 EXP calculation after trading a PKMN back isnt right.\n- #987 Celadon Game Corner Display Error (L22680)\n- #988 (Yellow) Missing Pikachu Interaction - Pewter City Pokemon Centre\n- #989 (Yellow) Missing Pikachu Interaction - Vermilion City Pokemon Fan Club\n- #990 (Yellow) Pikachu Missing Hat\n- #999 Pallet Town music while player follows Prof. Oak\n- #1007 Transition into battle state for the 'Catching Pikachu' scene missing\n- #1010 Rival's position resets\n- #1011 Rival dialogue issue\n- #1029 1px black line sits under the title logo at the SGB zone boundary\n- #1035 Alt Music not playing\n- #1041 Mimic text not swoing up\n- #1047 Using a TM or HM closes the menu afterwards.\n- #1048 Cannot select items from the sell menu.\n- #1056 Cursor not jumping when switching moves\n- #1065 Player walks through statue after final battle while walking into Hall Of Fame\n- #1066 Anomalies when opponent switches out Pokemon\n- #1069 missing {RAM:wNameBuffer}\n- #1073 _GotMonText not used\n- #1089 Scripted movement missing for player and rival\n- #1101 Recoil damage calculation bug\n- #1115 Hitting attack does no damage\n- #1119 Wrongs textlines and events\n- #1120 Health is black when viewing stats\n- #1129 Errors in bowing animations of the nurse\n- #1146 [Recomp] Map music glitch\n- #1149 issue that affects tranlations\n- #1175 Pikachu visible too late\n- #1189 Town Map Item\n- #1193 Status moves have (wrong) sound effects when battle animations are turned off\n- #1206 Gen1: Cinnebar island has unfishable tiles north and east\n- #1207 Poison Flicker Visual issue\n- #1214 Title Screen with OG Red is the wrong color\n- #1219 [GOLD] UNABLE TO USE SUN STONE\n- #1220 No screen shaking for hitting moves when battle animations are off\n- #1221 [Gold] Flaafy evolution to at Lvl 30 Ampharos skipped learn thunder punch\n- #1225 Jingles for learning moves missing\n\n## Contributors\n\n- @1Jamie\n- @bryanthaboi"
},
{
"version": "0.1.81",
"date": "2026-08-13",
+45 -12
View File
@@ -14,10 +14,9 @@
-- height, and flex-shrink compressing text until it overlapped.
--
-- THE RULES THIS FILE FOLLOWS:
-- * NO SCROLLING. Every list paginates (Kit.pager). Rows per page come
-- from the real viewport height, so a tall window shows more and a phone
-- shows fewer -- but a page's row count is bounded either way, which is
-- what makes a 500-mod index cost the same as a 10-mod one.
-- * Lists paginate (Kit.pager). The installed-mod list also scrolls inside
-- its viewport, so each of its pages can hold at least ten entries without
-- requiring a tall window. Pages still bound how many mod rows we visit.
-- * Every click handler only QUEUES work (imp._uiActions); update() drains
-- the queue, so an action that tears the view down (Play, Edit save)
-- never runs inside the frame that dispatched it.
@@ -46,9 +45,17 @@ local COMMUNITY_URL = "https://bois.icu"
local ACT_DEDUP = 0.35
-- Finger travel past this (px) is a drag, not a tap.
local TAP_SLOP2 = 16 * 16
-- Installed mods should not turn into a one- or two-item pager on a compact
-- display. Keep a useful page size, then let the list viewport scroll.
local MIN_MODS_PER_PAGE = 10
local function clamp(v, lo, hi) return math.max(lo, math.min(hi, v)) end
local function inRect(rect, x, y)
return rect ~= nil and x >= rect.x and x <= rect.x + rect.w
and y >= rect.y and y <= rect.y + rect.h
end
-- ------------------------------------------------------------- lifecycle
local function ensureState(imp)
@@ -146,7 +153,10 @@ end
function LauncherView.touchpressed(imp, id, x, y)
if not imp._flex then return end
imp._touchAt = imp._touchAt or {}
imp._touchAt[tostring(id)] = { x = x, y = y }
imp._touchAt[tostring(id)] = {
x = x, y = y,
modsList = (imp._modScrollMax or 0) > 0 and inRect(imp._modListRect, x, y),
}
end
function LauncherView.touchmoved(imp, id, x, y)
@@ -157,8 +167,14 @@ function LauncherView.touchmoved(imp, id, x, y)
if ddx * ddx + ddy * ddy > TAP_SLOP2 then
start.dragged = true
end
-- Short-window mode: a vertical drag scrolls the page (draw() clamps).
if start.dragged and (imp._pageScrollMax or 0) > 0 then
-- A drag that began in the installed-mod viewport scrolls that page's
-- rows. Its pager remains available for moving to the next ten-plus
-- entries; a drag elsewhere keeps the normal short-window page scroll.
if start.dragged and start.modsList then
local last = start.lastY or start.y
imp.modScroll = clamp((imp.modScroll or 0) - (y - last), 0,
imp._modScrollMax or 0)
elseif start.dragged and (imp._pageScrollMax or 0) > 0 then
local last = start.lastY or start.y
imp._pageScroll = (imp._pageScroll or 0) - (y - last)
end
@@ -1441,6 +1457,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
cy = cy + buildModScopeRow(imp, x, cy, w, m)
if #mods == 0 then
imp.modScroll, imp._modScrollMax, imp._modListRect = 0, 0, nil
Kit.emptyBox(x, cy, w, math.floor(110 * m.s), imp:_modsEmptyHint())
return
end
@@ -1492,16 +1509,30 @@ local function buildModsPanel(imp, x, y, w, availH, m)
+ math.floor(8 * m.s)
local pagerH = math.max(Kit.tapMin(), math.floor(30 * m.s))
local listH = availH - (cy - y) - pagerH - gap
local perPage = Kit.rowsThatFit(listH, rowH, gap, 1, 20)
local perPage = Kit.rowsThatFit(listH, rowH, gap, MIN_MODS_PER_PAGE, 20)
local first, last, cur, pages = Kit.pageBounds(page(imp, "mods"), #mods, perPage)
setPage(imp, "mods", cur)
local listTop = cy
setPage(imp, "mods",
Kit.wheelPage(x, listTop, w, listH, cur, #mods, perPage))
local shown = math.max(0, last - first + 1)
local contentH = shown * rowH + math.max(0, shown - 1) * gap
local scrollMax = math.max(0, contentH - listH)
local scroll = clamp(imp.modScroll or 0, 0, scrollMax)
imp._modListRect = { x = x, y = listTop, w = w, h = listH }
imp._modScrollMax = scrollMax
if scrollMax > 0 and (Kit.wheelY or 0) ~= 0 and Kit.hit(x, listTop, w, listH) then
scroll = clamp(scroll - Kit.wheelY * math.floor(48 * m.s), 0, scrollMax)
Kit.wheelY = 0
elseif scrollMax == 0 then
local wheelPage = Kit.wheelPage(x, listTop, w, listH, cur, #mods, perPage)
if wheelPage ~= cur then imp.modScroll = 0 end
setPage(imp, "mods", wheelPage)
end
imp.modScroll = scroll
Kit.pushClip(x, listTop, w, listH)
for i = first, last do
local mod = mods[i]
local ry = listTop + (i - first) * (rowH + gap)
local ry = listTop + (i - first) * (rowH + gap) - scroll
local rowKey = "mod-row-" .. mod.id
-- The whole row is the control: it opens the per-mod actions popup
-- (update / versions / delete moved there). Only the enable toggle
@@ -1600,9 +1631,11 @@ local function buildModsPanel(imp, x, y, w, availH, m)
px, ly, PAL.detail)
end
end
Kit.popClip()
local pagerY = listTop + (last - first + 1) * (rowH + gap)
local pagerY = listTop + listH + gap
local newPage = Kit.pager(x, pagerY, w, cur, #mods, perPage, "mods")
if newPage ~= cur then imp.modScroll = 0 end
setPage(imp, "mods", newPage)
end
+2 -2
View File
@@ -1228,8 +1228,8 @@ function RomImporter.new(onComplete, opts)
saveNotice = {},
-- MODS panel state (pass 3): mods is the cached LauncherMods.list() array
-- (refreshed lazily on first draw and after any toggle/install/delete);
-- modScroll is the list scroll offset (px, clamped in draw); modNotice is
-- the last install/delete result { ok, text } shown as a line above the list.
-- modScroll is the current paged list's inner scroll offset (px, clamped
-- in draw); modNotice is the last install/delete result { ok, text }.
mods = nil, modScroll = 0, modNotice = nil,
-- Which game the MODS panel is answering for (a GameVersion id, nil =
-- every game). Rows resolve their enable-state and their "runs here"
+1 -1
View File
@@ -736,7 +736,7 @@ COVERAGE["src.pokemon.Boxes"] = {
COVERAGE["src.world.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",
warned = "setFlag getFlag",
absent = "",
+84
View File
@@ -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
View File
@@ -6,8 +6,8 @@
-- stays unsupported; anything a mod legitimately needs belongs here.
local Logger = require("src.core.Logger")
local Assets = require("src.render.Assets")
local MapLoader = require("src.world.MapLoader")
local MapOverview = require("src.world.MapOverview")
local Party = require("src.pokemon.Party")
local Runtime = require("src.mods.Runtime")
@@ -15,59 +15,6 @@ local WorldAPI = {}
WorldAPI.__index = WorldAPI
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 stack = game and game.stack
@@ -147,16 +94,7 @@ end
function WorldAPI:mapOverview()
local ow = self:overworld()
if not ow or not ow.map then return nil, NO_OVERWORLD end
local map, rows, 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 map, markers = ow.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 }
@@ -175,15 +113,7 @@ function WorldAPI:mapOverview()
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, 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 }
return MapOverview.build(map, markers)
end
-- opts.arrive = "fly" | "teleport" picks the arrival FX; anything else
+27
View File
@@ -25,6 +25,8 @@
local Logger = require("src.core.Logger")
local Movement = require("src.script.gen2.Movement")
local Runtime = require("src.mods.Runtime")
local HiddenItems = require("src.world.gen2.HiddenItems")
local MapOverview = require("src.world.MapOverview")
local WorldAPI = {}
WorldAPI.__index = WorldAPI
@@ -50,6 +52,31 @@ function WorldAPI:current()
facing = p and p.facing }
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
-- come from the map setup method, so opts.arrive has nothing to select yet.
function WorldAPI:warpTo(mapId, x, y, facing, opts)
+47
View File
@@ -3,6 +3,7 @@ package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local Assets = require("src.render.Assets")
local WorldAPI = require("src.world.WorldAPI")
local WorldAPI2 = require("src.world.gen2.WorldAPI")
Assets.imageData = function()
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(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 = {
id = "TEST_MAP", widthCells = 2, heightCells = 2,
def = {
@@ -63,6 +68,48 @@ 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")
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 }
overview = api:mapOverview()
T.eq(overview.tileWidth, 4, "tile overview reports its width")