This commit is contained in:
bryanthaboi
2026-08-18 10:49:04 -04:00
parent c2c7fdafcf
commit d5ad830fb8
11 changed files with 331 additions and 14 deletions
+1
View File
@@ -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.
+3
View File
@@ -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
+18 -3
View File
@@ -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
+13
View File
@@ -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")
+83 -4
View File
@@ -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.
+23 -5
View File
@@ -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,