fix catch regression + add new menus
This commit is contained in:
@@ -77,8 +77,74 @@
|
||||
nor takes it away, on the same reasoning as AA -- what the look costs is
|
||||
the player's question, not a preset's.
|
||||
|
||||
### Changed
|
||||
|
||||
- **The settings live on menus of their own now, behind one red row at the
|
||||
top of OPTIONS.** This mod had grown to fourteen rows on the engine's
|
||||
list, spliced in as one block. OPTIONS shows four boxes at a time, so that
|
||||
was four screens of scrolling inside a list that already carried twenty
|
||||
engine rows, and finding SHADOWS meant knowing it was in there somewhere
|
||||
past the wireframe and the horizon bend.
|
||||
|
||||
What is on OPTIONS now is `DRAMATIC SHAPE..`, and it leads the list --
|
||||
a mod that replaces the look of the whole game should not make the player
|
||||
scroll to find out where its settings went, least of all past the engine
|
||||
rows it has quietly taken away. It opens VOXEL and T-SHIFT, which came off
|
||||
the engine's list with it, and four categories: **3D WORLD** (V-GRID,
|
||||
V-CURVE, RENDER DIST, WATER, DAYTIME), **BATTLES** (3D-BTL, BACK SPRITES,
|
||||
LET'S GO, STADIUM ROM), **PERFORMANCE** (FOREST FX, SHADOWS, AA) and
|
||||
**VR** (VR, SMOOTH TURN).
|
||||
|
||||
The split is not a new opinion: it is the `full` flag each row already
|
||||
carried. `full` marks a row the FULL preset does not take away, and the
|
||||
reason written beside each one was always the same -- this is a question
|
||||
about the HARDWARE, or about the GAME, not a knob on the diorama FULL is a
|
||||
preset for. So 3D WORLD is exactly the rows FULL owns, and needs no rule
|
||||
to disappear under it: every child filters itself out and an empty category
|
||||
is not offered. Under FULL the menu is four rows on one screen with no
|
||||
scroll arrow. The same rule retires VR where there is no VR to have.
|
||||
|
||||
**Nothing you had set has moved.** Every setting keeps its stored key, its
|
||||
ladder and its row id, so `options.lua` is byte-identical across the
|
||||
upgrade for a player who changes nothing -- and the hotkeys are untouched,
|
||||
which is what makes the nesting affordable: 3, 5, 6, 7, 8 and 9 still put
|
||||
every buried row one keypress away. The mod manager's own page still lists
|
||||
all thirteen settings flat, now in category order.
|
||||
|
||||
The row is drawn in red, which is a palette zone rather than a color:
|
||||
`setColor` cannot tint this text, because the glyph atlas is black ink and
|
||||
LOVE tints multiplicatively, and because the palette shader keys on the red
|
||||
channel alone and would send a red pixel to the lightest slot. What the
|
||||
zone changes is which color the shade the text was drawn in comes out as.
|
||||
It is MEWMON -- the palette the OPTIONS menu already wears -- copied with
|
||||
only the ink slot replaced, so the paper under the row is the same white as
|
||||
the row above it in all three ROMs, and the band covers the two text lines
|
||||
alone rather than the cursor and the box borders beside them. SGB INV
|
||||
reverses a palette, so there the red starts in the other slot and still
|
||||
lands on the ink; OG, OG INV and CLASSIC substitute their own tables
|
||||
outright, and the row simply draws monochrome, which is what asking for a
|
||||
screen with no colors in it should get.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A setting that pins another one now pins it from wherever it was
|
||||
changed.** 3D-BTL holds BATTLE LAYOUT at OG while a fight can be staged on
|
||||
the map, and FULL holds DAYTIME at SYNC while it owns that row. Both pins
|
||||
used to be a side effect of the options-rows hook, which every step on the
|
||||
OPTIONS menu happened to rerun -- so they fired whether or not the step was
|
||||
the one that mattered, and nothing had to name them. A step made on the
|
||||
mod's own menus reruns no hook, so the pinning is a function now, and the
|
||||
hook, the menus and the mod manager's page all ask for it.
|
||||
|
||||
- **An open OPTIONS menu notices a change made on a menu pushed over it.**
|
||||
The rebuild that keeps the row list honest compared the voxel level and the
|
||||
two battle switches across one call of `update`. The stack ticks its top
|
||||
state only, so a step taken on one of the mod's own menus happens while
|
||||
OPTIONS is suspended: both halves of that comparison were read after the
|
||||
fact and always agreed, and OPTIONS came back still showing a BATTLE LAYOUT
|
||||
row that no longer belonged there. The signature is held on the menu and
|
||||
stamped where the rows are built, which is the thing it is a signature of.
|
||||
|
||||
- **A building's back no longer wears its own front door.** Every voxelized
|
||||
building is its drawing extruded straight through the footprint, so the
|
||||
far wall is the facade again -- and read from behind, the facade mirrored:
|
||||
|
||||
+38
-2
@@ -285,19 +285,55 @@ end
|
||||
-- ball at the bottom of the frame, the throw a straight shot up the
|
||||
-- middle. Handed to BattleScene.render through the capture table's `rig`;
|
||||
-- everything downstream (pins, cards, sun, fov) is camera-generic.
|
||||
-- ------- and how far back the WORLD lets it stand
|
||||
--
|
||||
-- SEAT_BACK is what the shot wants. It is not always available: this seat
|
||||
-- is low (SEAT_UP is 13 world pixels, under a single cell) where the
|
||||
-- battle's own rig stands at 37.9, and it is planted three cells behind
|
||||
-- the player wherever the encounter happened -- which on a hedged route
|
||||
-- like ROUTE 6 is inside the hedge. The eye then looks out from within
|
||||
-- the foliage and the scenery it is standing in hangs across the top of
|
||||
-- the frame, over the horizon and the distant rooftops both.
|
||||
--
|
||||
-- ThirdPerson's boom answers exactly this question for the free-roam
|
||||
-- camera, so it answers it here rather than being restated: march back
|
||||
-- from the player's cell and stop at the first thing the eye may not be
|
||||
-- inside, keeping its PAD. Its rule is the right one for a seat this low,
|
||||
-- too -- terrain height always blocks, and anything BUILT on a cell
|
||||
-- blocks below head height, so a kerb is passed over and a hedge is not.
|
||||
--
|
||||
-- Pulling in brings the foe closer in a fixed field of view, which is a
|
||||
-- composition the throw already handles: the ring, the collision and the
|
||||
-- drag are all measured from the live shot, so they follow the seat.
|
||||
local SEAT_MIN_BACK = 20 -- never nearer than this: the held ball hangs
|
||||
-- at the player's own cell and has to stay in
|
||||
-- front of the eye
|
||||
|
||||
local function seatBack(px, pz, ax, az, eyeY)
|
||||
local ok, ow = pcall(function() return game().overworld end)
|
||||
if not (ok and ow) then return SEAT_BACK end
|
||||
local ThirdPerson = V.require("ThirdPerson")
|
||||
local okR, reach = pcall(ThirdPerson.reach, ow, { px, eyeY, pz },
|
||||
-ax, 0, -az, SEAT_BACK)
|
||||
if not (okR and reach) then return SEAT_BACK end
|
||||
return math.max(SEAT_MIN_BACK, math.min(SEAT_BACK, reach))
|
||||
end
|
||||
|
||||
local function captureRig(arena, groundY)
|
||||
local ex, ez = arena.enemy[1], arena.enemy[2]
|
||||
local px, pz = arena.player[1], arena.player[2]
|
||||
local ax, az = ex - px, ez - pz
|
||||
local l = math.sqrt(ax * ax + az * az)
|
||||
if l < 1e-6 then ax, az = 0, -1 else ax, az = ax / l, az / l end
|
||||
local eyeY = groundY + SEAT_UP
|
||||
local back = seatBack(px, pz, ax, az, eyeY)
|
||||
local cam = {
|
||||
eye = { px - ax * SEAT_BACK, groundY + SEAT_UP, pz - az * SEAT_BACK },
|
||||
eye = { px - ax * back, eyeY, pz - az * back },
|
||||
focus = { ex, groundY + 8, ez },
|
||||
fov = SEAT_FOV,
|
||||
}
|
||||
-- the pitch VoxelScene.pull wants: how far below level the seat looks
|
||||
local pitch = math.atan2(SEAT_UP - 8, SEAT_BACK + l)
|
||||
local pitch = math.atan2(SEAT_UP - 8, back + l)
|
||||
return cam, math.max(pitch, 0.05), SEAT_FRAME
|
||||
end
|
||||
|
||||
|
||||
+10
-6
@@ -168,6 +168,15 @@ function ModSetting:sync(value)
|
||||
self.index = indexOf(self, value)
|
||||
end
|
||||
|
||||
-- The label of the rung actually in force, which is not the stored one when
|
||||
-- that rung has been gated away (see get). Its own entry point because a
|
||||
-- caller can want the label without wanting a row: SettingsMenu puts one
|
||||
-- setting's rung on the second line of the CATEGORY that contains it.
|
||||
function ModSetting:valueLabel()
|
||||
local i = self:read()
|
||||
return self.labels[self:allows(i) and i or 1]
|
||||
end
|
||||
|
||||
-- The descriptor src/ui/OptionRows.lua renders, in the shape the
|
||||
-- ui.options.rows hook appends.
|
||||
function ModSetting:row()
|
||||
@@ -175,12 +184,7 @@ function ModSetting:row()
|
||||
return {
|
||||
id = "DRAMATIC_SHAPE:" .. self.key,
|
||||
label = self.label,
|
||||
-- the label of the rung actually in force, which is not the stored one
|
||||
-- when that rung has been gated away (see get)
|
||||
value = function()
|
||||
local i = self_:read()
|
||||
return self_.labels[self_:allows(i) and i or 1]
|
||||
end,
|
||||
value = function() return self_:valueLabel() end,
|
||||
step = function(game, dir)
|
||||
self_:cycle(game, dir)
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
-- This mod's settings, in categories, on menus of their own.
|
||||
--
|
||||
-- ------- why the flat list had to end
|
||||
--
|
||||
-- Every setting used to be spliced straight into the engine's OPTIONS list,
|
||||
-- one unbroken block of fourteen rows after the pipeline rows. OptionRows
|
||||
-- shows FOUR boxes at a time (src/ui/OptionRows.VISIBLE), so that block alone
|
||||
-- was four screens of scrolling inside a list that already carried twenty
|
||||
-- engine rows -- and a player looking for SHADOWS had to know it was in there
|
||||
-- somewhere, past the wireframe and the horizon bend.
|
||||
--
|
||||
-- The engine has no grouping to borrow: a row descriptor is
|
||||
-- { id, label, value, step, activate } and nothing else. No headers, no
|
||||
-- sections, no pages. What it DOES have is `activate`, and a state stack that
|
||||
-- any state may push onto -- which is how the engine's own MODS and CONTROLS
|
||||
-- rows work (src/ui/OptionsMenu.lua). So the categories are real screens.
|
||||
--
|
||||
-- ------- how the split was chosen
|
||||
--
|
||||
-- Not invented here: the mod already sorted its own settings, in the `full`
|
||||
-- flag on each SETTINGS entry. `full` marks a row the FULL preset does NOT
|
||||
-- take away, and the reasoning written next to each one is always the same
|
||||
-- -- this is a question about the HARDWARE, or about the GAME, not a knob on
|
||||
-- the diorama FULL is a preset for.
|
||||
--
|
||||
-- So 3D WORLD is exactly the set FULL owns, which is why it needs no special
|
||||
-- case to disappear under FULL: every child filters itself out and the
|
||||
-- category goes with them (see rows). PERFORMANCE is the three rows marked
|
||||
-- `full` for cost -- FOREST FX among them, on its own comment's reasoning
|
||||
-- ("`full` for the AA reason: additive shafts are fill rate"). BATTLES and VR
|
||||
-- are the two features that are not about the look at all.
|
||||
--
|
||||
-- ------- what did NOT change
|
||||
--
|
||||
-- Nothing that persists. Every ModSetting keeps its key, its ladder and its
|
||||
-- row id, so options.lua is byte-identical for a player who upgrades and
|
||||
-- changes nothing -- see lib/ModSetting.lua for why the key is the only
|
||||
-- identity a setting has. The hotkeys are untouched too, which is what makes
|
||||
-- the nesting affordable: a buried row is still one keypress away.
|
||||
|
||||
-- the mod namespace (see main.lua)
|
||||
local V = ...
|
||||
|
||||
local OptionRows = require("src.ui.OptionRows")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
|
||||
local SettingsMenu = {}
|
||||
SettingsMenu.__index = SettingsMenu
|
||||
|
||||
-- Opaque like the OPTIONS menu it sits on: the screen underneath is fully
|
||||
-- covered, so there is no reason to pay for drawing it.
|
||||
SettingsMenu.isOpaque = true
|
||||
|
||||
SettingsMenu.ROOT = "root"
|
||||
SettingsMenu.ROOT_LABEL = "DRAMATIC SHAPE.."
|
||||
|
||||
-- Row ids live in a namespace of their own -- "menu." rather than a setting
|
||||
-- key -- so they can never collide with the "DRAMATIC_SHAPE:<key>" ids the
|
||||
-- settings rows have carried since the beginning.
|
||||
function SettingsMenu.id(catId)
|
||||
return "DRAMATIC_SHAPE:menu." .. catId
|
||||
end
|
||||
|
||||
-- ------- the categories, in menu order
|
||||
--
|
||||
-- `summary` is the second line of the category's own row, the way MODS reads
|
||||
-- "%d INSTALLED" on the engine's menu. Where one setting IS the category --
|
||||
-- 3D-BTL for the battles, VR for the headset -- it says that setting's
|
||||
-- current rung, which is the thing a player actually wants to know without
|
||||
-- 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 = "battles", label = "BATTLES..",
|
||||
summary = function() return V.require("OverworldBattle").setting:valueLabel() end },
|
||||
{ id = "perf", label = "PERFORMANCE.." },
|
||||
{ id = "vr", label = "VR..",
|
||||
summary = function() return V.require("VR").setting:valueLabel() end },
|
||||
}
|
||||
|
||||
-- ------- what the menus are built from
|
||||
--
|
||||
-- SETTINGS lives in main.lua, next to the help text that goes with each row
|
||||
-- and the comments explaining every `when` and `full`. It is handed here
|
||||
-- rather than moved, so this file stays about PRESENTATION and that one stays
|
||||
-- the single place the mod's settings are declared.
|
||||
local settings = {}
|
||||
local pipelineRows = {}
|
||||
|
||||
function SettingsMenu.define(list)
|
||||
settings = list or {}
|
||||
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
|
||||
-- save.options.pipelines through their own step functions, and rebuilding
|
||||
-- them here would be a second implementation of a thing the engine already
|
||||
-- got right.
|
||||
function SettingsMenu.setPipelineRows(rows)
|
||||
pipelineRows = rows or {}
|
||||
end
|
||||
|
||||
-- ------- a step here has the same consequences as a step anywhere
|
||||
--
|
||||
-- Two of these settings PIN something else when they change: 3D-BTL holds
|
||||
-- BATTLE LAYOUT at OG while a fight can be staged on the map, and FULL holds
|
||||
-- DAYTIME at SYNC while it owns that row. Both used to happen because every
|
||||
-- step on the OPTIONS menu reran the ui.options.rows hook, which does the
|
||||
-- pinning on its way past.
|
||||
--
|
||||
-- Nothing reruns that hook from in here, so the pin is asked for directly.
|
||||
-- main.lua supplies it, because WHICH values follow which is a question about
|
||||
-- the mod's settings and not about the menu they are on.
|
||||
local onChanged = nil
|
||||
|
||||
function SettingsMenu.setOnChanged(fn)
|
||||
onChanged = fn
|
||||
end
|
||||
|
||||
local function isFull()
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
return V.require("VoxelState").isFull(Pipelines.level("voxel"))
|
||||
end
|
||||
|
||||
-- The one rule that decides whether a setting is on a menu, lifted unchanged
|
||||
-- from the options hook it used to live in.
|
||||
--
|
||||
-- FULL: a preset that owns the look, so the rows that describe the look go
|
||||
-- with it. And a row whose own switch is off the table this frame (BACK
|
||||
-- SPRITES, which needs a staged fight to be about) is left off with it. The
|
||||
-- mod manager's page carries every one of them either way.
|
||||
local function offered(entry, full)
|
||||
return (entry.full or not full) and (not entry.when or entry.when())
|
||||
end
|
||||
|
||||
-- The rows of one category, or of the root menu. PURE -- no state, no stack,
|
||||
-- no side effects -- so a caller that only wants to know what is on a menu
|
||||
-- (a test, or the root menu asking whether a category has anything in it)
|
||||
-- does not have to push a screen to find out.
|
||||
function SettingsMenu.rows(catId, game)
|
||||
local full = isFull()
|
||||
local out = {}
|
||||
if catId == SettingsMenu.ROOT then
|
||||
for _, row in ipairs(pipelineRows) do
|
||||
-- FULL owns the blur exactly as it owns the wireframe and the horizon
|
||||
-- bend, so T-SHIFT comes off with them
|
||||
if not (full and row.id == "pipeline:tiltshift") then
|
||||
out[#out + 1] = row
|
||||
end
|
||||
end
|
||||
for _, cat in ipairs(SettingsMenu.CATEGORIES) do
|
||||
local kids = SettingsMenu.rows(cat.id, game)
|
||||
-- An EMPTY category is not offered. This is the whole of what makes
|
||||
-- 3D WORLD disappear under FULL and VR disappear off Windows: no
|
||||
-- special case, just nothing left inside to open.
|
||||
if kids[1] then
|
||||
out[#out + 1] = {
|
||||
id = SettingsMenu.id(cat.id),
|
||||
label = cat.label,
|
||||
value = cat.summary
|
||||
or function() return ("%d SETTINGS"):format(#SettingsMenu.rows(cat.id, game)) end,
|
||||
activate = function(g)
|
||||
g.stack:push(SettingsMenu.new(g, cat.id))
|
||||
end,
|
||||
}
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
for _, entry in ipairs(settings) do
|
||||
if entry.cat == catId and offered(entry, full) then
|
||||
out[#out + 1] = entry[1]:row()
|
||||
end
|
||||
end
|
||||
-- and the ROM import, which is an ACTION and not a setting: there is no
|
||||
-- rung to store, nothing for the mod manager's page to persist and nothing
|
||||
-- to restore on the next boot, so it is appended here rather than living in
|
||||
-- SETTINGS. It sits under the battles because the STADIUM rungs of 3D-BTL
|
||||
-- are what it unlocks.
|
||||
if catId == "battles" then
|
||||
local ok, importRow = pcall(function()
|
||||
return V.require("StadiumRomPick").row()
|
||||
end)
|
||||
if ok and importRow then out[#out + 1] = importRow end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- ------- red ink for the mod's row on the OPTIONS menu
|
||||
--
|
||||
-- love.graphics.setColor CANNOT do this, and it is worth writing down why so
|
||||
-- nobody spends an afternoon on it. Twice over:
|
||||
--
|
||||
-- 1. The glyph atlas is BLACK ink on transparent (tools/extract/font.py),
|
||||
-- and Font.drawCode is a plain love.graphics.draw, which LOVE tints
|
||||
-- MULTIPLICATIVELY. black x red is black.
|
||||
-- 2. Even if it drew red, the palette shader (PaletteFX.shader) keys on the
|
||||
-- RED CHANNEL alone and throws G and B away -- r > 0.83 ? c0 : ... So a
|
||||
-- red pixel lands in c0, the LIGHTEST slot: white text on white paper.
|
||||
--
|
||||
-- What actually happens on this screen is that setColor picks a SHADE and the
|
||||
-- zone palette picks the COLOR. Black text is c3 and the white box fill is
|
||||
-- c0, so a zone whose c3 is red draws red text on paper that has not moved.
|
||||
-- The engine does the same thing for the party menu's HP bars
|
||||
-- (src/ui/PartyMenu.lua), which is the pattern this follows.
|
||||
SettingsMenu.INK = { 255, 0, 0 }
|
||||
|
||||
-- Built by copying MEWMON -- the palette the OPTIONS menu already wears --
|
||||
-- and replacing ONLY the ink slot, rather than inventing four colors. Red,
|
||||
-- Blue and Yellow ship different MEWMON tables, and this way the paper under
|
||||
-- the row is the same white as the row above it in all three.
|
||||
function SettingsMenu.redPalette(data)
|
||||
local base = PaletteFX.pal(data, "MEWMON")
|
||||
if not base then return nil end
|
||||
local out = { base[1], base[2], base[3], base[4] }
|
||||
-- SGB INV REVERSES the table (PaletteFX.effectiveColors, INV_MAP), so under
|
||||
-- it the ink is the first slot and the paper the last. Put the red where it
|
||||
-- will land on the INK either way: without this the row draws as a solid
|
||||
-- red block with white letters cut out of it.
|
||||
--
|
||||
-- The other modes need nothing. OG, OG INV and CLASSIC discard the table
|
||||
-- outright and substitute their own, so the row simply draws monochrome --
|
||||
-- which is correct: the player asked for a screen with no colors in it.
|
||||
out[PaletteFX.mode == "gbc_inv" and 1 or 4] = SettingsMenu.INK
|
||||
return out
|
||||
end
|
||||
|
||||
-- The two TEXT lines of the row in `slot` (1..OptionRows.VISIBLE), and only
|
||||
-- those. OptionRows.draw puts the label at x=16 and the value at x=24 -- tiles
|
||||
-- 2 and 3 -- on the second and third rows of each four-tile box. Tiles 0 and
|
||||
-- 19 are the box's own borders and tile 1 is the cursor, and all three are
|
||||
-- black glyphs that would turn red along with the text if the band spanned
|
||||
-- the whole row.
|
||||
function SettingsMenu.rowZone(data, slot)
|
||||
local pal = SettingsMenu.redPalette(data)
|
||||
if not pal then return nil end
|
||||
local top = (slot - 1) * 4 + 1
|
||||
return PaletteFX.zone(pal, 2, top, 18, top + 1)
|
||||
end
|
||||
|
||||
-- ------- the screen
|
||||
--
|
||||
-- Deliberately NOT an OptionsMenu instance, though the update loop below is
|
||||
-- modelled on its. main.lua monkey-patches OptionsMenu.update on the CLASS,
|
||||
-- and that patch rebuilds self.rows from OptionsMenu.new whenever the voxel
|
||||
-- level or the battle rows change -- which would replace a submenu's rows
|
||||
-- with the whole top-level OPTIONS list under the player's cursor. A state of
|
||||
-- our own cannot be caught by it.
|
||||
--
|
||||
-- It still renders through OptionRows, so it is the same four boxes, the same
|
||||
-- cursor and the same bottom line as every other menu in the game.
|
||||
function SettingsMenu.new(game, catId)
|
||||
local self = setmetatable({
|
||||
game = game,
|
||||
cat = catId or SettingsMenu.ROOT,
|
||||
index = 1,
|
||||
scroll = 0,
|
||||
}, SettingsMenu)
|
||||
self.rows = SettingsMenu.rows(self.cat, game)
|
||||
self.sig = SettingsMenu.signature(self.rows)
|
||||
return self
|
||||
end
|
||||
|
||||
function SettingsMenu.signature(rows)
|
||||
local ids = {}
|
||||
for i, row in ipairs(rows) do ids[i] = tostring(row.id) end
|
||||
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.
|
||||
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"
|
||||
end
|
||||
|
||||
-- A category's contents can change while the player is looking at them: 3D-BTL
|
||||
-- gives and takes BACK SPRITES, VR gives and takes SMOOTH TURN, and stepping
|
||||
-- VOXEL onto FULL empties 3D WORLD outright. Rebuilt only when the LIST
|
||||
-- actually differs, so the common case -- every other rung of every other row
|
||||
-- -- costs one string compare.
|
||||
function SettingsMenu:refresh()
|
||||
local rows = SettingsMenu.rows(self.cat, self.game)
|
||||
local sig = SettingsMenu.signature(rows)
|
||||
if sig == self.sig then return end
|
||||
-- Follow the row the cursor was ON rather than the slot it was in: a row
|
||||
-- can appear ABOVE the one just used, which would otherwise slide the
|
||||
-- cursor onto its neighbour. The bottom line follows itself.
|
||||
local wasBack = self.index > #self.rows
|
||||
local wasOn = self.rows[self.index] and self.rows[self.index].id
|
||||
self.rows, self.sig = rows, sig
|
||||
self.index, self.scroll = 1, 0
|
||||
if wasBack then
|
||||
self.index = #rows + 1
|
||||
else
|
||||
for i, row in ipairs(rows) do
|
||||
if wasOn and row.id == wasOn then self.index = i break end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function pop(self)
|
||||
local stack = self.game and self.game.stack
|
||||
if self.game and 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
|
||||
end
|
||||
|
||||
-- The engine's own options loop (src/ui/OptionsMenu.update), including its
|
||||
-- two conventions worth naming: `activate` SHADOWS `step` and fires on A
|
||||
-- alone, and the bottom line is a synthetic index past the end of the list
|
||||
-- rather than a row, so nothing a category contains can orphan the way out.
|
||||
function SettingsMenu:update()
|
||||
local input = self.game and self.game.input
|
||||
if not input then return end
|
||||
local rows = self.rows
|
||||
local back = #rows + 1
|
||||
local changed = false
|
||||
if input:wasPressed("up") then
|
||||
self.index = self.index - 1
|
||||
if self.index < 1 then self.index = back end
|
||||
elseif input:wasPressed("down") then
|
||||
self.index = self.index + 1
|
||||
if self.index > back then self.index = 1 end
|
||||
elseif input:wasPressed("left") or input:wasPressed("right")
|
||||
or input:wasPressed("a") then
|
||||
local dir = input:wasPressed("left") and -1 or 1
|
||||
local row = rows[self.index]
|
||||
if row and row.activate then
|
||||
if input:wasPressed("a") then row.activate(self.game) end
|
||||
elseif row and row.step then
|
||||
changed = row.step(self.game, dir) and true or false
|
||||
elseif input:wasPressed("a") then
|
||||
pop(self)
|
||||
return
|
||||
end
|
||||
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
|
||||
-- left it, with its own onCancel still to fire when they leave THAT.
|
||||
pop(self)
|
||||
return
|
||||
end
|
||||
if changed then
|
||||
-- before the rebuild, not after: pinning can itself change which rows are
|
||||
-- offered (3D-BTL switched on takes BACK SPRITES from off the table to on
|
||||
-- it), and refresh has to see the settled answer
|
||||
if onChanged then pcall(onChanged, self.game) end
|
||||
if self.game.writeOptions then
|
||||
pcall(self.game.writeOptions, self.game)
|
||||
end
|
||||
end
|
||||
self:refresh()
|
||||
self.scroll = OptionRows.clampScroll(self.index, self.scroll, #self.rows,
|
||||
#self.rows + 1)
|
||||
end
|
||||
|
||||
function SettingsMenu:draw()
|
||||
OptionRows.draw(self.game, self.rows, self.index, self.scroll,
|
||||
self:backLabel(), #self.rows + 1)
|
||||
end
|
||||
|
||||
-- REQUIRED, even though nothing here is red.
|
||||
--
|
||||
-- Game:draw walks the stack from the top and stops at the first state that
|
||||
-- HAS this method, not the first that answers something. Without one of our
|
||||
-- own the walk would fall through to the OPTIONS menu underneath -- whose
|
||||
-- sgbPalettes main.lua has patched to paint the mod's row red -- and that
|
||||
-- zone is addressed by SLOT, so it would land on whatever this menu happens
|
||||
-- to be showing in the same box.
|
||||
--
|
||||
-- MEWMON is what the OPTIONS menu wears, so a submenu is the same paper.
|
||||
function SettingsMenu:sgbPalettes(game)
|
||||
return PaletteFX.wholeNamed(game.data, "MEWMON")
|
||||
end
|
||||
|
||||
return SettingsMenu
|
||||
@@ -98,6 +98,9 @@ local FirstPerson = V.require("FirstPerson")
|
||||
local FreeMove = V.require("FreeMove")
|
||||
local CamControl = V.require("CamControl")
|
||||
local VR = V.require("VR")
|
||||
-- the mod's settings menus: the categories, the screens they open, and the
|
||||
-- red ink that marks this mod's one row on the engine's OPTIONS list
|
||||
local SettingsMenu = V.require("SettingsMenu")
|
||||
-- HORDE MODE: the konami code's minigame. Horde owns the state machine and
|
||||
-- every hook; the other four are the gun, the crowd, the readout and the
|
||||
-- chip-synthesized sounds it fires. See lib/Horde.lua for the whole design.
|
||||
@@ -449,8 +452,26 @@ local function stagedBattles()
|
||||
return OverworldBattle.enabled()
|
||||
end
|
||||
|
||||
-- ------- this mod's settings, grouped the way the menus present them
|
||||
--
|
||||
-- One entry per setting: the ModSetting itself, the help text the mod
|
||||
-- manager's page carries, and the fields that decide where it is offered.
|
||||
--
|
||||
-- cat which of SettingsMenu's categories the row lives on. The table is
|
||||
-- kept in category order as well, so the mod manager's own page --
|
||||
-- which has no categories to give and lists every row flat -- at
|
||||
-- least keeps related settings next to each other.
|
||||
-- when a predicate. The row is off the menu entirely while it answers
|
||||
-- false, because a row that decides nothing reads as a broken mod.
|
||||
-- full the row SURVIVES the FULL preset. FULL owns the look, so a row
|
||||
-- goes with it by default; `full` marks the ones that were never
|
||||
-- about the look. SettingsMenu leans on this and needs no rule of
|
||||
-- its own: 3D WORLD is exactly the rows WITHOUT it, so that whole
|
||||
-- category empties out under FULL and takes itself off the menu.
|
||||
local SETTINGS = {
|
||||
{ VoxelGrid.setting, "One-pixel wireframe along every voxel edge." },
|
||||
-- ------- 3D WORLD -- the diorama's own knobs, every one of them FULL's
|
||||
{ VoxelGrid.setting, "One-pixel wireframe along every voxel edge.",
|
||||
cat = "world" },
|
||||
{ WorldCurve.setting,
|
||||
"Bend the world down over the horizon, Animal Crossing style. 1 is a "
|
||||
.. "hint of roll at the frame edges and 2 is the classic read; 3 is as "
|
||||
@@ -458,7 +479,8 @@ local SETTINGS = {
|
||||
.. "walk into. 4 and 5 are past that on purpose and they are for a "
|
||||
.. "headset's DIORAMA, where the world is a model being looked at "
|
||||
.. "rather than walked around in -- 5 curls it into a half sphere, a "
|
||||
.. "town on top of its own little planet." },
|
||||
.. "town on top of its own little planet.",
|
||||
cat = "world" },
|
||||
{ ViewBox.setting,
|
||||
"How much of the map the camera bothers to draw. FIT is exactly the "
|
||||
.. "ground on screen and no more -- the shape a tilted camera really "
|
||||
@@ -472,24 +494,23 @@ local SETTINGS = {
|
||||
.. "closest, WIDE through WIDEST push the world's edge further out, "
|
||||
.. "and OFF stops cutting entirely. Not on 1ST or 3RD -- you are "
|
||||
.. "standing in the world there -- and the box opens out and away as "
|
||||
.. "the camera dives in." },
|
||||
.. "the camera dives in.",
|
||||
cat = "world" },
|
||||
{ Water.setting,
|
||||
"Reflections on water. FULL adds screen-space reflections of the "
|
||||
.. "shoreline, the trees and the buildings behind it; SKY is the sky, "
|
||||
.. "the sun and the moon alone, which is most of the look for a "
|
||||
.. "fraction of the cost." },
|
||||
-- `full` for the AA reason: additive shafts are fill rate, and under 4X
|
||||
-- supersampling that is a question about the hardware, not the look.
|
||||
{ ForestAtmos.setting,
|
||||
"The air of the deep woods (Viridian Forest): a ground haze, and "
|
||||
.. "volumetric light let down through the unseen canopy overhead -- "
|
||||
.. "gold spears of sun by day, silver moon rays at night, pollen "
|
||||
.. "drifting through the beams and fireflies once they cool. LOW "
|
||||
.. "keeps the haze, halves the beam march and stands the particles "
|
||||
.. "down. On a phone the row offers LOW alone: the beams need a "
|
||||
.. "depth texture the pass can read back, and no mobile driver here "
|
||||
.. "grants one.",
|
||||
full = true },
|
||||
.. "fraction of the cost.",
|
||||
cat = "world" },
|
||||
{ DayNight.setting,
|
||||
"What time it is outdoors: pin the sky to DAY, NIGHT, DUSK or DAWN, "
|
||||
.. "let CYCLE run it -- ten minutes of sun, ten of moon, with the "
|
||||
.. "shadows, the sky and the light following -- or SYNC it to the "
|
||||
.. "clock on the wall, so Kanto's evening falls when yours does.",
|
||||
cat = "world" },
|
||||
|
||||
-- ------- BATTLES -- what a fight is drawn over, and how it is played
|
||||
--
|
||||
-- `full` marks a row FULL does not take away. FULL owns the diorama's own
|
||||
-- knobs; what a battle is drawn over, and how it is framed, are not that.
|
||||
-- Off the OPTIONS menu while VR is on: the headset REQUIRES staged
|
||||
@@ -509,6 +530,7 @@ local SETTINGS = {
|
||||
.. "Pokemon Stadium (US) 1.0 ROM of your own -- import it from the "
|
||||
.. "STADIUM ROM row, or drop it in the baseroms folder and restart. No "
|
||||
.. "other version works: the reader is keyed to that one cartridge.",
|
||||
cat = "battles",
|
||||
when = function() return not VR.enabled() end, full = true },
|
||||
-- Only offered while a fight can actually be staged on the map: with 3D-BTL
|
||||
-- off the engine draws the classic screen, which is this row's ON already,
|
||||
@@ -517,6 +539,7 @@ local SETTINGS = {
|
||||
"Keep your own Pokemon on the battle menu, seen from behind in its "
|
||||
.. "original slot, instead of standing it on the map facing the foe. "
|
||||
.. "The foe is still out there on its own tile.",
|
||||
cat = "battles",
|
||||
when = function() return stagedBattles() and not VR.enabled() end,
|
||||
full = true },
|
||||
-- `full` like the battle rows: this is a GAMEPLAY mode, not a knob on
|
||||
@@ -534,12 +557,26 @@ local SETTINGS = {
|
||||
.. "throws, new species and your running catch combo. Needs 3D-BTL "
|
||||
.. "on; anywhere the staged fight cannot stand, balls quietly throw "
|
||||
.. "the classic way.",
|
||||
full = true },
|
||||
{ DayNight.setting,
|
||||
"What time it is outdoors: pin the sky to DAY, NIGHT, DUSK or DAWN, "
|
||||
.. "let CYCLE run it -- ten minutes of sun, ten of moon, with the "
|
||||
.. "shadows, the sky and the light following -- or SYNC it to the "
|
||||
.. "clock on the wall, so Kanto's evening falls when yours does." },
|
||||
cat = "battles", full = true },
|
||||
|
||||
-- ------- PERFORMANCE -- what the look COSTS, which is a different question
|
||||
--
|
||||
-- All three are `full`, and all three for the same reason: FULL is a preset
|
||||
-- for the diorama, not a licence to spend whatever the machine it happens
|
||||
-- to be running on has got. The player decides what their hardware can
|
||||
-- carry, from inside FULL like anywhere else.
|
||||
-- `full` for the AA reason: additive shafts are fill rate, and under 4X
|
||||
-- supersampling that is a question about the hardware, not the look.
|
||||
{ ForestAtmos.setting,
|
||||
"The air of the deep woods (Viridian Forest): a ground haze, and "
|
||||
.. "volumetric light let down through the unseen canopy overhead -- "
|
||||
.. "gold spears of sun by day, silver moon rays at night, pollen "
|
||||
.. "drifting through the beams and fireflies once they cool. LOW "
|
||||
.. "keeps the haze, halves the beam march and stands the particles "
|
||||
.. "down. On a phone the row offers LOW alone: the beams need a "
|
||||
.. "depth texture the pass can read back, and no mobile driver here "
|
||||
.. "grants one.",
|
||||
cat = "perf", full = true },
|
||||
-- `full` on AA's reasoning below, and for the same reason: the sun's pass
|
||||
-- is the most expensive thing in the frame after the geometry, so this is
|
||||
-- a question about the machine rather than a knob on the diorama, and it
|
||||
@@ -554,13 +591,9 @@ local SETTINGS = {
|
||||
.. "phone or an old machine. OFF is no shadow at all, the flat drop "
|
||||
.. "shadows under characters included, and the forest's light shafts go "
|
||||
.. "with it: the beams are lit by the sun's own map.",
|
||||
full = true },
|
||||
cat = "perf", full = true },
|
||||
-- Marked `full` for the opposite reason the battle rows are: this is not a
|
||||
-- knob on the look at all, it is what the look COSTS. FULL is a preset for
|
||||
-- the diorama, not a licence to spend four times the fill rate on the
|
||||
-- machine it happens to be running on, so it neither sets this nor takes
|
||||
-- the row away -- the player decides what their hardware can carry, from
|
||||
-- inside FULL like anywhere else.
|
||||
-- knob on the look at all, it is what the look COSTS.
|
||||
{ AntiAlias.setting,
|
||||
"Smooth the stair-stepped edges of the 3D world -- roof ridges, ledge "
|
||||
.. "lips, a tree against the sky -- by rendering the diorama larger than "
|
||||
@@ -569,7 +602,10 @@ local SETTINGS = {
|
||||
.. "reads smoother rather than sharper. 2X costs half again as many "
|
||||
.. "pixels in each direction and 4X twice, which makes this the most "
|
||||
.. "expensive row in the mod.",
|
||||
full = true },
|
||||
cat = "perf", full = true },
|
||||
|
||||
-- ------- VR -- the headset, and the one comfort knob that is only its
|
||||
--
|
||||
-- `full` for the same reason as AA: not a knob on the look, a question
|
||||
-- about the hardware on the desk.
|
||||
{ VR.setting,
|
||||
@@ -588,6 +624,7 @@ local SETTINGS = {
|
||||
.. "Menus and dialogs float on a panel. Needs a Windows OpenXR runtime "
|
||||
.. "and the mod running from a real folder; without them the row stays "
|
||||
.. "and the game stays flat, with the reason on the console.",
|
||||
cat = "vr",
|
||||
-- on Windows the row stays even when a runtime is missing (the console
|
||||
-- says why); off Windows -- mobile above all -- there is no VR to have
|
||||
-- and the row does not exist
|
||||
@@ -601,12 +638,15 @@ local SETTINGS = {
|
||||
.. "world past a head that did not move, which is the most reliable way "
|
||||
.. "to make somebody ill in a headset. Turn it on if you have your sea "
|
||||
.. "legs and want the continuity.",
|
||||
cat = "vr",
|
||||
-- and only under STANDARD: the stick turns a HEAD, and neither diorama
|
||||
-- mode has the player standing in the world to be turned
|
||||
when = function() return VR.enabled() and not VR.dioramaMode() end,
|
||||
full = true },
|
||||
}
|
||||
|
||||
SettingsMenu.define(SETTINGS)
|
||||
|
||||
local schema = {}
|
||||
for _, entry in ipairs(SETTINGS) do
|
||||
-- the VR rows are absent from the mod manager's page too where the
|
||||
@@ -783,29 +823,31 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
-- ------- the mode's rows, kept together
|
||||
-- ------- the mode's rows, on menus of their own
|
||||
--
|
||||
-- The engine splices a pipeline's row in beside TILT, because a display mode
|
||||
-- belongs with the other display modes; a mod's own ui.options.rows
|
||||
-- additions land at the END of the list. That left this mod's four rows in
|
||||
-- two places with unrelated engine rows between them, which reads as two
|
||||
-- unrelated features rather than one mode with settings.
|
||||
-- This mod used to put FOURTEEN rows on the engine's OPTIONS list, in one
|
||||
-- block spliced in beside the pipeline rows. OptionRows shows four boxes at a
|
||||
-- time, so that was four screens of scrolling inside a list that already
|
||||
-- carried twenty engine rows, and finding SHADOWS meant knowing it was in
|
||||
-- there past the wireframe and the horizon bend.
|
||||
--
|
||||
-- So the plain settings are inserted directly after the last of this mod's
|
||||
-- PIPELINE rows instead of appended. Nothing else moves: the block lands
|
||||
-- where the engine already decided display modes go.
|
||||
local function insertGrouped(out, extra)
|
||||
local anchor = nil
|
||||
for i, row in ipairs(out) do
|
||||
local id = type(row) == "table" and row.id
|
||||
if id == "pipeline:voxel" or id == "pipeline:tiltshift" then anchor = i end
|
||||
-- Now there is ONE row, and it leads the list. What it opens -- the
|
||||
-- categories, the screens, and why the split falls where it does -- is
|
||||
-- lib/SettingsMenu.lua. VOXEL and T-SHIFT go with it: they are this mod's
|
||||
-- display modes, the engine only spliced them beside TILT because it had
|
||||
-- nowhere better, and TILT is not on the menu any more anyway (see below).
|
||||
--
|
||||
-- Two things it takes to move a pipeline row: the engine's descriptor is
|
||||
-- captured on the way past and handed to SettingsMenu VERBATIM -- it persists
|
||||
-- through its own step function into save.options.pipelines, and rebuilding
|
||||
-- it here would be a second implementation of something the engine already
|
||||
-- got right -- and the row is then dropped from the top-level list so it is
|
||||
-- not in two places at once.
|
||||
local function captureRow(out, id)
|
||||
for _, row in ipairs(out) do
|
||||
if type(row) == "table" and row.id == id then return row end
|
||||
end
|
||||
if not anchor then
|
||||
for _, row in ipairs(extra) do out[#out + 1] = row end
|
||||
return out
|
||||
end
|
||||
for i, row in ipairs(extra) do table.insert(out, anchor + i, row) end
|
||||
return out
|
||||
return nil
|
||||
end
|
||||
|
||||
-- FULL owns the settings that describe the LOOK, so while it is selected those
|
||||
@@ -875,6 +917,25 @@ local function pinEngineFx(game)
|
||||
if changed and game.writeOptions then pcall(game.writeOptions, game) end
|
||||
end
|
||||
|
||||
-- ------- the values that follow other values
|
||||
--
|
||||
-- Two settings hold a third in place. 3D-BTL pins BATTLE LAYOUT to OG while a
|
||||
-- fight can be staged on the map, and FULL pins DAYTIME to SYNC while it owns
|
||||
-- that row. Both pins used to be a side effect of the rows hook, which every
|
||||
-- step on the OPTIONS menu reran -- so they happened whether or not the step
|
||||
-- was the one that mattered, and nothing had to name them.
|
||||
--
|
||||
-- Now a step can happen on the mod's own menu, where no hook runs, or on the
|
||||
-- mod manager's page, where one never did. So the pinning is a function, and
|
||||
-- all three routes ask for it.
|
||||
local function pinDependents(game)
|
||||
if stagedBattles() then OverworldBattle.forceOG(game) end
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
if Voxel.isFull(Pipelines.level("voxel")) then DayNight.forceSync(game) end
|
||||
end
|
||||
|
||||
SettingsMenu.setOnChanged(pinDependents)
|
||||
|
||||
-- call next() first and decorate what comes back, so every other mod's
|
||||
-- rows survive this one
|
||||
mod.hooks:wrap("ui.options.rows", function(next, game, rows)
|
||||
@@ -901,46 +962,50 @@ mod.hooks:wrap("ui.options.rows", function(next, game, rows)
|
||||
OverworldBattle.forceOG(game)
|
||||
dropRow(out, "battleLayout")
|
||||
end
|
||||
local full = Voxel.isFull(Pipelines.level("voxel"))
|
||||
if full then
|
||||
if Voxel.isFull(Pipelines.level("voxel")) then
|
||||
-- FULL owns the rows that PARAMETERISE the diorama -- the wireframe, the
|
||||
-- horizon bend, the blur, the hour -- so those come off the menu and
|
||||
-- DAYTIME is held at SYNC while its row is unreachable.
|
||||
-- horizon bend, the blur, the hour -- so DAYTIME is held at SYNC while its
|
||||
-- row is unreachable. The rows themselves come off inside SettingsMenu,
|
||||
-- which is where they live now: T-SHIFT with the wireframe and the bend,
|
||||
-- and each of them by the same `full` rule rather than by name.
|
||||
DayNight.forceSync(game)
|
||||
dropRow(out, "pipeline:tiltshift")
|
||||
end
|
||||
local extra = {}
|
||||
for _, entry in ipairs(SETTINGS) do
|
||||
-- Two things decide whether a row is offered.
|
||||
--
|
||||
-- FULL: a preset that owns the look, so the rows that describe the look go
|
||||
-- with it. The BATTLE rows are not that -- 3D-BTL decides what a fight is
|
||||
-- drawn OVER and BACK SPRITES how it is framed, and neither is a knob on
|
||||
-- the diorama FULL is a preset for. FULL still SETS them on arrival (see
|
||||
-- applyFull); it does not hold them, so leaving them on the menu is the
|
||||
-- difference between a preset and a lock.
|
||||
--
|
||||
-- And a row whose own switch is off the table this frame (BACK SPRITES,
|
||||
-- which needs a staged fight to be about) is left off with it. The mod
|
||||
-- manager's page carries every one of them either way.
|
||||
local offered = (entry.full or not full)
|
||||
and (not entry.when or entry.when())
|
||||
if offered then extra[#extra + 1] = entry[1]:row() end
|
||||
-- The two pipeline rows move INTO the mod's own root menu: captured as the
|
||||
-- engine built them, then dropped from here so they are not in two places.
|
||||
local captured, voxelRow = {}, nil
|
||||
for _, id in ipairs({ "pipeline:voxel", "pipeline:tiltshift" }) do
|
||||
local row = captureRow(out, id)
|
||||
-- a pipeline the registry refused is simply not there, and the menu says
|
||||
-- so by not offering it rather than by offering a hole
|
||||
if row then captured[#captured + 1] = row end
|
||||
if id == "pipeline:voxel" then voxelRow = row end
|
||||
dropRow(out, id)
|
||||
end
|
||||
-- and the ROM import, which is an ACTION and not a setting: there is no
|
||||
-- rung to store, nothing for the mod manager's page to persist and nothing
|
||||
-- to restore on the next boot, so it is appended here rather than living in
|
||||
-- SETTINGS. nil on a platform with no file dialog, which takes it off the
|
||||
-- menu rather than offering a button that cannot do anything.
|
||||
-- On EVERY platform. Where there is no file dialog it says WHERE? and
|
||||
-- shows the folder to put the cartridge in, which is the one thing a
|
||||
-- player on a phone could not otherwise find out -- the row used to vanish
|
||||
-- there, which reads as the feature being missing rather than manual.
|
||||
local okPick, importRow = pcall(function()
|
||||
return V.require("StadiumRomPick").row()
|
||||
end)
|
||||
if okPick and importRow then extra[#extra + 1] = importRow end
|
||||
return insertGrouped(out, extra)
|
||||
SettingsMenu.setPipelineRows(captured)
|
||||
-- ------- one row, and it leads the list
|
||||
--
|
||||
-- At the TOP rather than spliced in beside the display modes it used to sit
|
||||
-- with. This is a mod that replaces the whole look of the game, and a player
|
||||
-- who installed it and went looking for its settings should not have to
|
||||
-- scroll to find out where they went -- least of all past the engine rows it
|
||||
-- has quietly taken away.
|
||||
--
|
||||
-- Inserted after next() has run, so it leads every OTHER mod's rows too. The
|
||||
-- second line is VOXEL's own value function, which makes the row say what
|
||||
-- the mode is currently doing without opening it -- and reuses the engine's
|
||||
-- label ladder rather than restating it.
|
||||
table.insert(out, 1, {
|
||||
id = SettingsMenu.id(SettingsMenu.ROOT),
|
||||
label = SettingsMenu.ROOT_LABEL,
|
||||
value = voxelRow and voxelRow.value or nil,
|
||||
-- `activate` and not `step`: the engine fires activate on A alone, and a
|
||||
-- row that OPENS something should not also answer Left and Right
|
||||
-- (src/ui/OptionsMenu.update).
|
||||
activate = function(g)
|
||||
g.stack:push(SettingsMenu.new(g, SettingsMenu.ROOT))
|
||||
end,
|
||||
})
|
||||
return out
|
||||
end)
|
||||
|
||||
-- The mod manager writes and persists on its own, so the only thing left
|
||||
@@ -951,14 +1016,11 @@ mod.events:on("mod.options_changed", function(payload)
|
||||
if payload.key == entry[1].key then entry[1]:sync(payload.value) end
|
||||
end
|
||||
-- 3D-BTL switched on from the manager's page pins BATTLE LAYOUT exactly as
|
||||
-- the OPTIONS row does. The manager persists its own value; this is the one
|
||||
-- that has to follow it.
|
||||
if stagedBattles() then OverworldBattle.forceOG() end
|
||||
-- and DAYTIME changed from the manager's page while FULL owns it snaps
|
||||
-- straight back to SYNC -- the OPTIONS row is hidden, but the manager's is
|
||||
-- not, and FULL's pin must hold against both
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
if Voxel.isFull(Pipelines.level("voxel")) then DayNight.forceSync() end
|
||||
-- the mod's own row does, and DAYTIME changed there while FULL owns it snaps
|
||||
-- straight back to SYNC -- that row is off the mod's menus under FULL, but
|
||||
-- the manager's page carries every setting unconditionally, and the pin has
|
||||
-- to hold against both.
|
||||
pinDependents()
|
||||
end)
|
||||
|
||||
-- ------- keeping the geometry in step with the world
|
||||
@@ -1058,30 +1120,55 @@ end)
|
||||
-- rerun every mod's ui.options.rows hook once per keypress. The cursor is
|
||||
-- clamped rather than reset, so it stays on the row it was just used on
|
||||
-- instead of jumping to the top when the list below it shortens.
|
||||
--
|
||||
-- Held on the INSTANCE rather than compared across one call of update, and
|
||||
-- that is not a tidying: those three rows live in a SUBMENU now, and the
|
||||
-- stack only ticks its top state (src/core/StateStack.update). So the step
|
||||
-- that changes them happens while this menu is suspended and a
|
||||
-- before/after pair taken around inner() would both be read after the fact
|
||||
-- and always agree. A signature that outlives the suspension does not.
|
||||
do
|
||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||
if not OptionsMenu.dramaticShapeFullHook then
|
||||
local OptionRows = require("src.ui.OptionRows")
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
local inner = OptionsMenu.update
|
||||
local innerPalettes = OptionsMenu.sgbPalettes
|
||||
|
||||
local function idAt(menu, index)
|
||||
local row = menu.rows and menu.rows[index or 1]
|
||||
return type(row) == "table" and row.id or nil
|
||||
end
|
||||
|
||||
-- What the row LIST depends on: whether FULL is selected (it owns the
|
||||
-- rows that describe the look), and the two switches that give and take
|
||||
-- an engine row -- 3D-BTL, which owns BATTLE LAYOUT, and VR, which hides
|
||||
-- both battle rows while it is on. Only the FULL-ness of the voxel level
|
||||
-- matters, so stepping 35 to 50 is not a change.
|
||||
local function signature()
|
||||
return string.format("%s|%s|%s",
|
||||
tostring(Voxel.isFull(Pipelines.level("voxel"))),
|
||||
tostring(OverworldBattle.enabled()), tostring(VR.enabled()))
|
||||
end
|
||||
|
||||
-- Stamped where the ROWS are built, which is the thing the signature is a
|
||||
-- signature OF. Read lazily on the first update instead and a menu opened
|
||||
-- before the change and updated after it would compare the new state
|
||||
-- against itself and never rebuild.
|
||||
local innerNew = OptionsMenu.new
|
||||
function OptionsMenu.new(game, opts)
|
||||
local menu = innerNew(game, opts)
|
||||
menu.dramaticShapeSig = signature()
|
||||
return menu
|
||||
end
|
||||
|
||||
function OptionsMenu:update(dt)
|
||||
local before = Pipelines.level("voxel")
|
||||
local hadBattles = OverworldBattle.enabled()
|
||||
-- the VR row hides the two battle rows while it is on, so stepping
|
||||
-- it changes the LIST exactly the way 3D-BTL does
|
||||
local hadVR = VR.enabled()
|
||||
local wasOn = idAt(self, self.index)
|
||||
local before = self.dramaticShapeSig or signature()
|
||||
inner(self, dt)
|
||||
local after = Pipelines.level("voxel")
|
||||
local crossedFull = after ~= before
|
||||
and (Voxel.isFull(before) or Voxel.isFull(after))
|
||||
if crossedFull or OverworldBattle.enabled() ~= hadBattles
|
||||
or VR.enabled() ~= hadVR then
|
||||
local after = signature()
|
||||
self.dramaticShapeSig = after
|
||||
if before ~= after then
|
||||
local rebuilt = OptionsMenu.new(self.game)
|
||||
self.rows = rebuilt.rows
|
||||
-- Follow the row the cursor was ON rather than the slot it was in:
|
||||
@@ -1095,6 +1182,36 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
-- ------- and the mod's own row is red
|
||||
--
|
||||
-- Why this is a palette zone and not love.graphics.setColor -- twice over
|
||||
-- -- is written out in lib/SettingsMenu.lua, next to the code that builds
|
||||
-- the palette. The short of it: setColor picks a SHADE on this screen and
|
||||
-- the zone picks the COLOR.
|
||||
--
|
||||
-- Addressed by SLOT, because the row scrolls: it leads the list, so it is
|
||||
-- normally the top box, but a player who scrolls past it must not leave a
|
||||
-- red band behind on whatever takes its place. Searched by id rather than
|
||||
-- assumed to be row 1 for the same reason -- another mod's hook running
|
||||
-- after ours could put something above it.
|
||||
function OptionsMenu:sgbPalettes(game)
|
||||
local zones = innerPalettes and innerPalettes(self, game) or nil
|
||||
local scroll = self.scroll or 0
|
||||
for slot = 1, OptionRows.VISIBLE do
|
||||
local row = self.rows and self.rows[scroll + slot]
|
||||
if type(row) == "table"
|
||||
and row.id == SettingsMenu.id(SettingsMenu.ROOT) then
|
||||
local zone = SettingsMenu.rowZone(game and game.data, slot)
|
||||
if zone then
|
||||
zones = zones or {}
|
||||
zones[#zones + 1] = zone
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
return zones
|
||||
end
|
||||
|
||||
OptionsMenu.dramaticShapeFullHook = true
|
||||
end
|
||||
end
|
||||
|
||||
+388
-87
@@ -119,6 +119,46 @@ T.eq(byLabel.VOXEL.value(), "FULL", "the row renders the current rung's label")
|
||||
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local VoxelState = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelState")
|
||||
-- ------- reaching the rows now that they are on menus of their own
|
||||
--
|
||||
-- The ui.options.rows hook contributes ONE row to the engine's list, and that
|
||||
-- row opens the mod's own screens. So a test asks SettingsMenu.rows -- which
|
||||
-- is pure on purpose, precisely so that asking what is on a menu does not mean
|
||||
-- pushing one -- rather than scanning the hook's output for a setting.
|
||||
--
|
||||
-- Keyed by id, because that is the one thing about a row that is a promise.
|
||||
-- The old positional lookups (hookedRows[7] is 3D-BTL) pinned an ORDER the
|
||||
-- menus are now free to change, and every one of them had to be rewritten to
|
||||
-- get here; none of them was testing the order on purpose.
|
||||
--
|
||||
-- One table rather than three locals: this file is one Lua chunk, a chunk has
|
||||
-- 200 local slots, and it is already spending them carefully (see the scoped
|
||||
-- sections throughout).
|
||||
local Menus = { lib = run.loader.exports.DRAMATIC_SHAPE.lib.require("SettingsMenu") }
|
||||
|
||||
function Menus.rows(catId, game)
|
||||
local ids = {}
|
||||
for _, row in ipairs(Menus.lib.rows(catId, game or { data = Data })) do
|
||||
ids[row.id] = row
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
-- every row the mod offers ANYWHERE: the root menu plus all four categories.
|
||||
-- What most of this file wants, since "is this setting on a menu at all" is
|
||||
-- the question the `when` and `full` gates actually answer.
|
||||
function Menus.offered(game)
|
||||
local ids = Menus.rows(Menus.lib.ROOT, game)
|
||||
for _, cat in ipairs(Menus.lib.CATEGORIES) do
|
||||
for id, row in pairs(Menus.rows(cat.id, game)) do ids[id] = row end
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
-- the id of a category's own row on the root menu
|
||||
function Menus.catId(catId)
|
||||
return Menus.lib.id(catId)
|
||||
end
|
||||
|
||||
-- ------- FULL is a preset that owns the rows describing the LOOK
|
||||
--
|
||||
@@ -131,36 +171,67 @@ local VoxelState = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelState")
|
||||
-- diorama the preset is a preset FOR. FULL sets them on arrival and then lets
|
||||
-- go, which is what makes it a preset rather than a lock.
|
||||
Pipelines.setLevel("voxel", VoxelState.FULL_LEVEL)
|
||||
-- scoped, like the sections below: this file is one Lua chunk and a chunk has
|
||||
-- 200 local slots, so a section that wants four borrows them
|
||||
do
|
||||
local fullRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data },
|
||||
{ { id = "tilt" }, { id = "pipeline:voxel" },
|
||||
{ id = "pipeline:tiltshift" } })
|
||||
local fullIds = {}
|
||||
for _, row in ipairs(fullRows) do fullIds[row.id] = true end
|
||||
T.check(fullIds["pipeline:voxel"], "FULL keeps the VOXEL row it lives on")
|
||||
T.check(not fullIds["pipeline:tiltshift"],
|
||||
-- what the hook leaves on the ENGINE's list: one row, and the pipeline rows
|
||||
-- gone from it because they have moved onto the mod's own root menu
|
||||
T.check(fullIds[Menus.catId(Menus.lib.ROOT)],
|
||||
"the mod contributes exactly one row to the OPTIONS menu")
|
||||
T.check(not fullIds["pipeline:voxel"],
|
||||
"and takes VOXEL off the engine's list -- it lives on the mod's menu now")
|
||||
T.check(not fullIds["pipeline:tiltshift"], "and T-SHIFT with it")
|
||||
T.eq(fullRows[1].id, Menus.catId(Menus.lib.ROOT),
|
||||
"and it LEADS the list: a mod that replaces the look of the whole game "
|
||||
.. "should not make the player scroll to find out where its settings went")
|
||||
|
||||
local fullRoot = Menus.rows(Menus.lib.ROOT)
|
||||
local fullOffered = Menus.offered()
|
||||
T.check(fullRoot["pipeline:voxel"], "FULL keeps the VOXEL row it lives on")
|
||||
T.check(not fullRoot["pipeline:tiltshift"],
|
||||
"FULL takes T-SHIFT off the menu -- it owns the blur")
|
||||
T.check(not fullIds["DRAMATIC_SHAPE:grid"], "and V-GRID")
|
||||
T.check(not fullIds["DRAMATIC_SHAPE:curve"], "and V-CURVE")
|
||||
T.check(not fullIds["DRAMATIC_SHAPE:viewbox"], "and RENDER DIST")
|
||||
T.check(not fullIds["DRAMATIC_SHAPE:daytime"], "and DAYTIME")
|
||||
T.check(not fullOffered["DRAMATIC_SHAPE:grid"], "and V-GRID")
|
||||
T.check(not fullOffered["DRAMATIC_SHAPE:curve"], "and V-CURVE")
|
||||
T.check(not fullOffered["DRAMATIC_SHAPE:viewbox"], "and RENDER DIST")
|
||||
T.check(not fullOffered["DRAMATIC_SHAPE:water"], "and WATER")
|
||||
T.check(not fullOffered["DRAMATIC_SHAPE:daytime"], "and DAYTIME")
|
||||
-- and with all five of them gone the category has nothing left in it, so it
|
||||
-- takes ITSELF off the root menu. No rule anywhere says "hide 3D WORLD under
|
||||
-- FULL" -- that category is exactly the rows FULL owns, and an empty category
|
||||
-- is not offered.
|
||||
T.check(not fullRoot[Menus.catId("world")],
|
||||
"so 3D WORLD is empty, and an empty category is not offered")
|
||||
|
||||
-- but the battle rows survive it: they are not knobs on the look, and FULL
|
||||
-- sets them once rather than holding them, so a player who wants the classic
|
||||
-- back sprite (or no staged fights at all) can still say so from inside FULL
|
||||
T.check(fullIds["DRAMATIC_SHAPE:battles"], "3D-BTL is still on the menu under FULL")
|
||||
T.check(fullIds["DRAMATIC_SHAPE:battleBack"], "and BACK SPRITES with it")
|
||||
T.check(fullOffered["DRAMATIC_SHAPE:battles"],
|
||||
"3D-BTL is still on the menu under FULL")
|
||||
T.check(fullOffered["DRAMATIC_SHAPE:battleBack"], "and BACK SPRITES with it")
|
||||
T.check(fullRoot[Menus.catId("battles")],
|
||||
"and the category that carries them is still offered")
|
||||
-- and AA, for the opposite reason: it is not a knob on the look at all, it is
|
||||
-- what the look COSTS, and only the player knows what their machine can carry
|
||||
T.check(fullIds["DRAMATIC_SHAPE:aa"], "and AA, which FULL neither sets nor owns")
|
||||
T.check(fullOffered["DRAMATIC_SHAPE:aa"],
|
||||
"and AA, which FULL neither sets nor owns")
|
||||
-- SHADOWS on the same reasoning: the sun's pass is what the look costs, and
|
||||
-- a preset for the diorama does not get to spend a second draw of the whole
|
||||
-- world on a machine that cannot carry one
|
||||
T.check(fullIds["DRAMATIC_SHAPE:shadows"],
|
||||
T.check(fullOffered["DRAMATIC_SHAPE:shadows"],
|
||||
"and SHADOWS, the other row that is a question about the hardware")
|
||||
T.check(fullRoot[Menus.catId("perf")],
|
||||
"PERFORMANCE is the category for exactly that question, and it survives too")
|
||||
-- VR survives FULL on AA's reasoning: whether a headset is on the desk is
|
||||
-- not the diorama's to decide
|
||||
T.check(fullIds["DRAMATIC_SHAPE:vr"], "and VR, likewise the hardware's question")
|
||||
T.check(fullOffered["DRAMATIC_SHAPE:vr"],
|
||||
"and VR, likewise the hardware's question")
|
||||
end
|
||||
|
||||
-- DAYTIME is not only hidden under FULL, it is HELD at SYNC: the row cannot
|
||||
-- be reached while FULL owns it, so a value changed underneath (the mod
|
||||
@@ -290,8 +361,14 @@ local fxIds = {}
|
||||
for _, row in ipairs(fxRows) do fxIds[row.id] = true end
|
||||
T.check(not fxIds["tilt"], "TILT is off the OPTIONS menu")
|
||||
T.check(not fxIds["gbcfx"], "and so is GBC FX")
|
||||
T.check(fxIds["colors"] and fxIds["pipeline:voxel"],
|
||||
T.check(fxIds["colors"],
|
||||
"with every other row the engine offered still on it")
|
||||
-- VOXEL is off this list too, but for the opposite reason: not taken away,
|
||||
-- MOVED -- onto the mod's own root menu, which the one remaining row opens
|
||||
T.check(not fxIds["pipeline:voxel"] and fxIds[Menus.catId(Menus.lib.ROOT)],
|
||||
"and VOXEL moved onto the mod's own menu rather than being dropped")
|
||||
T.check(Menus.rows(Menus.lib.ROOT)["pipeline:voxel"],
|
||||
"-- where it is still offered")
|
||||
|
||||
T.eq(fxGame.save.options.tilt, 0,
|
||||
"a save that had TILT on is pinned back to off, not left on with no row")
|
||||
@@ -310,32 +387,64 @@ T.check(not fullFxIds["tilt"] and not fullFxIds["gbcfx"],
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
end
|
||||
|
||||
-- ------- and off FULL, the rows come back, grouped with the mode
|
||||
-- ------- and off FULL, the rows come back -- on the mod's own menus
|
||||
--
|
||||
-- The engine splices a pipeline row in beside TILT and lands a mod's own
|
||||
-- additions at the END of the list, which would leave this mode's four rows
|
||||
-- in two places with unrelated rows between them.
|
||||
-- What the engine's list carries is ONE row, and it leads. What that row opens
|
||||
-- is the mod: the two pipeline rows it took off the engine's list, and the
|
||||
-- four categories.
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
do
|
||||
local grouped = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data },
|
||||
{ { id = "tilt" }, { id = "pipeline:voxel" },
|
||||
{ id = "pipeline:tiltshift" },
|
||||
{ id = "void_fill" } })
|
||||
local order = {}
|
||||
for i, row in ipairs(grouped) do order[row.id] = i end
|
||||
T.check(order["pipeline:tiltshift"] < order["DRAMATIC_SHAPE:grid"],
|
||||
"the mode's settings follow its pipeline rows")
|
||||
-- V-GRID, V-CURVE, RENDER DIST, WATER, FOREST FX, then 3D-BTL
|
||||
T.eq(order["DRAMATIC_SHAPE:battles"] - order["pipeline:tiltshift"], 6,
|
||||
"and sit in one unbroken block, not scattered to the end of the list")
|
||||
T.check(order["void_fill"] > order["DRAMATIC_SHAPE:battles"],
|
||||
"with the engine's own later rows still after them")
|
||||
-- the engine gave it four rows and got three back: three of ours are gone
|
||||
-- (tilt dropped, both pipelines moved) and one is added, at the front
|
||||
-- the engine offered four and two come back: TILT dropped, both pipeline rows
|
||||
-- moved onto the mod's own menu, and one row added at the front
|
||||
T.eq(#grouped, 2, "one row for the whole mod, and the engine's own beneath it")
|
||||
T.eq(grouped[1].id, Menus.catId(Menus.lib.ROOT), "and it leads the list")
|
||||
T.eq(grouped[2].id, "void_fill",
|
||||
"with the engine's own rows following in the order it built them")
|
||||
|
||||
local root = Menus.rows(Menus.lib.ROOT)
|
||||
T.check(root["pipeline:voxel"] and root["pipeline:tiltshift"],
|
||||
"both pipeline rows are on the mod's root menu")
|
||||
for _, cat in ipairs({ "world", "battles", "perf", "vr" }) do
|
||||
T.check(root[Menus.catId(cat)],
|
||||
("off FULL the %s category is offered"):format(cat))
|
||||
end
|
||||
|
||||
-- the split itself: each setting on the category that owns it, and nowhere
|
||||
-- else. Order inside a category is free to change; membership is the promise.
|
||||
local WHERE = {
|
||||
grid = "world", curve = "world", viewbox = "world", water = "world",
|
||||
daytime = "world",
|
||||
battles = "battles", battleBack = "battles", letsgo = "battles",
|
||||
atmos = "perf", shadows = "perf", aa = "perf",
|
||||
vr = "vr", smoothturn = "vr",
|
||||
}
|
||||
for key, cat in pairs(WHERE) do
|
||||
local id = "DRAMATIC_SHAPE:" .. key
|
||||
-- smoothturn and battleBack need their own switch on first; they are
|
||||
-- checked where those switches are, so only assert the ones offered now
|
||||
if Menus.offered()[id] then
|
||||
T.check(Menus.rows(cat)[id],
|
||||
("%s is on the %s menu"):format(key, cat))
|
||||
end
|
||||
end
|
||||
-- the ROM import is an action, not a setting, and it sits with the rungs of
|
||||
-- 3D-BTL it unlocks
|
||||
T.check(Menus.rows("battles")["DRAMATIC_SHAPE:stadiumRom"],
|
||||
"and STADIUM ROM is under the battles, which is what it unlocks")
|
||||
end
|
||||
|
||||
-- ------- the open menu notices when FULL is stepped onto or off
|
||||
--
|
||||
-- OptionsMenu reads its row list every frame but builds it once, so without
|
||||
-- a rebuild the rows FULL owns stay on screen until the menu is reopened --
|
||||
-- and stepping OFF FULL never brings them back.
|
||||
-- A menu reads its row list every frame but builds it once, so without a
|
||||
-- rebuild the rows FULL owns stay on screen until it is reopened -- and
|
||||
-- stepping OFF FULL never brings them back.
|
||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||
local pressed = {}
|
||||
local menuGame = {
|
||||
@@ -343,17 +452,30 @@ local menuGame = {
|
||||
save = { options = { pipelines = {}, modOptions = {} } },
|
||||
mods = { modOptions = {} },
|
||||
input = { wasPressed = function(_, k) return pressed[k] or false end },
|
||||
stack = { pop = function() end },
|
||||
-- a real stack, because the mod's own menus push and pop on it
|
||||
stack = {
|
||||
states = {},
|
||||
push = function(s, st) s.states[#s.states + 1] = st end,
|
||||
pop = function(s) s.states[#s.states] = nil end,
|
||||
top = function(s) return s.states[#s.states] end,
|
||||
},
|
||||
writeOptions = function() end,
|
||||
}
|
||||
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
local menu = OptionsMenu.new(menuGame)
|
||||
local function rowIndex(m, id)
|
||||
for i, row in ipairs(m.rows) do if row.id == id then return i end end
|
||||
end
|
||||
T.check(rowIndex(menu, "DRAMATIC_SHAPE:grid"),
|
||||
"off FULL the menu opens with the mode's settings on it")
|
||||
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
-- The mod's root menu shows the ENGINE's own pipeline descriptors, captured by
|
||||
-- the hook on its way past. The sections above fed the hook stub rows, which
|
||||
-- are enough to test what is dropped but carry no step function -- so hand it
|
||||
-- the real ones before driving the menu, or a keypress on VOXEL does nothing.
|
||||
Runtime.call("ui.options.rows", function(_, r) return r end, menuGame,
|
||||
Pipelines.rows(menuGame))
|
||||
local menu = Menus.lib.new(menuGame, Menus.lib.ROOT)
|
||||
T.check(rowIndex(menu, Menus.catId("world")),
|
||||
"off FULL the root menu opens with the 3D WORLD category on it")
|
||||
|
||||
-- step the VOXEL row from 15 down to FULL, the way the player would
|
||||
menu.index = rowIndex(menu, "pipeline:voxel")
|
||||
@@ -361,10 +483,12 @@ pressed = { left = true }
|
||||
menu:update(0)
|
||||
pressed = {}
|
||||
T.eq(Pipelines.level("voxel"), 1, "the step landed on FULL")
|
||||
T.check(not rowIndex(menu, "DRAMATIC_SHAPE:grid"),
|
||||
"and the rows FULL owns left the OPEN menu at once")
|
||||
T.check(not rowIndex(menu, "pipeline:tiltshift"), "T-SHIFT with them")
|
||||
T.check(not rowIndex(menu, Menus.catId("world")),
|
||||
"and the category FULL emptied left the OPEN menu at once")
|
||||
T.check(not rowIndex(menu, "pipeline:tiltshift"), "T-SHIFT with it")
|
||||
T.check(menu.index <= #menu.rows + 1, "the cursor stayed in range")
|
||||
T.eq(menu.index, rowIndex(menu, "pipeline:voxel"),
|
||||
"and stayed on the row the player just used")
|
||||
|
||||
-- and back off it again
|
||||
menu.index = rowIndex(menu, "pipeline:voxel")
|
||||
@@ -372,16 +496,17 @@ pressed = { right = true }
|
||||
menu:update(0)
|
||||
pressed = {}
|
||||
T.eq(Pipelines.level("voxel"), 2, "the step left FULL")
|
||||
T.check(rowIndex(menu, "DRAMATIC_SHAPE:grid"),
|
||||
"and the rows came straight back without reopening the menu")
|
||||
T.check(rowIndex(menu, Menus.catId("world")),
|
||||
"and the category came straight back without reopening the menu")
|
||||
T.check(rowIndex(menu, "pipeline:tiltshift"), "T-SHIFT too")
|
||||
|
||||
-- ------- 3D-BTL owns BATTLE LAYOUT, and takes it off the OPEN menu too
|
||||
-- ------- 3D-BTL owns BATTLE LAYOUT, from a menu the OPTIONS list cannot see
|
||||
--
|
||||
-- The row this one takes away sits ABOVE it in the list, so the cursor has to
|
||||
-- follow the row it was ON rather than the slot it was in -- otherwise the very
|
||||
-- press that switched staged battles on would leave the cursor a row further
|
||||
-- down than the player left it.
|
||||
-- The switch lives on the mod's BATTLES menu now, and the stack ticks its TOP
|
||||
-- state only (src/core/StateStack.update). So the step happens while the
|
||||
-- OPTIONS menu underneath is suspended, and a before/after pair taken around
|
||||
-- one call of its update would be read entirely after the fact and always
|
||||
-- agree. The signature is held on the instance for exactly this.
|
||||
do
|
||||
local Battles = run.loader.exports.DRAMATIC_SHAPE.lib.require("OverworldBattle")
|
||||
Battles.setting:setValue(false, menuGame) -- staged battles off
|
||||
@@ -390,37 +515,214 @@ Pipelines.setLevel("voxel", 2)
|
||||
local layoutMenu = OptionsMenu.new(menuGame)
|
||||
T.check(rowIndex(layoutMenu, "battleLayout"),
|
||||
"with staged battles off, the engine's BATTLE LAYOUT row is on the menu")
|
||||
layoutMenu.index = rowIndex(layoutMenu, "DRAMATIC_SHAPE:battles")
|
||||
|
||||
local battleMenu = Menus.lib.new(menuGame, "battles")
|
||||
T.check(not rowIndex(battleMenu, "DRAMATIC_SHAPE:battleBack"),
|
||||
"and BACK SPRITES is off the BATTLES menu, having no staged fight to frame")
|
||||
battleMenu.index = rowIndex(battleMenu, "DRAMATIC_SHAPE:battles")
|
||||
pressed = { right = true }
|
||||
layoutMenu:update(0)
|
||||
battleMenu:update(0)
|
||||
pressed = {}
|
||||
T.eq(Battles.setting:get(), true, "the step switched staged battles on")
|
||||
T.check(not rowIndex(layoutMenu, "battleLayout"),
|
||||
"and BATTLE LAYOUT left the open menu with the same keypress")
|
||||
T.eq(menuGame.save.options.battleLayout, "og", "pinned to OG on the way out")
|
||||
T.eq(layoutMenu.index, rowIndex(layoutMenu, "DRAMATIC_SHAPE:battles"),
|
||||
T.check(rowIndex(battleMenu, "DRAMATIC_SHAPE:battleBack"),
|
||||
"and BACK SPRITES arrived on the open submenu with the same keypress")
|
||||
-- it arrives BELOW the row that was used, so this one only holds because the
|
||||
-- cursor follows the row rather than the slot
|
||||
T.eq(battleMenu.index, rowIndex(battleMenu, "DRAMATIC_SHAPE:battles"),
|
||||
"with the cursor still on the row the player just used")
|
||||
|
||||
layoutMenu:update(0)
|
||||
T.check(not rowIndex(layoutMenu, "battleLayout"),
|
||||
"and the OPTIONS menu underneath drops BATTLE LAYOUT on its next update, "
|
||||
.. "though the step it is reacting to happened while it was suspended")
|
||||
end
|
||||
|
||||
-- ------- the row OPENS the menus, and B comes back one level at a time
|
||||
do
|
||||
menuGame.stack.states = {}
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
local hookRow
|
||||
for _, row in ipairs(Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
menuGame, Pipelines.rows(menuGame))) do
|
||||
if row.id == Menus.catId(Menus.lib.ROOT) then hookRow = row end
|
||||
end
|
||||
T.check(hookRow.activate and not hookRow.step,
|
||||
"the row answers A alone -- `activate` SHADOWS `step` in the engine's own "
|
||||
.. "dispatch, so a row carrying both would silently ignore Left and Right")
|
||||
T.eq(hookRow.value(menuGame), "15",
|
||||
"and its second line is VOXEL's own, so it says what the mode is doing "
|
||||
.. "without being opened")
|
||||
|
||||
hookRow.activate(menuGame)
|
||||
local root = menuGame.stack:top()
|
||||
T.check(root ~= nil, "pressing A pushed a screen")
|
||||
T.eq(#root.rows, #Menus.lib.rows(Menus.lib.ROOT, menuGame),
|
||||
"carrying the root menu's rows")
|
||||
T.eq(root:backLabel(), "BACK", "and a way out named for what it does")
|
||||
|
||||
-- the second line of a category row
|
||||
T.eq(root.rows[rowIndex(root, Menus.catId("battles"))].value(),
|
||||
Menus.rows("battles", menuGame)["DRAMATIC_SHAPE:battles"].value(),
|
||||
"a category one setting speaks for says that setting's rung")
|
||||
T.eq(root.rows[rowIndex(root, Menus.catId("world"))].value(), "5 SETTINGS",
|
||||
"and one no single row speaks for counts them instead, which is honest "
|
||||
.. "rather than arbitrary")
|
||||
|
||||
-- into a category, and back out of it a level at a time
|
||||
root.index = rowIndex(root, Menus.catId("world"))
|
||||
pressed = { a = true }
|
||||
root:update(0)
|
||||
pressed = {}
|
||||
T.eq(menuGame.stack:top():backLabel(), "BACK: 3D WORLD",
|
||||
"A on a category opens it, and the bottom line says which one -- "
|
||||
.. "OptionRows has no header, so the way out carries the name")
|
||||
T.check(menuGame.stack:top() ~= root, "which is a screen of its own")
|
||||
|
||||
pressed = { b = true }
|
||||
menuGame.stack:top():update(0)
|
||||
pressed = {}
|
||||
T.eq(menuGame.stack:top(), root, "B comes back ONE level, to the root menu")
|
||||
pressed = { start = true }
|
||||
root:update(0)
|
||||
pressed = {}
|
||||
T.eq(menuGame.stack:top(), nil, "and START leaves it the way B does")
|
||||
end
|
||||
|
||||
-- ------- the mod's row is RED, which is a palette zone and not a color
|
||||
--
|
||||
-- setColor cannot do it here twice over: the glyph atlas is black ink and LOVE
|
||||
-- tints multiplicatively, and the palette shader keys on the red channel alone
|
||||
-- so a red pixel would land in the LIGHTEST slot (see lib/SettingsMenu.lua).
|
||||
-- What a zone changes is which color the SHADE the text was drawn in comes out
|
||||
-- as, so what there is to assert is where the band sits and which slot carries
|
||||
-- the ink.
|
||||
do
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
-- the fixture dataset ships no palettes; stand MEWMON up, since the point is
|
||||
-- that the red palette is MEWMON COPIED rather than four invented colors
|
||||
Data.palettes = { palettes = {
|
||||
MEWMON = { { 255, 239, 255 }, { 255, 140, 140 }, { 80, 80, 255 }, { 0, 0, 0 } },
|
||||
} }
|
||||
|
||||
local red = Menus.lib.redPalette(Data)
|
||||
T.eq(#red, 4, "the red palette is a palette like any other, four colors")
|
||||
T.eq(red[1][2], 239,
|
||||
"and its paper is MEWMON's own -- Red, Blue and Yellow ship different "
|
||||
.. "tables, and the row must not shift the white it sits on")
|
||||
|
||||
-- What has to hold is not which slot the red is in but where it comes OUT:
|
||||
-- black text is shade 3 and the white box fill shade 0, and the display mode
|
||||
-- transforms the table before the shader ever sees it (effectiveColors). So
|
||||
-- assert through that, which is the thing the screen actually draws.
|
||||
do
|
||||
local eff = PaletteFX.effectiveColors(red)
|
||||
T.check(eff[4][1] == 255 and eff[4][2] == 0 and eff[4][3] == 0,
|
||||
"the ink shade comes out red")
|
||||
T.eq(eff[1][2], 239, "and the paper shade is the white it always was")
|
||||
end
|
||||
|
||||
-- SGB INV REVERSES the table, so the red has to start in the other slot to
|
||||
-- land on the ink. Without the special case the row draws as a solid red
|
||||
-- block with the letters cut out of it.
|
||||
PaletteFX.mode = "gbc_inv"
|
||||
do
|
||||
local eff = PaletteFX.effectiveColors(Menus.lib.redPalette(Data))
|
||||
T.check(eff[4][1] == 255 and eff[4][2] == 0,
|
||||
"under SGB INV the ink shade STILL comes out red, once the mode has had "
|
||||
.. "its turn at the table")
|
||||
T.eq(eff[1][1], 0,
|
||||
"on the same dark paper SGB INV gives every other row -- red ink, not a "
|
||||
.. "red block")
|
||||
end
|
||||
PaletteFX.mode = "gbc"
|
||||
|
||||
-- the band covers the two TEXT lines of the box and nothing else: tile 1 is
|
||||
-- the cursor and tiles 0 and 19 are the box borders, all of them black glyphs
|
||||
-- that would turn red with the label if the zone spanned the whole row
|
||||
local zone = Menus.lib.rowZone(Data, 1)
|
||||
T.eq(zone.x, 16, "the band starts at the label column, past the cursor")
|
||||
T.eq(zone.y, 8, "on the first box's label line")
|
||||
T.eq(zone.w, 17 * 8, "and stops short of the box's right border")
|
||||
T.eq(zone.h, 16, "covering the label and the value under it")
|
||||
T.eq(Menus.lib.rowZone(Data, 3).y, 8 * ((3 - 1) * 4 + 1),
|
||||
"and it is addressed by SLOT, so it follows the row down the screen")
|
||||
|
||||
-- on the menu itself: the whole-screen palette it always had, plus one band
|
||||
menuGame.stack.states = {}
|
||||
Runtime.call("ui.options.rows", function(_, r) return r end, menuGame,
|
||||
Pipelines.rows(menuGame))
|
||||
local palMenu = OptionsMenu.new(menuGame)
|
||||
T.eq(#palMenu:sgbPalettes(menuGame), 2,
|
||||
"the OPTIONS menu paints its own palette and then the one band")
|
||||
T.eq(palMenu:sgbPalettes(menuGame)[2].y, 8,
|
||||
"on row 1, where the mod's row leads the list")
|
||||
T.eq(palMenu:sgbPalettes(menuGame)[2].colors[4][1], 255, "and it is the red one")
|
||||
|
||||
-- scrolled past, the band goes with it: a red stripe left behind on whatever
|
||||
-- took the slot would be worse than no marker at all
|
||||
palMenu.scroll = 4
|
||||
T.eq(#palMenu:sgbPalettes(menuGame), 1,
|
||||
"scrolled off screen, the row takes its band with it")
|
||||
|
||||
-- and a submenu paints its own. This is the whole reason it has the method:
|
||||
-- Game:draw stops at the first state that HAS one, so a submenu without it
|
||||
-- would inherit the band above and land it on an unrelated row.
|
||||
T.eq(#Menus.lib.new(menuGame, Menus.lib.ROOT):sgbPalettes(menuGame), 1,
|
||||
"a submenu owns its palette rather than inheriting the red band")
|
||||
|
||||
Data.palettes = nil
|
||||
end
|
||||
|
||||
-- level 2 is the "15" rung: any rung that is not FULL, so the settings the
|
||||
-- preset owns are back on the menu
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
local hookedRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data }, { { id = "text_speed" } })
|
||||
do
|
||||
-- what the engine really hands the hook: its own rows, the pipeline rows
|
||||
-- among them. The mod moves those two onto its root menu, so a call that
|
||||
-- left them out would leave the root menu with nothing but categories.
|
||||
local given = { { id = "text_speed" } }
|
||||
for _, row in ipairs(Pipelines.rows({ data = Data })) do
|
||||
given[#given + 1] = row
|
||||
end
|
||||
Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data }, given)
|
||||
end
|
||||
-- Every row the mod offers, keyed by id. RENDER DIST, FOREST FX, LET'S GO,
|
||||
-- SHADOWS and AA are read straight out of it where they are used rather than
|
||||
-- named here, because this chunk is one main function and has 200 local slots
|
||||
-- to spend.
|
||||
local hookedRows = Menus.offered()
|
||||
-- one per setting, plus the STADIUM ROM action row -- which is not a setting
|
||||
-- (nothing to store, nothing for the mod manager to persist) and is offered
|
||||
-- on every platform, saying WHERE? rather than IMPORT where there is no file
|
||||
-- dialog to open
|
||||
T.eq(#hookedRows, 14, "the options hook added a row per setting, plus the "
|
||||
.. "STADIUM ROM action row")
|
||||
local grid, curve, water = hookedRows[2], hookedRows[3], hookedRows[5]
|
||||
local battles, backRow, daytime = hookedRows[7], hookedRows[8], hookedRows[10]
|
||||
-- the RENDER DIST row is hookedRows[4], FOREST FX hookedRows[6], LET'S GO
|
||||
-- hookedRows[9], SHADOWS hookedRows[11] and AA hookedRows[12]; all five are
|
||||
-- read where they are used rather than named here, because this chunk is one
|
||||
-- main function and has 200 local slots to spend
|
||||
T.eq(hookedRows[4].label, "RENDER DIST", "the viewport row carries its label")
|
||||
T.eq(hookedRows[4].value(), "FIT",
|
||||
-- dialog to open -- and the four category rows that carry them
|
||||
do
|
||||
-- named rather than counted: a count says "something moved" and this says
|
||||
-- WHAT is meant to be reachable. SMOOTH TURN is absent because VR is off,
|
||||
-- which is its own gate and is tested where VR is turned on.
|
||||
local want = { "grid", "curve", "viewbox", "water", "daytime",
|
||||
"battles", "battleBack", "letsgo", "stadiumRom",
|
||||
"atmos", "shadows", "aa", "vr" }
|
||||
local n = 0
|
||||
for _ in pairs(hookedRows) do n = n + 1 end
|
||||
for _, key in ipairs(want) do
|
||||
T.check(hookedRows["DRAMATIC_SHAPE:" .. key],
|
||||
("%s is reachable from the mod's menus"):format(key))
|
||||
end
|
||||
T.check(hookedRows["pipeline:voxel"] and hookedRows["pipeline:tiltshift"],
|
||||
"and both pipeline rows with them")
|
||||
-- those, the two pipelines and the four categories, and nothing else
|
||||
T.eq(n, #want + 2 + 4, "and nothing else is on them")
|
||||
end
|
||||
local grid = hookedRows["DRAMATIC_SHAPE:grid"]
|
||||
local curve, water = hookedRows["DRAMATIC_SHAPE:curve"], hookedRows["DRAMATIC_SHAPE:water"]
|
||||
local battles = hookedRows["DRAMATIC_SHAPE:battles"]
|
||||
local backRow, daytime = hookedRows["DRAMATIC_SHAPE:battleBack"],
|
||||
hookedRows["DRAMATIC_SHAPE:daytime"]
|
||||
T.eq(hookedRows["DRAMATIC_SHAPE:viewbox"].label, "RENDER DIST",
|
||||
"the viewport row carries its label")
|
||||
T.eq(hookedRows["DRAMATIC_SHAPE:viewbox"].value(), "FIT",
|
||||
"and defaults to FIT -- the cut IS the window the flat game already "
|
||||
.. "framed, which is the whole claim the row makes")
|
||||
T.eq(water.label, "WATER", "the water row carries its label")
|
||||
@@ -429,8 +731,8 @@ T.eq(water.value(), "FULL",
|
||||
water.step({ save = { options = {} }, mods = { modOptions = {} } }, 1)
|
||||
T.eq(water.value(), "SKY",
|
||||
"stepping down drops the screen-space march and keeps the sky, sun and moon")
|
||||
T.eq(hookedRows[6].label, "FOREST FX", "the atmosphere row carries its label")
|
||||
T.eq(hookedRows[6].value(), "FULL",
|
||||
T.eq(hookedRows["DRAMATIC_SHAPE:atmos"].label, "FOREST FX", "the atmosphere row carries its label")
|
||||
T.eq(hookedRows["DRAMATIC_SHAPE:atmos"].value(), "FULL",
|
||||
"and defaults to FULL -- it only spends anything on a map with an "
|
||||
.. "atmosphere entry, which is one forest today")
|
||||
T.eq(daytime.label, "DAYTIME", "the day/night row carries its label")
|
||||
@@ -451,8 +753,8 @@ T.eq(backRow.value(), "OFF",
|
||||
.. "map, so the classic slot is opt-in")
|
||||
T.check(backRow.id ~= battles.id and backRow.id:find("battleBack", 1, true),
|
||||
"on its own key, so it persists beside 3D-BTL rather than over it")
|
||||
T.eq(hookedRows[9].label, "LET'S GO", "the capture-mode row carries its label")
|
||||
T.eq(hookedRows[9].value(), "OFF",
|
||||
T.eq(hookedRows["DRAMATIC_SHAPE:letsgo"].label, "LET'S GO", "the capture-mode row carries its label")
|
||||
T.eq(hookedRows["DRAMATIC_SHAPE:letsgo"].value(), "OFF",
|
||||
"and starts OFF -- a gameplay mode is opt-in, whatever the diorama does")
|
||||
|
||||
-- stepping writes through to the one place both rows read
|
||||
@@ -532,7 +834,7 @@ do
|
||||
local AntiAlias = run.loader.exports.DRAMATIC_SHAPE.lib.require("AntiAlias")
|
||||
local VoxelGrid = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelGrid")
|
||||
local aaGame = { save = { options = {} }, mods = { modOptions = {} } }
|
||||
local aa = hookedRows[12]
|
||||
local aa = hookedRows["DRAMATIC_SHAPE:aa"]
|
||||
T.eq(aa.label, "AA", "the anti-aliasing row carries its label")
|
||||
T.eq(aa.value(), "OFF",
|
||||
"and starts off -- supersampling is a cost knob, and a mod must not spend "
|
||||
@@ -588,7 +890,7 @@ end
|
||||
;(function()
|
||||
local Shadows = run.loader.exports.DRAMATIC_SHAPE.lib.require("Shadows")
|
||||
local ShadowMap = run.loader.exports.DRAMATIC_SHAPE.lib.require("ShadowMap")
|
||||
local shadowRow = hookedRows[11]
|
||||
local shadowRow = hookedRows["DRAMATIC_SHAPE:shadows"]
|
||||
local shGame = { save = { options = {} }, mods = { modOptions = {} } }
|
||||
T.eq(shadowRow.label, "SHADOWS", "the shadow row carries its label")
|
||||
T.eq(shadowRow.value(), "ON",
|
||||
@@ -3336,19 +3638,16 @@ T.eq(Battles.backSetting:get(), true, "without being rewritten underneath")
|
||||
-- ...so the row comes off the menu with it, on the same reasoning the mod's
|
||||
-- other absent rows come off: a row that no longer decides anything is worse
|
||||
-- than no row
|
||||
local offRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
backGame, { { id = "tilt" } })
|
||||
local offIds = {}
|
||||
for _, row in ipairs(offRows) do offIds[row.id] = true end
|
||||
T.check(offIds["DRAMATIC_SHAPE:battles"], "3D-BTL itself is still offered")
|
||||
T.check(not offIds["DRAMATIC_SHAPE:battleBack"],
|
||||
local offRows = Menus.rows("battles", backGame)
|
||||
T.check(offRows["DRAMATIC_SHAPE:battles"], "3D-BTL itself is still offered")
|
||||
T.check(not offRows["DRAMATIC_SHAPE:battleBack"],
|
||||
"but BACK SPRITES is off the menu while there is no staged fight to be about")
|
||||
|
||||
Battles.setting:setValue(true, backGame)
|
||||
local onRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
backGame, { { id = "tilt" } })
|
||||
-- switched back on it returns, and to the BATTLES menu -- the one the switch
|
||||
-- it depends on is on, so the two are read together
|
||||
local onAt = {}
|
||||
for i, row in ipairs(onRows) do onAt[row.id] = i end
|
||||
for i, row in ipairs(Menus.lib.rows("battles", backGame)) do onAt[row.id] = i end
|
||||
T.check(onAt["DRAMATIC_SHAPE:battleBack"], "switched back on, so is the row")
|
||||
T.eq(onAt["DRAMATIC_SHAPE:battleBack"] - onAt["DRAMATIC_SHAPE:battles"], 1,
|
||||
"directly under the row it belongs to")
|
||||
@@ -5962,26 +6261,28 @@ end)()
|
||||
local VRMod = lib.require("VR")
|
||||
T.eq(VRMod.smoothTurn:get(), false, "SMOOTH TURN is off out of the box")
|
||||
|
||||
local function optionRows()
|
||||
local out = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data }, { { id = "tilt" } })
|
||||
local ids = {}
|
||||
for _, row in ipairs(out) do ids[row.id] = row end
|
||||
return ids
|
||||
end
|
||||
|
||||
Pipelines.setLevel("voxel", 3) -- off FULL, which owns other rows
|
||||
VRMod.setting:sync(false)
|
||||
T.check(not optionRows()["DRAMATIC_SHAPE:smoothturn"],
|
||||
"with VR off the row is not on the OPTIONS menu")
|
||||
T.check(not Menus.rows("vr")["DRAMATIC_SHAPE:smoothturn"],
|
||||
"with VR off the row is not on the VR menu")
|
||||
-- and with the headset's own row the only thing left on it, that menu is
|
||||
-- still worth offering: a category is dropped when it is EMPTY, not when it
|
||||
-- is short
|
||||
T.check(Menus.rows(Menus.lib.ROOT)[Menus.catId("vr")],
|
||||
"though the category is, on the strength of the VR row alone")
|
||||
|
||||
VRMod.setting:sync(true)
|
||||
local smoothRow = optionRows()["DRAMATIC_SHAPE:smoothturn"]
|
||||
local smoothRow = Menus.rows("vr")["DRAMATIC_SHAPE:smoothturn"]
|
||||
T.check(smoothRow ~= nil, "and with VR on it is")
|
||||
if smoothRow then
|
||||
T.eq(smoothRow.label, "SMOOTH TURN", "under its own name")
|
||||
T.eq(smoothRow.value(), "OFF", "reading OFF until the player says otherwise")
|
||||
end
|
||||
-- VR on takes the two battle rows away, which is the other half of the same
|
||||
-- gate: the headset requires staged battles and forbids back sprites, so
|
||||
-- both rows decide nothing there
|
||||
T.check(not Menus.rows("battles")["DRAMATIC_SHAPE:battles"],
|
||||
"and the battle rows are gone, being decided by the headset instead")
|
||||
VRMod.setting:sync(false)
|
||||
|
||||
-- ------- the gun's own bookkeeping
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
-- Probe: the reported grass along the TOP of the frame in GO-style
|
||||
-- battles on ROUTE 6.
|
||||
--
|
||||
-- The capture seat is a different camera from the battle's own: the "tele"
|
||||
-- rig stands 145 world px back at a height of 37.9 (well over two cells,
|
||||
-- clear of anything that grows on the ground), while the head-on capture
|
||||
-- seat stands 46 back at a height of 13 -- BELOW the top of a 16px grass
|
||||
-- tuft or hedge. On a route lined with the stuff, the eye is inside it.
|
||||
--
|
||||
-- So: stage a capture at several spots along Route 6 and record where the
|
||||
-- eye actually is relative to the ground, with a shot of each.
|
||||
--
|
||||
-- SHOT_DIR=.scratchpad/route6 \
|
||||
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/letsgo_route6.lua \
|
||||
-- "/c/Program Files/LOVE/lovec.exe" .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or ".scratchpad/route6"
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local Bag = require("src.inventory.Bag")
|
||||
|
||||
local exports = game.mods and game.mods.exports
|
||||
local lib = exports and exports.DRAMATIC_SHAPE and exports.DRAMATIC_SHAPE.lib
|
||||
if not lib then
|
||||
U.log("DRAMATIC_SHAPE is not loaded")
|
||||
return
|
||||
end
|
||||
local LetsGo = lib.require("LetsGo")
|
||||
local CatchThrow = lib.require("CatchThrow")
|
||||
local ChunkMesher = lib.require("ChunkMesher")
|
||||
|
||||
pcall(os.execute, 'mkdir -p "' .. DIR .. '" 2>/dev/null')
|
||||
pcall(os.execute, 'mkdir "' .. DIR:gsub("/", "\\") .. '" 2>nul')
|
||||
|
||||
game.save.party = { Pokemon.new(game.data, "CHARIZARD", 45) }
|
||||
game.save.player.name = "RED"
|
||||
Bag.add(game.save, "POKE_BALL", 99, game.data)
|
||||
CatchThrow.lastBall = "POKE_BALL"
|
||||
LetsGo.setting:setValue("full", game)
|
||||
|
||||
local MAP = os.getenv("DS_MAP") or "ROUTE_6"
|
||||
-- a spread down the route: the reported shot is on the path with hedges
|
||||
-- both sides, which is where a low seat has the least room
|
||||
local SPOTS = {}
|
||||
for _, xy in ipairs({ { 5, 6 }, { 5, 14 }, { 9, 20 }, { 4, 26 }, { 10, 32 } }) do
|
||||
SPOTS[#SPOTS + 1] = xy
|
||||
end
|
||||
|
||||
for i, sp in ipairs(SPOTS) do
|
||||
U.teleport(game, MAP, sp[1], sp[2], "up")
|
||||
U.wait(50)
|
||||
-- confirm the teleport actually landed: a first cut reported the SAME
|
||||
-- eye at all five spots, which meant the probe never left the save's
|
||||
-- own position and every "spot" was one place wearing five labels
|
||||
do
|
||||
local ow = game.overworld
|
||||
local m = ow and ow.map
|
||||
U.log(("spot %d: on map %s at cell %s,%s")
|
||||
:format(i, tostring(m and (m.id or m.name) or "?"),
|
||||
tostring(ow and ow.player and ow.player.cellX),
|
||||
tostring(ow and ow.player and ow.player.cellY)))
|
||||
end
|
||||
for _ = 1, 600 do
|
||||
if ChunkMesher.pending() == 0 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
|
||||
local bt = BattleState.newWild(game, "ODDISH", 13)
|
||||
bt.onFinish = function() end
|
||||
game.overworld:pushBattle(bt)
|
||||
U.wait(70)
|
||||
for _ = 1, 80 do
|
||||
if bt.phase == "menu" or CatchThrow.session() then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(6)
|
||||
end
|
||||
U.wait(40)
|
||||
|
||||
local s = CatchThrow.session()
|
||||
if s and s.shot and s.shot.eye then
|
||||
local e = s.shot.eye
|
||||
local p = s.playerPos
|
||||
local back = math.sqrt((e[1] - p[1]) ^ 2 + (e[3] - p[3]) ^ 2)
|
||||
U.log(("spot %d (%d,%d): eye = %.1f,%.1f,%.1f height %.1f over ground "
|
||||
.. "seat %.1f back (wants 46)%s")
|
||||
:format(i, sp[1], sp[2], e[1], e[2], e[3], e[2] - s.groundY, back,
|
||||
back < 45.5 and " <-- the world pulled it in" or ""))
|
||||
else
|
||||
U.log(("spot %d (%d,%d): no capture session"):format(i, sp[1], sp[2]))
|
||||
end
|
||||
U.shot(game, ("%s/spot%d.png"):format(DIR, i))
|
||||
|
||||
-- out, and all the way off the stack before the next spot
|
||||
U.tap(game, "b")
|
||||
for _ = 1, 300 do
|
||||
if not CatchThrow.session() and game.stack:top() ~= bt then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(4)
|
||||
end
|
||||
U.wait(30)
|
||||
end
|
||||
U.log("done -- " .. DIR)
|
||||
end
|
||||
Reference in New Issue
Block a user