mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-23 05:58:26 +02:00
@@ -1968,6 +1968,7 @@ function Game2:applyOptions()
|
||||
})
|
||||
require("src.core.VideoMode").applyOptions(options)
|
||||
require("src.core.FrameCap").applyOptions(options)
|
||||
require("src.world.gen2.BorderFill").applyOptions(options)
|
||||
local GBCFX = require("src.render.GBCFX")
|
||||
if GBCFX.applyOptions(options) and self.save then
|
||||
-- applyOptions returns true when it had to clear an unsupported level.
|
||||
|
||||
@@ -274,6 +274,9 @@ Save.DEFAULT_OPTIONS = {
|
||||
color = "gbc",
|
||||
videoMode = "windowed",
|
||||
fpsCap = 60,
|
||||
-- VOID FILL: fade | water | trees | black. fade is each map header's own
|
||||
-- border block with the dissolve across a boundary (#1418).
|
||||
voidFill = "fade",
|
||||
musicVol = 7, -- 0-7, like the GB's NR50 master volume
|
||||
sfxVol = 7, -- 0-7
|
||||
musicFilter = 0, -- low-pass steps, 0 = off
|
||||
|
||||
@@ -529,9 +529,9 @@ end
|
||||
-- Gold reads NONE of the rows above. Its OPTION screen writes a different
|
||||
-- set of names, several of which collide with Gen 1's at a different TYPE
|
||||
-- (battleStyle "SHIFT" vs "shift", textSpeed a label vs a frame delay), and
|
||||
-- its renderer has no battle layout, no SGB palette packs and no void fill --
|
||||
-- so a gear opened on the Gold tab used to offer a dozen controls that did
|
||||
-- nothing and hide the seven that the cart itself has.
|
||||
-- its renderer has no battle layout and no SGB palette packs -- so a gear
|
||||
-- opened on the Gold tab used to offer a dozen controls that did nothing
|
||||
-- and hide the seven that the cart itself has.
|
||||
--
|
||||
-- The block lives in options.lua under `gold`, which is exactly where
|
||||
-- src/core/gen2/Save.lua loadOptions reads it, so an edit here is live on the
|
||||
@@ -617,6 +617,21 @@ local function gen2Rows(opts, hooks)
|
||||
end)
|
||||
end
|
||||
|
||||
local okFill, BorderFill = pcall(require, "src.world.gen2.BorderFill")
|
||||
if okFill and BorderFill.VOID_FILLS then
|
||||
add(Strings("VOID FILL"),
|
||||
function() return BorderFill.voidFillLabel(opts.voidFill) end,
|
||||
function(dir)
|
||||
local modes = BorderFill.VOID_FILLS
|
||||
local cur, idx = opts.voidFill or "fade", 1
|
||||
for i, m in ipairs(modes) do
|
||||
if m == cur then idx = i break end
|
||||
end
|
||||
opts.voidFill = modes[wrapIndex(idx - 1 + dir, #modes) + 1]
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
-- Same #136 gate as the Gen 1 row and the in-game one.
|
||||
local okFx, GBCFX = pcall(require, "src.render.GBCFX")
|
||||
if okFx and GBCFX.isSupported() then
|
||||
|
||||
+51
-23
@@ -1626,13 +1626,17 @@ end
|
||||
-- The persisted sort choice both mod panels share. The chooser itself is a
|
||||
-- popup (buildSortModal); panels just read the current key and offer a
|
||||
-- "Sort" button, which is what freed the chip row's two lines of space.
|
||||
local function sortDefs()
|
||||
return {
|
||||
local function sortDefs(scope)
|
||||
local defs = {
|
||||
{ key = "name", label = Strings("Name") },
|
||||
{ key = "popularity", label = Strings("Popularity") },
|
||||
{ key = "release", label = Strings("Release date") },
|
||||
{ key = "updated", label = Strings("Last updated") },
|
||||
{ key = "popularity", label = Strings("Most downloaded") },
|
||||
}
|
||||
if scope == "find" then
|
||||
defs[#defs + 1] = { key = "trending", label = Strings("Trending") }
|
||||
end
|
||||
defs[#defs + 1] = { key = "release", label = Strings("Release date") }
|
||||
defs[#defs + 1] = { key = "updated", label = Strings("Last updated") }
|
||||
return defs
|
||||
end
|
||||
|
||||
-- Sorting is decorate-sort-undecorate: the key is computed once per entry
|
||||
@@ -1684,7 +1688,7 @@ local function sortCacheOk(cache, src, key, rev, pending)
|
||||
return pending and (Kit.time - (cache.at or 0)) < RESORT_DEBOUNCE
|
||||
end
|
||||
|
||||
local function currentSort(imp)
|
||||
local function currentSort(imp, scope)
|
||||
local sortKey = imp.modSort
|
||||
if sortKey == nil then
|
||||
local ok, opts = pcall(require("src.core.SaveData").loadOptions)
|
||||
@@ -1694,6 +1698,7 @@ local function currentSort(imp)
|
||||
sortKey = sortKey or "popularity"
|
||||
imp.modSort = sortKey
|
||||
end
|
||||
if sortKey == "trending" and scope ~= "find" then return "popularity" end
|
||||
return sortKey
|
||||
end
|
||||
|
||||
@@ -1760,7 +1765,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
action = function() imp:_syncModUpdateInfo(true) end })
|
||||
btn(imp, place(sortW), cy, sortW, bh, "mods-sort", Strings("Sort"), {
|
||||
font = "small",
|
||||
action = function() imp._sortPopup = true end })
|
||||
action = function() imp._sortPopup = "mods" end })
|
||||
elseif medReq <= w then
|
||||
-- Tier 2 (Medium / Compact): Surface Import, Updates, and Sort directly
|
||||
btn(imp, place(importW), cy, importW, bh, "mods-import", importLabel, {
|
||||
@@ -1771,7 +1776,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
action = function() imp:_syncModUpdateInfo(true) end })
|
||||
btn(imp, place(sortW), cy, sortW, bh, "mods-sort", Strings("Sort"), {
|
||||
font = "small",
|
||||
action = function() imp._sortPopup = true end })
|
||||
action = function() imp._sortPopup = "mods" end })
|
||||
btn(imp, place(moreW), cy, moreW, bh, "mods-more-actions", Strings("More..."), {
|
||||
font = "small",
|
||||
action = function() imp._modHeaderActionsPopup = true end })
|
||||
@@ -1787,7 +1792,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
action = function() imp:chooseMod() end })
|
||||
btn(imp, place(sortW), cy, sortW, bh, "mods-sort", Strings("Sort"), {
|
||||
font = "small",
|
||||
action = function() imp._sortPopup = true end })
|
||||
action = function() imp._sortPopup = "mods" end })
|
||||
btn(imp, place(moreW), cy, moreW, bh, "mods-more-actions", Strings("More..."), {
|
||||
font = "small",
|
||||
action = function() imp._modHeaderActionsPopup = true end })
|
||||
@@ -1819,7 +1824,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
||||
return
|
||||
end
|
||||
|
||||
local sortKey = currentSort(imp)
|
||||
local sortKey = currentSort(imp, "mods")
|
||||
|
||||
-- Immediate mode paints this panel every frame; re-sorting the whole list
|
||||
-- per frame (with lowercased-string allocations in the comparator) fed the
|
||||
@@ -2210,7 +2215,7 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
local sw = Kit.textWidth("small", Strings("Sort")) + math.floor(20 * m.s)
|
||||
btn(imp, place(sw), cy, sw, fieldH, "find-sort", Strings("Sort"), {
|
||||
font = "small",
|
||||
action = function() imp._sortPopup = true end })
|
||||
action = function() imp._sortPopup = "find" end })
|
||||
-- The Filter button carries its state: blue while a category is active,
|
||||
-- so a filtered-down list never reads as "the index shrank".
|
||||
local fw = Kit.textWidth("small", Strings("Filter")) + math.floor(20 * m.s)
|
||||
@@ -2230,7 +2235,7 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
return
|
||||
end
|
||||
|
||||
local sortKey = currentSort(imp)
|
||||
local sortKey = currentSort(imp, "find")
|
||||
|
||||
-- Same caching rule as the MODS tab: the comparator allocates, so only
|
||||
-- re-sort when the inputs actually change.
|
||||
@@ -2249,6 +2254,7 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
-- fetch for every entry in the index (see _findStatsCached).
|
||||
local stats = imp:_findStatsCached(entry)
|
||||
if sortKey == "popularity" then return stats and stats.total or -1 end
|
||||
if sortKey == "trending" then return stats and stats.recent or -1 end
|
||||
if sortKey == "release" then return stats and stats.first or "0000-00-00" end
|
||||
return stats and stats.latest or "0000-00-00"
|
||||
end,
|
||||
@@ -2336,13 +2342,14 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
bx, ly, PAL.heading)
|
||||
local by2 = ly + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
-- meta and stats on one line, the download count first (and green)
|
||||
-- because it is what the default Popularity sort is ordering by: a
|
||||
-- because it is what the default Most-downloaded sort is ordering by: a
|
||||
-- narrow window ellipsizes the tail, and the count must survive that.
|
||||
local stats = imp:_findStats(entry)
|
||||
local baseCol = note and PAL.green or PAL.detail
|
||||
local lead = "v" .. tostring(ModIndex.displayVersion(entry))
|
||||
if note then lead = lead .. " - " .. note end
|
||||
local dl = stats and ModUpdate.downloadsLine(stats.total) or nil
|
||||
local dl = stats and ModUpdate.downloadsShort(stats.total) or nil
|
||||
local hasCount = stats ~= nil and stats.total ~= nil
|
||||
local dates = stats and ModUpdate.datesLine(stats.first, stats.latest)
|
||||
or nil
|
||||
local rest = {}
|
||||
@@ -2352,11 +2359,13 @@ local function buildFindPanel(imp, x, y, w, availH, m)
|
||||
end
|
||||
if dates then
|
||||
rest[#rest + 1] = dates
|
||||
elseif not dl and (entry.summary or "") ~= "" then
|
||||
elseif not hasCount and (entry.summary or "") ~= "" then
|
||||
rest[#rest + 1] = entry.summary
|
||||
end
|
||||
local segs = { { lead, baseCol } }
|
||||
if dl then segs[#segs + 1] = { " - " .. dl, PAL.green } end
|
||||
if dl then
|
||||
segs[#segs + 1] = { " - " .. dl, hasCount and PAL.green or PAL.faint }
|
||||
end
|
||||
if #rest > 0 then
|
||||
segs[#segs + 1] = { " - " .. table.concat(rest, " - "), baseCol }
|
||||
end
|
||||
@@ -2944,7 +2953,7 @@ local function buildModHeaderActionsModal(imp, m)
|
||||
{ label = Strings("Check for updates"), action = function() imp:_syncModUpdateInfo(true) end },
|
||||
{ label = Strings("Enable all mods"), kind = "good", action = function() imp:_setAllMods(true) end },
|
||||
{ label = Strings("Disable all mods"), kind = "warn", action = function() imp:_setAllMods(false) end },
|
||||
{ label = Strings("Sort mods..."), action = function() imp._sortPopup = true end },
|
||||
{ label = Strings("Sort mods..."), action = function() imp._sortPopup = "mods" end },
|
||||
}
|
||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
+ #btns * (m.btnH + gap) + m.btnH + pad
|
||||
@@ -2972,7 +2981,8 @@ end
|
||||
-- Sort chooser, shared by the MODS and FIND MODS tabs (they share the
|
||||
-- persisted key, so one popup serves both).
|
||||
local function buildSortModal(imp, m)
|
||||
local defs = sortDefs()
|
||||
local scope = imp._sortPopup
|
||||
local defs = sortDefs(scope)
|
||||
local pad = math.floor(18 * m.s)
|
||||
local w = math.floor(360 * m.s)
|
||||
local gap = math.floor(8 * m.s)
|
||||
@@ -2982,7 +2992,7 @@ local function buildSortModal(imp, m)
|
||||
local cy = py + pad
|
||||
Kit.text("button", Strings("Sort by"), px + pad, cy, PAL.heading)
|
||||
cy = cy + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||
local cur = currentSort(imp)
|
||||
local cur = currentSort(imp, scope)
|
||||
for _, s in ipairs(defs) do
|
||||
local key = s.key
|
||||
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "sortpop-" .. key, s.label, {
|
||||
@@ -3362,25 +3372,43 @@ local function buildFindEntryModal(imp, m)
|
||||
local gap = math.floor(8 * m.s)
|
||||
local nBtns = 3 -- install row, details/source row, close row
|
||||
local noteH = note and (Kit.textHeight("small") + math.floor(4 * m.s)) or 0
|
||||
local stats = imp:_findStats(entry)
|
||||
local trend = {}
|
||||
local trendLine = stats and ModUpdate.trendingLine(stats.recent, stats.windowDays)
|
||||
if trendLine then trend[#trend + 1] = trendLine end
|
||||
if stats and stats.asOf then
|
||||
trend[#trend + 1] = Strings("counts approximate, as of %s",
|
||||
tostring(stats.asOf):match("^%d%d%d%d%-%d%d%-%d%d") or stats.asOf)
|
||||
end
|
||||
trend = (#trend > 0) and table.concat(trend, " - ") or nil
|
||||
local trendH = trend and (Kit.textHeight("small") + math.floor(2 * m.s)) or 0
|
||||
local h = pad + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
+ Kit.textHeight("small") + noteH + math.floor(12 * m.s)
|
||||
+ Kit.textHeight("small") + trendH + noteH + math.floor(12 * m.s)
|
||||
+ nBtns * (m.btnH + gap) - gap + pad
|
||||
local px, py, pw = modalPanel(m, w, h)
|
||||
local cy = py + pad
|
||||
Kit.text("button", Kit.ellipsize("button", entry.title or entry.id,
|
||||
pw - 2 * pad), px + pad, cy, PAL.heading)
|
||||
cy = cy + Kit.textHeight("button") + math.floor(4 * m.s)
|
||||
local stats = imp:_findStats(entry)
|
||||
local lead = "v" .. tostring(ModIndex.displayVersion(entry))
|
||||
if entry.author then lead = lead .. " - " .. entry.author end
|
||||
if entry.categories and entry.categories[1] then
|
||||
lead = lead .. " - " .. entry.categories[1]
|
||||
end
|
||||
local dl = stats and ModUpdate.downloadsLine(stats.total) or nil
|
||||
local dl = stats and ModUpdate.downloadsShort(stats.total) or nil
|
||||
local hasCount = stats ~= nil and stats.total ~= nil
|
||||
local segs = { { lead, PAL.detail } }
|
||||
if dl then segs[#segs + 1] = { " - " .. dl, PAL.green } end
|
||||
if dl then
|
||||
segs[#segs + 1] = { " - " .. dl, hasCount and PAL.green or PAL.faint }
|
||||
end
|
||||
segLine("small", segs, px + pad, cy, pw - 2 * pad)
|
||||
cy = cy + Kit.textHeight("small")
|
||||
if trend then
|
||||
cy = cy + math.floor(2 * m.s)
|
||||
Kit.text("small", Kit.ellipsize("small", trend, pw - 2 * pad),
|
||||
px + pad, cy, PAL.muted)
|
||||
cy = cy + Kit.textHeight("small")
|
||||
end
|
||||
if note then
|
||||
cy = cy + math.floor(4 * m.s)
|
||||
Kit.text("small", note, px + pad, cy, PAL.green)
|
||||
|
||||
@@ -4449,9 +4449,14 @@ function RomImporter:_findStatsCached(entry)
|
||||
end
|
||||
return nil -- retry window open; _requestFindStats decides what to do
|
||||
end
|
||||
if entry.downloads ~= nil or entry.first_release or entry.last_release then
|
||||
cached = { total = entry.downloads, first = entry.first_release,
|
||||
latest = entry.last_release, done = true }
|
||||
local ModIndex = require("src.mods.ModIndex")
|
||||
local dl = ModIndex.downloadStats(entry)
|
||||
local dates = ModIndex.releaseDates(entry)
|
||||
if dl or dates then
|
||||
cached = { total = dl and dl.total, recent = dl and dl.recent,
|
||||
windowDays = dl and dl.window_days, asOf = dl and dl.as_of,
|
||||
first = dates and dates.first, latest = dates and dates.latest,
|
||||
done = true }
|
||||
self._findStatsCache[entry.id] = cached
|
||||
return cached
|
||||
end
|
||||
|
||||
+42
-2
@@ -26,6 +26,7 @@ local ModIndex = {}
|
||||
-- single repo's releases; a whole index is heavier and changes more slowly.
|
||||
ModIndex.CACHE_TTL = 24 * 60 * 60
|
||||
ModIndex.SCHEMA_VERSION = 1
|
||||
ModIndex.CACHE_VERSION = 2
|
||||
|
||||
-- ------- pure: source resolution
|
||||
|
||||
@@ -149,6 +150,43 @@ local function parseLatest(raw)
|
||||
}
|
||||
end
|
||||
|
||||
local function parseDownloads(raw)
|
||||
if type(raw) == "number" or type(raw) == "string" then
|
||||
local n = tonumber(raw)
|
||||
return n and { total = n } or nil
|
||||
end
|
||||
if type(raw) ~= "table" then return nil end
|
||||
local out = {
|
||||
total = tonumber(raw.total),
|
||||
recent = tonumber(raw.recent),
|
||||
window_days = tonumber(raw.window_days),
|
||||
as_of = str(raw.as_of),
|
||||
}
|
||||
if out.total == nil and out.recent == nil then return nil end
|
||||
return out
|
||||
end
|
||||
|
||||
-- downloadStats(entry) -> { total, recent, window_days, as_of } | nil
|
||||
function ModIndex.downloadStats(entry)
|
||||
if type(entry) ~= "table" then return nil end
|
||||
return parseDownloads(entry.downloads)
|
||||
end
|
||||
|
||||
local function isoDay(v)
|
||||
if type(v) ~= "string" then return nil end
|
||||
return v:match("^(%d%d%d%d%-%d%d%-%d%d)")
|
||||
end
|
||||
|
||||
-- releaseDates(entry) -> { first, latest } | nil
|
||||
function ModIndex.releaseDates(entry)
|
||||
if type(entry) ~= "table" then return nil end
|
||||
local first = isoDay(entry.first_release)
|
||||
local latest = isoDay(entry.last_release)
|
||||
or isoDay(entry.latest and entry.latest.published_at)
|
||||
if not (first or latest) then return nil end
|
||||
return { first = first, latest = latest }
|
||||
end
|
||||
|
||||
local function parseEntry(raw)
|
||||
if type(raw) ~= "table" or not str(raw.id) then return nil end
|
||||
return {
|
||||
@@ -175,11 +213,11 @@ local function parseEntry(raw)
|
||||
conflicts = raw.conflicts,
|
||||
thumbnail = str(raw.thumbnail),
|
||||
description_url = str(raw.description_url),
|
||||
-- Optional release stats a feed author can publish: total downloads
|
||||
-- Optional release stats a feed author can publish: download counts
|
||||
-- across all releases plus first/last release dates. Additive-only,
|
||||
-- so a feed that carries them stays readable by every build that
|
||||
-- predates them (and one that does not still renders fine here).
|
||||
downloads = tonumber(raw.downloads),
|
||||
downloads = parseDownloads(raw.downloads),
|
||||
first_release = str(raw.first_release),
|
||||
last_release = str(raw.last_release),
|
||||
latest = parseLatest(raw.latest),
|
||||
@@ -516,6 +554,7 @@ function ModIndex.cacheFresh(entry, now, ttl)
|
||||
now = now or os.time()
|
||||
ttl = ttl or ModIndex.CACHE_TTL
|
||||
return entry ~= nil and type(entry.checkedAt) == "number"
|
||||
and entry.version == ModIndex.CACHE_VERSION
|
||||
and (now - entry.checkedAt) < ttl
|
||||
end
|
||||
|
||||
@@ -527,6 +566,7 @@ function ModIndex.writeCache(feed, index)
|
||||
opts.modIndexCache = opts.modIndexCache or {}
|
||||
opts.modIndexCache[feed] = {
|
||||
checkedAt = os.time(),
|
||||
version = ModIndex.CACHE_VERSION,
|
||||
generatedAt = index.generatedAt,
|
||||
categories = index.categories,
|
||||
mods = index.mods,
|
||||
|
||||
+34
-1
@@ -265,9 +265,42 @@ function ModUpdate.downloadsLine(total)
|
||||
ModUpdate.formatCount(total))
|
||||
end
|
||||
|
||||
ModUpdate.NO_COUNT = "?"
|
||||
|
||||
function ModUpdate.abbrevCount(n)
|
||||
n = tonumber(n)
|
||||
if not n or n ~= n or n < 0 then return ModUpdate.NO_COUNT end
|
||||
n = math.floor(n)
|
||||
if n < 1000 then return tostring(n) end
|
||||
local units, idx, value = { "k", "M", "B" }, 0, n
|
||||
repeat
|
||||
value = value / 1000
|
||||
idx = idx + 1
|
||||
until idx >= #units or math.floor(value * 10 + 0.5) / 10 < 1000
|
||||
local scaled = math.floor(value * 10 + 0.5) / 10
|
||||
local shown = (scaled % 1 == 0) and tostring(math.floor(scaled))
|
||||
or string.format("%.1f", scaled)
|
||||
return shown .. units[idx]
|
||||
end
|
||||
|
||||
function ModUpdate.downloadsShort(total)
|
||||
return Strings("%s downloads", ModUpdate.abbrevCount(total))
|
||||
end
|
||||
|
||||
function ModUpdate.trendingLine(recent, windowDays)
|
||||
if recent == nil then return nil end
|
||||
if not windowDays then
|
||||
return Strings("%s recently", ModUpdate.abbrevCount(recent))
|
||||
end
|
||||
return Strings("%s in the last %d days", ModUpdate.abbrevCount(recent),
|
||||
math.floor(windowDays))
|
||||
end
|
||||
|
||||
function ModUpdate.datesLine(first, latest)
|
||||
if not (first or latest) then return nil end
|
||||
return Strings("Released %s - Updated %s", first or "?", latest or "?")
|
||||
if not first then return Strings("Updated %s", latest) end
|
||||
if not latest then return Strings("Released %s", first) end
|
||||
return Strings("Released %s - Updated %s", first, latest)
|
||||
end
|
||||
|
||||
function ModUpdate.statsLine(total, first, latest)
|
||||
|
||||
+33
-16
@@ -24,14 +24,18 @@
|
||||
-- MoneyTopRightMenuHeader menu_coords 11, 0, 19, 2 -- a 9x3 box, and
|
||||
-- PlaceMoneyTextbox writes the amount at
|
||||
-- MenuBoxCoord2Tile + SCREEN_WIDTH + 1 = (12,1)
|
||||
-- MenuHeader_Buy menu_coords 1, 3, 19, 11 -- a 19x9 box holding 4
|
||||
-- entries of two rows. ScrollingMenu_UpdateDisplay
|
||||
-- starts at (2,4); PlaceMenuItemName prints the name
|
||||
-- there and .PrintBCDPrices is handed that origin
|
||||
-- plus the menu's own width (8) plus SCREEN_WIDTH, so
|
||||
-- the price lands at (10,5). The ▲ sits on the box's
|
||||
-- top right corner (19,3) and the ▼ on its bottom
|
||||
-- right (19,11).
|
||||
-- MenuHeader_Buy menu_coords 1, 3, 19, 11 -- a 19x9 rect holding 4
|
||||
-- entries of two rows. BuyMenuLoop copies that
|
||||
-- header and calls ScrollingMenu; it never calls
|
||||
-- InitScrollingMenu or MenuBox, so the rect is NOT
|
||||
-- a framed window. ScrollingMenu_UpdateDisplay
|
||||
-- calls ClearWholeMenuBox on it (spaces, no border)
|
||||
-- and starts printing at (2,4); PlaceMenuItemName
|
||||
-- prints the name there and .PrintBCDPrices is
|
||||
-- handed that origin plus the menu's own width (8)
|
||||
-- plus SCREEN_WIDTH, so the price lands at (10,5).
|
||||
-- The ▲ sits on the rect's top right (19,3) and
|
||||
-- the ▼ on its bottom right (19,11).
|
||||
-- UpdateItemDescription Textbox (0,12) interior 18x4 -- a 20x6 box -- with
|
||||
-- the description at (1,14)
|
||||
-- BuyItem_MenuHeader menu_coords 7, 15, 19, 17 -- a 13x3 box, and
|
||||
@@ -75,7 +79,9 @@ local SFX_TRANSACTION = "Sfx_Transaction"
|
||||
|
||||
local MartMenu = {}
|
||||
MartMenu.__index = MartMenu
|
||||
-- engine/items/mart.asm:54
|
||||
-- The top menu overlays the mart (StandardMart .HowMayIHelpYou /
|
||||
-- .AnythingElse: LoadStandardMenuHeader + PrintText). BuyMenu is
|
||||
-- FadeToMenu + BlankScreen, so enterBuy shadows this for the list.
|
||||
MartMenu.isOpaque = false
|
||||
|
||||
-- constants/mart_constants.asm. The `pokemart` macro emits this as one byte
|
||||
@@ -563,6 +569,10 @@ function MartMenu:enterBuy()
|
||||
self.phase = "buy"
|
||||
self.index = 1
|
||||
self.scroll = 0
|
||||
-- BuyMenu (engine/items/mart.asm): `call FadeToMenu / farcall BlankScreen`.
|
||||
-- BlankScreen fills the tilemap with spaces and the palettes with white, so
|
||||
-- the mart must not keep drawing in the letterbox under the list.
|
||||
self.isOpaque = (self.phase == "buy")
|
||||
end
|
||||
|
||||
function MartMenu:total()
|
||||
@@ -618,6 +628,8 @@ end
|
||||
-- BuyMenu returns into StandardMart .Buy, which falls through to
|
||||
-- .AnythingElse; the other three dialog kinds end on their come-again line.
|
||||
function MartMenu:leaveBuy()
|
||||
-- CloseSubmenu restores the map before .AnythingElse / the come-again line.
|
||||
self.isOpaque = nil
|
||||
if self.martType == "STANDARD" then
|
||||
self:enterTop(self.text.askMore)
|
||||
else
|
||||
@@ -885,11 +897,10 @@ function MartMenu:description()
|
||||
return def and def.description or nil
|
||||
end
|
||||
|
||||
-- The bottom visible entry's price row IS the box's bottom border row
|
||||
-- (entry row 10 + SCREEN_WIDTH = 11, MenuHeader_Buy's own last line), and a
|
||||
-- GB glyph REPLACES the tile it prints over, border and all. White under
|
||||
-- the seven money tiles first is that replacement; on the three rows clear
|
||||
-- of the border it repaints white over white.
|
||||
-- The bottom visible entry's price row is MenuHeader_Buy's last line
|
||||
-- (entry row 10 + SCREEN_WIDTH = 11). A GB glyph REPLACES the tile it
|
||||
-- prints over, so white under the seven money tiles first is that
|
||||
-- replacement. On a BlankScreen field it is white over white.
|
||||
local function printPriceOpaque(amount, ty)
|
||||
local G = love.graphics
|
||||
G.setColor(1, 1, 1, 1)
|
||||
@@ -899,8 +910,10 @@ local function printPriceOpaque(amount, ty)
|
||||
end
|
||||
|
||||
function MartMenu:drawBuyList()
|
||||
-- engine/items/mart.asm:542
|
||||
Chrome.box(LIST_BOX_X, LIST_BOX_Y, LIST_BOX_W, LIST_BOX_H)
|
||||
-- ScrollingMenu_UpdateDisplay (engine/menus/scrolling_menu.asm) calls
|
||||
-- ClearWholeMenuBox, not MenuBox: the MenuHeader_Buy rect is a cleared
|
||||
-- field, not a framed window. BlankScreen already filled the tilemap
|
||||
-- with spaces, so the names just land on white.
|
||||
for row = 1, VISIBLE_ROWS do
|
||||
local i = row + self.scroll
|
||||
local ty = LIST_Y + (row - 1) * LIST_SPACING
|
||||
@@ -960,6 +973,10 @@ function MartMenu:drawUnder()
|
||||
self:drawTopMenu()
|
||||
self:drawTextBox(self.topLines)
|
||||
elseif phase == "buy" or phase == "buyQuantity" then
|
||||
-- BlankScreen (engine/overworld/player_object.asm): the whole tilemap
|
||||
-- is spaces before PlaceMoneyTopRight / the scrolling list / the
|
||||
-- description textbox go down. Only those last two are framed.
|
||||
Chrome.clear()
|
||||
self:drawMoneyBox()
|
||||
self:drawBuyList()
|
||||
self:drawDescription()
|
||||
|
||||
@@ -134,6 +134,19 @@ local ROWS = {
|
||||
text = function(options)
|
||||
return require("src.render.Zoom").offsetLabel(options.zoom or 0)
|
||||
end },
|
||||
-- VOID FILL: FADE is each map's own border block with the dissolve across
|
||||
-- a boundary; WATER / TREES force one outdoor block; BLACK is a flat void.
|
||||
-- #1418. Same key the Gen 1 OPTION screen uses, different ladder (FADE
|
||||
-- is Gold's default because that is already what the maps call for).
|
||||
{ label = "VOID FILL", key = "voidFill", port = true,
|
||||
cycle = function(options, delta)
|
||||
local BorderFill = require("src.world.gen2.BorderFill")
|
||||
BorderFill.setVoidFill(options.voidFill or "fade")
|
||||
options.voidFill = BorderFill.cycle(delta)
|
||||
end,
|
||||
text = function(options)
|
||||
return require("src.world.gen2.BorderFill").voidFillLabel(options.voidFill)
|
||||
end },
|
||||
{ label = "TILT", key = "tilt", port = true,
|
||||
cycle = function(options, delta)
|
||||
local Tilt = require("src.render.Tilt")
|
||||
|
||||
@@ -329,6 +329,10 @@ function OverworldState:setMap(mapId, x, y, facing, opts)
|
||||
MapLoader.invalidateAll()
|
||||
end
|
||||
self.map = MapLoader.load(Game.data, mapId)
|
||||
-- EnterMap -> ClearVariablesOnEnterMap zeroes wStepCounter
|
||||
-- (engine/overworld/clear_variables.asm:7), but a connection crossing
|
||||
-- never reaches EnterMap (home/overworld.asm:675 .loadNewMap)
|
||||
if not (opts and opts.seamless) then Game.save.poisonSteps = 0 end
|
||||
-- STRENGTH deactivates on every real map load (home/overworld.asm
|
||||
-- EnterMap -> ResetUsingStrengthOutOfBattleBit clears BIT_STRENGTH_ACTIVE
|
||||
-- of wStatusFlags1). setMap is the single choke point for every map-id
|
||||
@@ -4296,6 +4300,9 @@ end
|
||||
-- battle is optional; when given, Oak's Lab OPP_RIVAL1 losses skip the
|
||||
-- blackout (pret HandlePlayerBlackOut) so the map script can HealParty.
|
||||
function OverworldState:afterBattle(result, battle)
|
||||
-- .battleOccurred tail-jumps to EnterMap (home/overworld.asm:353), so
|
||||
-- ClearVariablesOnEnterMap zeroes wStepCounter on every battle return
|
||||
Game.save.poisonSteps = 0
|
||||
if battle and battle.kind == "wild" then
|
||||
self.wildEncounterGraceSteps = WILD_ENCOUNTER_GRACE_STEPS
|
||||
end
|
||||
|
||||
@@ -152,16 +152,95 @@ function BorderFill.bake(atlas, tileset, blockId, bgSet, waterFrame)
|
||||
return img
|
||||
end
|
||||
|
||||
-- VOID FILL (#1418): the beyond-edge scenery under survey zoom. FADE (the
|
||||
-- default) is each map header's own border block with the dissolve below;
|
||||
-- WATER / TREES force one block on outdoor tilesets that actually have that
|
||||
-- scenery; BLACK is a flat void. Indoor and cave tilesets have no tree wall
|
||||
-- or water metatile, so water/trees fall through to the map's own border
|
||||
-- rather than painting a random block.
|
||||
--
|
||||
-- Canonical ids are the wMapBorderBlock values already used as those fills
|
||||
-- in data/maps/attributes.asm: New Bark / Route 30 $05 trees, Cherrygrove
|
||||
-- $35 water, Pallet $0f trees, Cinnabar $43 water, Ilex Forest $05 trees.
|
||||
BorderFill.VOID_FILLS = { "fade", "water", "trees", "black" }
|
||||
BorderFill.voidFill = "fade"
|
||||
|
||||
local FILL_BLOCKS = {
|
||||
TILESET_JOHTO = { trees = 0x05, water = 0x35 },
|
||||
TILESET_JOHTO_MODERN = { trees = 0x05, water = 0x35 },
|
||||
TILESET_KANTO = { trees = 0x0f, water = 0x43 },
|
||||
TILESET_FOREST = { trees = 0x05 },
|
||||
}
|
||||
|
||||
function BorderFill.setVoidFill(mode)
|
||||
local ok = false
|
||||
for _, name in ipairs(BorderFill.VOID_FILLS) do
|
||||
if name == mode then ok = true; break end
|
||||
end
|
||||
BorderFill.voidFill = ok and mode or "fade"
|
||||
end
|
||||
|
||||
function BorderFill.cycle(delta)
|
||||
local cur = BorderFill.voidFill or "fade"
|
||||
local at = 1
|
||||
for i, name in ipairs(BorderFill.VOID_FILLS) do
|
||||
if name == cur then at = i; break end
|
||||
end
|
||||
local n = #BorderFill.VOID_FILLS
|
||||
at = (at - 1 + (delta or 1)) % n + 1
|
||||
BorderFill.setVoidFill(BorderFill.VOID_FILLS[at])
|
||||
return BorderFill.voidFill
|
||||
end
|
||||
|
||||
function BorderFill.applyOptions(opts)
|
||||
BorderFill.setVoidFill(opts and opts.voidFill or "fade")
|
||||
end
|
||||
|
||||
function BorderFill.voidFillLabel(mode)
|
||||
mode = mode or BorderFill.voidFill or "fade"
|
||||
if mode == "water" then return "WATER" end
|
||||
if mode == "trees" then return "TREES" end
|
||||
if mode == "black" then return "BLACK" end
|
||||
-- trailing space blanks the 5-char WATER/TREES/BLACK from the value column
|
||||
return "FADE "
|
||||
end
|
||||
|
||||
-- The metatile the void should bake, or false when BLACK skips tiling.
|
||||
function BorderFill.fillBlock(def)
|
||||
local mode = BorderFill.voidFill or "fade"
|
||||
if mode == "black" then return false end
|
||||
if (mode == "water" or mode == "trees") and def then
|
||||
local fills = FILL_BLOCKS[def.tileset]
|
||||
local block = fills and fills[mode]
|
||||
if block ~= nil then return block end
|
||||
end
|
||||
return def and def.borderBlock or 0
|
||||
end
|
||||
|
||||
-- Crossfade identity: FADE dissolves per map (Cherrygrove water -> Route 30
|
||||
-- trees). WATER/TREES dissolve only when the forced block itself changes
|
||||
-- (Johto water -> Kanto water), so walking two Johto routes does not fade
|
||||
-- identical water against itself. BLACK is one sheet everywhere.
|
||||
function BorderFill.fillKey(def)
|
||||
local mode = BorderFill.voidFill or "fade"
|
||||
if mode == "black" then return "black" end
|
||||
local block = BorderFill.fillBlock(def)
|
||||
if mode == "water" or mode == "trees" then
|
||||
return mode .. "|" .. tostring(def and def.tileset) .. "|" .. tostring(block)
|
||||
end
|
||||
return "fade|" .. tostring(def and def.id)
|
||||
end
|
||||
|
||||
-- Each map header carries its OWN border block, so crossing a boundary swaps
|
||||
-- the whole void from one block to another: Cherrygrove's water becomes Route
|
||||
-- 30's trees between one frame and the next. On a 20x18 viewport that is a few
|
||||
-- pixels at the screen edge and nobody sees it; under survey zoom the void is
|
||||
-- most of the window, and the swap reads as the background popping.
|
||||
--
|
||||
-- So the swap is dissolved rather than cut. `key` is the map the image belongs
|
||||
-- to, not the image itself: the same block gets re-baked by the daytime
|
||||
-- rollover, the COLOR option and the two-frame cave flicker, and a dissolve on
|
||||
-- any of those would smear the flicker into mush.
|
||||
-- So the swap is dissolved rather than cut. `key` is the fill identity
|
||||
-- (BorderFill.fillKey), not the image itself: the same block gets re-baked by
|
||||
-- the daytime rollover, the COLOR option and the two-frame cave flicker, and a
|
||||
-- dissolve on any of those would smear the flicker into mush.
|
||||
BorderFill.CROSSFADE_FRAMES = 20
|
||||
|
||||
-- The bookkeeping half, love-free so it can be checked without a canvas.
|
||||
|
||||
@@ -7900,8 +7900,10 @@ function World:borderWaterFrame(def, tileset)
|
||||
end
|
||||
end
|
||||
if not tile then return nil end
|
||||
local fill = BorderFill.fillBlock(def)
|
||||
if fill == false then return nil end
|
||||
local block = tileset.blocks
|
||||
and tileset.blocks[BorderFill.blockFor(0, def.borderBlock) + 1]
|
||||
and tileset.blocks[BorderFill.blockFor(0, fill) + 1]
|
||||
if not block then return nil end
|
||||
local found = false
|
||||
for i = 1, 16 do
|
||||
@@ -7933,11 +7935,19 @@ function World:borderImageFor(mapId)
|
||||
if not def then return nil end
|
||||
local tileset = self.tilesets and self.tilesets[def.tileset]
|
||||
if not tileset then return nil end
|
||||
-- VOID FILL black skips the tiled bake; drawGround paints a flat void.
|
||||
local fill = BorderFill.fillBlock(def)
|
||||
if fill == false then return nil end
|
||||
local blockId = BorderFill.blockFor(0, fill)
|
||||
-- A border block made of water animates with the rest of the map, so this
|
||||
-- frame's row joins the key: four bakes per map instead of one.
|
||||
-- frame's row joins the key: four bakes per map instead of one. The VOID
|
||||
-- FILL mode is in the key so switching FADE/WATER/TREES does not keep a
|
||||
-- stale bake (#1418).
|
||||
local waterFrame = self:borderWaterFrame(def, tileset)
|
||||
local cacheKey = BorderFill.cacheKey(mapId .. "|" .. tostring(daytime)
|
||||
.. "|" .. tostring(GbcPalette.mode) .. "|" .. tostring(flicker)
|
||||
.. "|" .. tostring(BorderFill.voidFill or "fade")
|
||||
.. "|" .. tostring(blockId)
|
||||
.. "|" .. tostring(waterFrame and waterFrame.row or 0))
|
||||
local cached = self.mapImages[cacheKey]
|
||||
if cached ~= nil then return cached or nil end
|
||||
@@ -7949,7 +7959,7 @@ function World:borderImageFor(mapId)
|
||||
bgSet = Palettes.withCaveFlicker(bgSet, flicker or 1)
|
||||
end
|
||||
local ok, img = pcall(BorderFill.bake, atlas, tileset,
|
||||
BorderFill.blockFor(0, def.borderBlock), bgSet, waterFrame)
|
||||
blockId, bgSet, waterFrame)
|
||||
-- `false` rather than nil: a bake that cannot be made (a headless run with
|
||||
-- no canvas support) must not be retried once per frame forever.
|
||||
self.mapImages[cacheKey] = (ok and img) or false
|
||||
@@ -9719,8 +9729,16 @@ function World:drawGround(s)
|
||||
else
|
||||
bw, bh = GameViewport.dimensions()
|
||||
end
|
||||
BorderFill.draw(self, self:borderImageFor(self.map.id),
|
||||
cam.x, cam.y, bw, bh, s, self.map.id)
|
||||
if BorderFill.fillBlock(self.map.def) == false then
|
||||
-- BLACK: World:draw clears to a brown letterbox, so the void itself
|
||||
-- has to be an actual black sheet or the map sits on that colour.
|
||||
G.setColor(0, 0, 0, 1)
|
||||
G.rectangle("fill", 0, 0, bw, bh)
|
||||
G.setColor(1, 1, 1, 1)
|
||||
else
|
||||
BorderFill.draw(self, self:borderImageFor(self.map.id),
|
||||
cam.x, cam.y, bw, bh, s, BorderFill.fillKey(self.map.def))
|
||||
end
|
||||
end
|
||||
for _, nb in ipairs(self.neighbors) do
|
||||
G.draw(nb.image,
|
||||
|
||||
Reference in New Issue
Block a user