shiny capture

This commit is contained in:
DramaticShape
2026-08-08 14:02:29 -04:00
parent bee22507b2
commit b8b2e0cd33
9 changed files with 1000 additions and 144 deletions
+183 -21
View File
@@ -137,6 +137,7 @@ ForestAtmos.configFor = configFor
-- shader and texture sentinels, which is what a lost GL context needs.
local layoutCache = {}
local grassCache = {} -- the grass deal, keyed by map; false = no grass
local meshCache = {}
local shaders = {} -- keyed by variant; nil untried, false refused
local leafTex = nil -- the tiling leaf-dapple field
@@ -156,9 +157,10 @@ end
function ForestAtmos.invalidate(mapId)
if mapId then
layoutCache[mapId] = nil
grassCache[mapId] = nil
meshCache[mapId] = nil
else
layoutCache, meshCache = {}, {}
layoutCache, grassCache, meshCache = {}, {}, {}
shaders = {}
leafTex = nil
rayMesh = nil
@@ -194,6 +196,18 @@ ForestAtmos.RAMP = {
alpha = 0.40, density = 1.15, motes = 0.0, flies = 1.0 },
}
-- How far out the night shift is, 0..1, at clock `t`. Pulled out of the
-- frame's own loop because the GRASS fireflies (below) need it on maps
-- that have no atmosphere at all -- and pulled out rather than copied so
-- the two can never disagree about when a firefly comes on.
function ForestAtmos.fireflyLevel(t)
local flies = 0
for name, w in pairs(DayNight.mix(t or DayNight.time())) do
flies = flies + (ForestAtmos.RAMP[name] or ForestAtmos.RAMP.day).flies * w
end
return flies
end
-- The frame's atmosphere for `map` at clock `t` (defaulting to now), or
-- nil -- no entry, or the row is OFF -- in which case nothing is drawn
-- and Voxel3D.fog should be left nil.
@@ -203,7 +217,8 @@ function ForestAtmos.frame(map, t)
if not cfg then return nil end
local mix = DayNight.mix(t or DayNight.time())
local fr, fg, fb, rr, rg, rb = 0, 0, 0, 0, 0, 0
local alpha, dens, motes, flies = 0, 0, 0, 0
local alpha, dens, motes = 0, 0, 0
local flies = ForestAtmos.fireflyLevel(t)
for name, w in pairs(mix) do
local p = ForestAtmos.RAMP[name] or ForestAtmos.RAMP.day
fr, fg, fb = fr + p.fog[1] * w, fg + p.fog[2] * w, fb + p.fog[3] * w
@@ -211,7 +226,6 @@ function ForestAtmos.frame(map, t)
alpha = alpha + p.alpha * w
dens = dens + p.density * w
motes = motes + p.motes * w
flies = flies + p.flies * w
end
-- the haze takes on a little of the light standing in it
local LEAN = 0.15
@@ -374,6 +388,111 @@ function ForestAtmos.layout(cfg, w, h)
return { motes = motes, flies = flies }
end
-- ------- the grass fireflies
--
-- The same particle -- same mesh format, same blinking shader, same hour's
-- ramp -- dealt over a map's TALL GRASS instead of over its whole volume,
-- and on any outdoor map rather than only the ones with an atmosphere
-- entry. Grass IS the entry: a route with tall grass on it gets fireflies
-- after dark without anybody authoring a line, and a map with no grass
-- cell on it deals nothing and costs one scan.
--
-- Placement asks the engine's own question, `map:isGrassCell` -- the
-- cell's collision tile, the rule that decides where a wild battle can
-- start -- which is the same test Structures runs before it sprouts a
-- tuft. The grass GRAPHIC also turns up as decorative filler inside plain
-- ground blocks, and going by the tile would hang fireflies over town
-- plazas (the trap that note in Structures.buildGrass records).
--
-- They are dealt into the cell rather than at its middle, and the shader's
-- own wander (sway 10, 4) carries each one about a cell's width from where
-- it was dealt -- so a patch reads as a patch with fireflies loose over it,
-- not as a grid of lights. The height band is the tufts' own: 3..15, which
-- is blade height and a little air (Structures stands its blades to y = 16).
--
-- This is the FULL rung, like every other particle here. The per-tuft
-- firefly cards in Structures.buildGrass are the layer underneath -- they
-- are static geometry the scene shader already carries, so grass still has
-- something alight after dark on LOW and on Android, where this pass is
-- not drawn at all.
local GRASS_PER_CELL = 0.8 -- fireflies per grass cell...
local GRASS_CAP = 200 -- ...up to this many on one map
local CELL = 16 -- world px, the grid isGrassCell speaks
-- A map id deals its own swarm: stable across visits and saves (nothing
-- here rides the clock), different between maps, so two routes with the
-- same amount of grass do not get the same arrangement of lights.
local function seedOf(id)
local s = 0x51D
for i = 1, #id do s = (s * 31 + id:byte(i)) % 0x100000000 end
return s
end
-- `cells` is a flat list of { cx, cy } pairs; count is worked out from how
-- many there are. Deterministic, and separated from the map so a test can
-- hand it a meadow.
function ForestAtmos.grassLayout(cells, seed, per, cap)
local n = #cells
if n == 0 then return {} end
local want = min(floor(n * (per or GRASS_PER_CELL) + 0.5), cap or GRASS_CAP)
local rng = newRng(seed or 0x51D)
local flies = {}
for _ = 1, want do
local c = cells[1 + min(floor(rng:unit() * n), n - 1)]
flies[#flies + 1] = {
x = c[1] * CELL + rng:unit() * CELL,
y = 3 + rng:unit() * 12,
z = c[2] * CELL + rng:unit() * CELL,
phase = rng:unit() * 6.2832,
rate = 0.5 + rng:unit(),
}
end
return flies
end
-- Only where the clock reaches: an outdoor map, or a CANOPY one (Viridian
-- Forest is not outdoor -- no sky, no sun -- but night still falls in it,
-- and it is the map these fireflies were drawn for). A cave stays a cave.
local function litByTheHour(map)
if DayNight.isCanopy(map) then return true end
local ok, Map = pcall(require, "src.world.Map")
if not (ok and Map and Map.isOutdoor) then return false end
local got, outdoor = pcall(Map.isOutdoor, map.def or {})
return got and outdoor or false
end
-- One scan of the map's cells, cached with everything else that goes stale
-- with it. `false` records "scanned, no grass" so a grassless map is never
-- walked twice.
local function grassFliesFor(map)
local hit = grassCache[map.id]
if hit ~= nil then return hit or nil end
if not (map.isGrassCell and litByTheHour(map)) then
grassCache[map.id] = false
return nil
end
local cells = {}
local w = map.widthCells or ((map.def and map.def.width or 0) * 2)
local h = map.heightCells or ((map.def and map.def.height or 0) * 2)
-- one pcall around the whole scan, not one per cell: a route is a couple
-- of thousand cells and this runs on the frame that first draws the map
pcall(function()
for cy = 0, h - 1 do
for cx = 0, w - 1 do
if map:isGrassCell(cx, cy) then cells[#cells + 1] = { cx, cy } end
end
end
end)
local cfg = configFor(map.id) or {}
local knob = cfg.grassFlies or {}
local flies = ForestAtmos.grassLayout(cells, seedOf(map.id),
knob.per, knob.cap)
grassCache[map.id] = (#flies > 0) and flies or false
return grassCache[map.id] or nil
end
ForestAtmos.grassFliesFor = grassFliesFor
-- A map is width x height BLOCKS of 4x4 tiles of 8 pixels -- times 32
-- for world pixels (the same arithmetic Structures runs in tiles).
local function layoutFor(map)
@@ -637,6 +756,9 @@ local PART_SHADER = [[
uniform float size;
uniform vec2 sway; // wander amplitude: horizontal, vertical
uniform float blinky; // 0 = steady motes, 1 = blinking fireflies
uniform vec3 origin; // the map's corner: zero for the one being
// stood on, the connection offset for a
// neighbour's swarm (one mesh, drawn per map)
attribute vec4 AtmosData; // corner x, corner y, phase, rate
vec4 position(mat4 transform_projection, vec4 vertex_position) {
float ph = AtmosData.z;
@@ -644,7 +766,7 @@ local PART_SHADER = [[
float t = time * (0.5 + rt);
// bounded wander only -- three incommensurate sines, so nothing ever
// walks off the map or needs a CPU tick to bring it home
vec3 base = vertex_position.xyz + vec3(
vec3 base = vertex_position.xyz + origin + vec3(
sin(t * 0.23 + ph) * sway.x,
sin(t * 0.17 + ph * 2.7) * sway.y,
cos(t * 0.19 + ph * 1.3) * sway.x);
@@ -733,12 +855,19 @@ local function buildPartMesh(points)
return mesh
end
local function meshesFor(map, L)
-- The map's three swarms in one cache entry: the atmosphere's pollen and
-- fireflies, which exist only for a map with an entry, and the grass
-- fireflies, which exist for any outdoor map with tall grass on it. Each
-- is nil on its own -- a route builds one mesh, Viridian Forest builds
-- three, and a cave builds none.
local function meshesFor(map)
local hit = meshCache[map.id]
if hit then return hit end
local L = layoutFor(map)
local M = {
motes = buildPartMesh(L.motes),
flies = buildPartMesh(L.flies),
motes = L and buildPartMesh(L.motes) or nil,
flies = L and buildPartMesh(L.flies) or nil,
grass = buildPartMesh(grassFliesFor(map) or {}),
}
meshCache[map.id] = M
return M
@@ -746,6 +875,7 @@ end
local MOTE_COLOR = { 1.0, 0.96, 0.78 }
local FLY_COLOR = { 0.72, 1.0, 0.45 }
local HOME = { 0, 0, 0 } -- the origin of the map being stood on
-- The billboard frame: the camera's own right and up, from the same
-- fields every pass sets (per VR eye too -- drawScene runs per eye and
@@ -774,19 +904,29 @@ end
-- geometry like the Stadium flames. Anything missing -- no entry, OFF, a
-- refused shader, no readable depth, no shadow map -- subtracts only
-- itself.
function ForestAtmos.draw(map)
--
-- `f` is nil for every map without an atmosphere entry, and the pass does
-- NOT stop there any more: the grass fireflies belong to the grass, not to
-- an authored line, so the fog and the beams sit that map out and the
-- particle block below still runs. Every read of `f` past here is guarded.
function ForestAtmos.draw(map, neighbors)
local rung = ForestAtmos.setting:get()
if rung == "off" then return end
if not (map and map.id) then return end
local f = ForestAtmos.frame(map)
if not f then return end
local Voxel3D = V.require("Voxel3D")
local ShadowMap = V.require("ShadowMap")
if f.rayAlpha > 0.01 then
if not Voxel3D.depthReadable() then
say("depth", "no readable depth this frame -- beams off, fog stays")
return
end
-- A refusal here takes the BEAMS, not the frame: this one used to
-- `return` out of the function, which took the particles with it. They
-- fall through now, so a machine that cannot hand back a readable depth
-- buffer still gets fireflies over its grass.
local beams = f and f.rayAlpha > 0.01 or false
if beams and not Voxel3D.depthReadable() then
say("depth", "no readable depth this frame -- beams off, fog stays")
beams = false
end
if beams then
-- no beams without the sun's own pass: uvVP is only the world -> map
-- transform while a shadow map is actually standing
local sunTex = ShadowMap.active() and ShadowMap.texture()
@@ -843,11 +983,13 @@ function ForestAtmos.draw(map)
end
if rung == "full" then
local L = layoutFor(map)
local M = L and meshesFor(map, L)
local M = meshesFor(map)
-- the night shift, for the grass swarm: the atmosphere's own answer
-- where there is one, and the bare ramp where there is not
local flyLevel = f and f.fireflyLevel or ForestAtmos.fireflyLevel()
local psh = partShader()
local axisR, axisU = billboardAxes(Voxel3D)
if M and psh and axisR then
if (M.motes or M.flies or M.grass) and psh and axisR then
Voxel3D.blend("add")
if Voxel3D.beginEffect(psh) then
pcall(psh.send, psh, "vp", "row", Voxel3D.vp)
@@ -860,7 +1002,8 @@ function ForestAtmos.draw(map)
pcall(psh.send, psh, "axisR", axisR)
pcall(psh.send, psh, "axisU", axisU)
pcall(psh.send, psh, "time", ForestAtmos.time)
if M.motes and f.moteLevel > 0.02 then
pcall(psh.send, psh, "origin", HOME)
if M.motes and f and f.moteLevel > 0.02 then
pcall(psh.send, psh, "size", 1.4)
pcall(psh.send, psh, "sway", { 5, 2.5 })
pcall(psh.send, psh, "blinky", 0)
@@ -868,13 +1011,32 @@ function ForestAtmos.draw(map)
pcall(psh.send, psh, "level", f.moteLevel * 0.5)
pcall(love.graphics.draw, M.motes)
end
if M.flies and f.fireflyLevel > 0.02 then
-- every swarm of fireflies shares every uniform but the mesh and
-- the map corner it stands on: the grass ones ARE the forest's,
-- moved onto the grass.
--
-- The NEIGHBOURS matter here in a way they never did for the fog
-- or the beams. A connected route is drawn in full -- its ground,
-- its trees, its grass and the firefly cards standing in it -- so
-- a swarm that stopped at the seam would draw a line across the
-- map where the lights ran out. Each neighbour's own deal is a
-- mesh already cached against its id; it costs a uniform and a
-- draw call to put it where the terrain under it is.
if flyLevel > 0.02 then
pcall(psh.send, psh, "size", 1.6)
pcall(psh.send, psh, "sway", { 10, 4 })
pcall(psh.send, psh, "blinky", 1)
pcall(psh.send, psh, "dotColor", FLY_COLOR)
pcall(psh.send, psh, "level", f.fireflyLevel * 0.85)
pcall(love.graphics.draw, M.flies)
pcall(psh.send, psh, "level", flyLevel * 0.85)
if M.flies then pcall(love.graphics.draw, M.flies) end
if M.grass then pcall(love.graphics.draw, M.grass) end
for _, nb in ipairs(neighbors or {}) do
local NM = nb.map and nb.map.id and meshesFor(nb.map)
if NM and NM.grass then
pcall(psh.send, psh, "origin", { nb.ox or 0, 0, nb.oy or 0 })
pcall(love.graphics.draw, NM.grass)
end
end
end
Voxel3D.endEffect()
end
+178
View File
@@ -0,0 +1,178 @@
-- SELECT on a row explains what it does.
--
-- ------- why this exists at all
--
-- Every setting in this mod has ALWAYS carried a paragraph of help. It goes
-- into the schema handed to the mod manager (ModSetting:schema takes it), it
-- has been written and kept up to date beside every row in main.lua's
-- SETTINGS -- and nothing in the engine has ever drawn it. Not the OPTIONS
-- menu, whose row is a label and a value and has no room for a third thing;
-- not the mod manager's own page, which renders the same two lines. It was
-- authored, structured, accurate prose sitting in a field with no reader.
--
-- So it gets one. A row on this mod's menus says what it IS on one line and
-- what it is SET TO on the next, and SELECT says what that means -- which is
-- the question a row like RENDER DIST or 2D-3D B cannot answer in eighteen
-- characters however the label is worded.
--
-- SELECT rather than a button that already does something: A steps a setting,
-- B leaves, and the d-pad moves. SELECT is free on a menu -- the mod's own
-- SELECT hotkey is installed on OverworldController:handleInput, which only
-- runs while the overworld is the top state, so a menu can have the button
-- without taking anything from the map.
--
-- ------- the shape of it
--
-- The game's own dialogue box: drawn with Font.drawBox, so the border is the
-- ROM's own glyphs and a mod-supplied font theme retextures this along with
-- everything else (Font.BORDER) -- and anchored to the BOTTOM of the screen
-- with the menu still visible above it, which is where this game has put
-- every line of text anybody has ever read in it.
--
-- Sized to what it holds rather than to the screen. Each description is one
-- sentence, so most of these are five or six tiles tall and the row being
-- asked about is still on screen over the top of the box. A sentence long
-- enough to overflow scrolls instead of growing past MAX_LINES, a line at a
-- time on the d-pad -- which is a fallback, not the design: the answer to a
-- description that needs scrolling is a shorter description.
-- the mod namespace (see main.lua)
local V = ...
local Font = require("src.render.Font")
local Theme = require("src.ui.Theme")
local PaletteFX = require("src.render.PaletteFX")
local SettingsHelp = {}
SettingsHelp.__index = SettingsHelp
-- NOT opaque: the menu stays drawn underneath, so the row being asked about
-- is still on screen above the box. That is most of why the box is only as
-- tall as it needs to be.
SettingsHelp.isOpaque = false
-- The box spans the screen's twenty tiles and its border owns the outer ring,
-- so text runs from tile 1. Seventeen columns rather than eighteen: tile 18
-- is kept clear for the more-arrow, which would otherwise land on top of the
-- last character of any line that filled the width.
local COLS = 17
local PEN_X = 8
local SCREEN_ROWS = 18
-- title, plus the body, plus the two border rows
local CHROME_ROWS = 3
-- A sentence needing more than this scrolls. Eight lines of seventeen is 136
-- characters, which is a long sentence and a box two thirds up the screen.
local MAX_LINES = 8
-- Break a string into lines that fit, on word boundaries. Unbounded, unlike
-- StadiumScreen's -- that one is capping a save path to what a fixed plate can
-- show, and this one is the whole point of the screen.
local function wrapped(str, cols)
cols = cols or COLS
local lines, line = {}, nil
for word in tostring(str or ""):gmatch("%S+") do
local try = line and (line .. " " .. word) or word
if #try <= cols then
line = try
else
if line then lines[#lines + 1] = line end
-- a word longer than the line is broken across lines rather than cut;
-- nothing in the help text is that long today, but losing the end of a
-- sentence silently is not a failure mode worth leaving open
while #word > cols do
lines[#lines + 1] = word:sub(1, cols)
word = word:sub(cols + 1)
end
line = word
end
end
if line then lines[#lines + 1] = line end
return lines
end
SettingsHelp.wrapped = wrapped
function SettingsHelp.new(game, title, body)
return setmetatable({
game = game,
title = tostring(title or ""):gsub("%.%.$", ""),
lines = wrapped(body),
top = 0,
}, SettingsHelp)
end
-- How many body lines this box shows: all of them, unless there are more than
-- a box is allowed to be tall.
function SettingsHelp:bodyRows()
return math.min(#self.lines, MAX_LINES)
end
function SettingsHelp:maxTop()
return math.max(0, #self.lines - self:bodyRows())
end
-- Every button that could mean "done" closes it, including SELECT itself --
-- the press that opened the box is the one a player is most likely to reach
-- for to get rid of it. A is in there too: it steps a setting everywhere else
-- on these menus, and stepping one you cannot see would be worse than an
-- extra way out.
local DISMISS = { "a", "b", "start", "select" }
function SettingsHelp:update()
local input = self.game and self.game.input
if not input then return end
local maxTop = self:maxTop()
-- the d-pad only does anything when there is something below the fold; a
-- box showing its whole sentence has nowhere to go and says so by not
-- moving
if input:wasPressed("down") then
self.top = math.min(maxTop, self.top + 1)
return
elseif input:wasPressed("up") then
self.top = math.max(0, self.top - 1)
return
end
for _, btn in ipairs(DISMISS) do
if input:wasPressed(btn) then
local stack = self.game.stack
if self.game.data then
require("src.core.Sound").play(self.game.data, "Press_AB")
end
if stack and stack:top() == self then stack:pop() end
return
end
end
end
function SettingsHelp:draw()
local body = self:bodyRows()
local th = body + CHROME_ROWS
local ty = SCREEN_ROWS - th -- anchored to the bottom of the screen
Font.drawBox(0, ty, 20, th)
love.graphics.setColor(0, 0, 0, 1)
-- the row's own name, so the box says what it is about even where it covers
-- the row that was asked
Font.draw(self.title, PEN_X, (ty + 1) * 8)
for i = 1, body do
local line = self.lines[self.top + i]
if not line then break end
Font.draw(line, PEN_X, (ty + 1 + i) * 8)
end
-- the same marker the options list uses for "there is more below this", so
-- it means here what it means there
if self.top < self:maxTop() then
Font.drawCode(Theme.moreArrow, 144, (ty + th - 2) * 8)
end
love.graphics.setColor(1, 1, 1, 1)
end
-- Game:draw stops at the first state that HAS this method, so without one the
-- box would inherit whatever is underneath -- which is a menu of ours, whose
-- answer happens to be right. Stated anyway: the reason that answer is right
-- is not a property of this screen, and a future menu that paints something
-- of its own would silently repaint this box with it.
function SettingsHelp:sgbPalettes(game)
return PaletteFX.wholeNamed(game.data, "MEWMON")
end
return SettingsHelp
+72 -16
View File
@@ -70,12 +70,36 @@ end
-- opening it. Where no single row speaks for the rest, it counts them, which
-- is honest rather than arbitrary.
SettingsMenu.CATEGORIES = {
{ id = "world", label = "3D WORLD.." },
{ id = "world", label = "3D WORLD..",
help = "The diorama itself: how far the world bends, how much of it is "
.. "drawn, what the water does and what hour it is outdoors." },
{ id = "battles", label = "BATTLES..",
summary = function() return V.require("OverworldBattle").setting:valueLabel() end },
{ id = "perf", label = "PERFORMANCE.." },
summary = function() return V.require("OverworldBattle").setting:valueLabel() end,
help = "What a fight is drawn over, how it is framed, and how a ball is "
.. "thrown." },
{ id = "perf", label = "PERFORMANCE..",
help = "What the look costs -- the three most expensive things in the "
.. "frame after the geometry itself." },
{ id = "vr", label = "VR..",
summary = function() return V.require("VR").setting:valueLabel() end },
summary = function() return V.require("VR").setting:valueLabel() end,
help = "PCVR through OpenXR, and the one comfort setting that belongs to "
.. "the headset alone." },
}
-- ------- help for the rows that are not settings
--
-- The thirteen settings each carry their own paragraph in main.lua's SETTINGS,
-- next to the row it explains. What is left is the two pipeline rows -- whose
-- descriptors belong to the ENGINE, so there is nowhere in them to put this --
-- and the ROM import, which is an action rather than a setting and has no
-- SETTINGS entry to live in.
local ROW_HELP = {
["pipeline:voxel"] = "The overworld extruded into real geometry and walked "
.. "by a 3D camera, with the numbered rungs its angle in degrees.",
["pipeline:tiltshift"] = "A tilt-shift blur that sells the miniature-model "
.. "look, sharp across the middle and softening above and below it.",
["DRAMATIC_SHAPE:stadiumRom"] = "Imports the Pokemon Stadium (US) 1.0 "
.. "cartridge that 3D-BTL's STADIUM rungs need.",
}
-- ------- what the menus are built from
@@ -91,6 +115,25 @@ function SettingsMenu.define(list)
settings = list or {}
end
-- What SELECT shows for a row: the setting's own paragraph out of SETTINGS,
-- the category's out of CATEGORIES, or one of the three above for the rows
-- that have nowhere else to keep it.
--
-- Looked up BY ID rather than hung on the row as a field, because two of
-- these rows are the engine's own tables reused verbatim -- and annotating
-- somebody else's table is how a mod ends up owning a field it never meant
-- to. nil for a row with nothing to say, which SELECT reads as "no box".
function SettingsMenu.helpFor(id)
if ROW_HELP[id] then return ROW_HELP[id] end
for _, cat in ipairs(SettingsMenu.CATEGORIES) do
if SettingsMenu.id(cat.id) == id then return cat.help end
end
for _, entry in ipairs(settings) do
if "DRAMATIC_SHAPE:" .. entry[1].key == id then return entry[2] end
end
return nil
end
-- VOXEL and T-SHIFT are the ENGINE's row descriptors (src/render/Pipelines
-- .rows), captured by the options hook on its way past and shown here instead
-- of at the top level. Reused verbatim, tables and all: they persist in
@@ -273,19 +316,19 @@ function SettingsMenu.signature(rows)
return table.concat(ids, "\1")
end
-- OptionRows has no room for a title: the four boxes fill the screen down to
-- the bottom line. So the bottom line carries the name as well as the way
-- out, which is the one place left to say where the player is. "BACK" alone
-- at the root, where "BACK: DRAMATIC SHAPE" would run past the 18 characters
-- the line has.
-- The bottom line is the only place on this screen to say anything that is
-- not a row: OptionRows' four boxes fill everything above it and there is no
-- header slot. It spends that line on the two buttons that are not obvious.
--
-- It used to carry the category's NAME instead, for orientation. The hint
-- won: a binding nobody knows about is worth nothing, and where the player is
-- was just answered by the row they pressed A on. Sixteen characters of the
-- eighteen the line has, which is also why the name could not stay -- "BACK:
-- PERFORMANCE" is seventeen on its own.
SettingsMenu.BACK_LABEL = "B BACK SEL HELP"
function SettingsMenu:backLabel()
if self.cat == SettingsMenu.ROOT then return "BACK" end
for _, cat in ipairs(SettingsMenu.CATEGORIES) do
if cat.id == self.cat then
return "BACK: " .. (cat.label:gsub("%.%.$", ""))
end
end
return "BACK"
return SettingsMenu.BACK_LABEL
end
-- A category's contents can change while the player is looking at them: 3D-BTL
@@ -349,6 +392,19 @@ function SettingsMenu:update()
pop(self)
return
end
elseif input:wasPressed("select") then
-- SELECT explains the row the cursor is on. Every row on these menus has
-- something to say -- the settings have carried a paragraph each since
-- they were written, and nothing has ever drawn it (see SettingsHelp) --
-- but a row that does not is simply left alone rather than opening an
-- empty box.
local row = rows[self.index]
local help = row and SettingsMenu.helpFor(row.id)
if help and self.game.stack then
self.game.stack:push(
V.require("SettingsHelp").new(self.game, row.label, help))
end
return
elseif input:wasPressed("b") or input:wasPressed("start") then
-- B and START both, like every other menu -- and one level only: this
-- pops US, leaving the OPTIONS menu underneath exactly as the player
+4 -2
View File
@@ -1246,8 +1246,10 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
-- finished depth buffer, so the trees occlude the light and the light
-- writes nothing; here in the prop slot, after everything the beams
-- should fall across and inside drawScene so VR gets them per eye. On
-- the one map that has any, today.
ForestAtmos.draw(state.map)
-- the one map that has any, today -- but the FIREFLIES over tall grass
-- need no entry and reach every outdoor map, which is why the connected
-- neighbours go too: their grass is drawn, so their lights are.
ForestAtmos.draw(state.map, state.neighbors)
-- The VR pokedex in the player's left hand, last of all: a prop over
-- the world drawn with real depth, so leaning it into a wall still