mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 10:30:59 +02:00
shiny capture
This commit is contained in:
+183
-21
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user