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
|
nor takes it away, on the same reasoning as AA -- what the look costs is
|
||||||
the player's question, not a preset's.
|
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
|
### 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
|
- **A building's back no longer wears its own front door.** Every voxelized
|
||||||
building is its drawing extruded straight through the footprint, so the
|
building is its drawing extruded straight through the footprint, so the
|
||||||
far wall is the facade again -- and read from behind, the facade mirrored:
|
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
|
-- 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`;
|
-- middle. Handed to BattleScene.render through the capture table's `rig`;
|
||||||
-- everything downstream (pins, cards, sun, fov) is camera-generic.
|
-- 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 function captureRig(arena, groundY)
|
||||||
local ex, ez = arena.enemy[1], arena.enemy[2]
|
local ex, ez = arena.enemy[1], arena.enemy[2]
|
||||||
local px, pz = arena.player[1], arena.player[2]
|
local px, pz = arena.player[1], arena.player[2]
|
||||||
local ax, az = ex - px, ez - pz
|
local ax, az = ex - px, ez - pz
|
||||||
local l = math.sqrt(ax * ax + az * az)
|
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
|
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 = {
|
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 },
|
focus = { ex, groundY + 8, ez },
|
||||||
fov = SEAT_FOV,
|
fov = SEAT_FOV,
|
||||||
}
|
}
|
||||||
-- the pitch VoxelScene.pull wants: how far below level the seat looks
|
-- 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
|
return cam, math.max(pitch, 0.05), SEAT_FRAME
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+10
-6
@@ -168,6 +168,15 @@ function ModSetting:sync(value)
|
|||||||
self.index = indexOf(self, value)
|
self.index = indexOf(self, value)
|
||||||
end
|
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
|
-- The descriptor src/ui/OptionRows.lua renders, in the shape the
|
||||||
-- ui.options.rows hook appends.
|
-- ui.options.rows hook appends.
|
||||||
function ModSetting:row()
|
function ModSetting:row()
|
||||||
@@ -175,12 +184,7 @@ function ModSetting:row()
|
|||||||
return {
|
return {
|
||||||
id = "DRAMATIC_SHAPE:" .. self.key,
|
id = "DRAMATIC_SHAPE:" .. self.key,
|
||||||
label = self.label,
|
label = self.label,
|
||||||
-- the label of the rung actually in force, which is not the stored one
|
value = function() return self_:valueLabel() end,
|
||||||
-- 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,
|
|
||||||
step = function(game, dir)
|
step = function(game, dir)
|
||||||
self_:cycle(game, dir)
|
self_:cycle(game, dir)
|
||||||
return true
|
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 FreeMove = V.require("FreeMove")
|
||||||
local CamControl = V.require("CamControl")
|
local CamControl = V.require("CamControl")
|
||||||
local VR = V.require("VR")
|
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
|
-- 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
|
-- 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.
|
-- chip-synthesized sounds it fires. See lib/Horde.lua for the whole design.
|
||||||
@@ -449,8 +452,26 @@ local function stagedBattles()
|
|||||||
return OverworldBattle.enabled()
|
return OverworldBattle.enabled()
|
||||||
end
|
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 = {
|
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,
|
{ WorldCurve.setting,
|
||||||
"Bend the world down over the horizon, Animal Crossing style. 1 is a "
|
"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 "
|
.. "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 "
|
.. "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 "
|
.. "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 "
|
.. "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,
|
{ ViewBox.setting,
|
||||||
"How much of the map the camera bothers to draw. FIT is exactly the "
|
"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 "
|
.. "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, "
|
.. "closest, WIDE through WIDEST push the world's edge further out, "
|
||||||
.. "and OFF stops cutting entirely. Not on 1ST or 3RD -- you are "
|
.. "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 "
|
.. "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,
|
{ Water.setting,
|
||||||
"Reflections on water. FULL adds screen-space reflections of the "
|
"Reflections on water. FULL adds screen-space reflections of the "
|
||||||
.. "shoreline, the trees and the buildings behind it; SKY is the sky, "
|
.. "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 "
|
.. "the sun and the moon alone, which is most of the look for a "
|
||||||
.. "fraction of the cost." },
|
.. "fraction of the cost.",
|
||||||
-- `full` for the AA reason: additive shafts are fill rate, and under 4X
|
cat = "world" },
|
||||||
-- supersampling that is a question about the hardware, not the look.
|
{ DayNight.setting,
|
||||||
{ ForestAtmos.setting,
|
"What time it is outdoors: pin the sky to DAY, NIGHT, DUSK or DAWN, "
|
||||||
"The air of the deep woods (Viridian Forest): a ground haze, and "
|
.. "let CYCLE run it -- ten minutes of sun, ten of moon, with the "
|
||||||
.. "volumetric light let down through the unseen canopy overhead -- "
|
.. "shadows, the sky and the light following -- or SYNC it to the "
|
||||||
.. "gold spears of sun by day, silver moon rays at night, pollen "
|
.. "clock on the wall, so Kanto's evening falls when yours does.",
|
||||||
.. "drifting through the beams and fireflies once they cool. LOW "
|
cat = "world" },
|
||||||
.. "keeps the haze, halves the beam march and stands the particles "
|
|
||||||
.. "down. On a phone the row offers LOW alone: the beams need a "
|
-- ------- BATTLES -- what a fight is drawn over, and how it is played
|
||||||
.. "depth texture the pass can read back, and no mobile driver here "
|
--
|
||||||
.. "grants one.",
|
|
||||||
full = true },
|
|
||||||
-- `full` marks a row FULL does not take away. FULL owns the diorama's own
|
-- `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.
|
-- 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
|
-- 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 "
|
.. "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 "
|
.. "STADIUM ROM row, or drop it in the baseroms folder and restart. No "
|
||||||
.. "other version works: the reader is keyed to that one cartridge.",
|
.. "other version works: the reader is keyed to that one cartridge.",
|
||||||
|
cat = "battles",
|
||||||
when = function() return not VR.enabled() end, full = true },
|
when = function() return not VR.enabled() end, full = true },
|
||||||
-- Only offered while a fight can actually be staged on the map: with 3D-BTL
|
-- 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,
|
-- 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 "
|
"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. "
|
.. "original slot, instead of standing it on the map facing the foe. "
|
||||||
.. "The foe is still out there on its own tile.",
|
.. "The foe is still out there on its own tile.",
|
||||||
|
cat = "battles",
|
||||||
when = function() return stagedBattles() and not VR.enabled() end,
|
when = function() return stagedBattles() and not VR.enabled() end,
|
||||||
full = true },
|
full = true },
|
||||||
-- `full` like the battle rows: this is a GAMEPLAY mode, not a knob on
|
-- `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 "
|
.. "throws, new species and your running catch combo. Needs 3D-BTL "
|
||||||
.. "on; anywhere the staged fight cannot stand, balls quietly throw "
|
.. "on; anywhere the staged fight cannot stand, balls quietly throw "
|
||||||
.. "the classic way.",
|
.. "the classic way.",
|
||||||
full = true },
|
cat = "battles", full = true },
|
||||||
{ DayNight.setting,
|
|
||||||
"What time it is outdoors: pin the sky to DAY, NIGHT, DUSK or DAWN, "
|
-- ------- PERFORMANCE -- what the look COSTS, which is a different question
|
||||||
.. "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 "
|
-- All three are `full`, and all three for the same reason: FULL is a preset
|
||||||
.. "clock on the wall, so Kanto's evening falls when yours does." },
|
-- 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
|
-- `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
|
-- 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
|
-- 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 "
|
.. "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 "
|
.. "shadows under characters included, and the forest's light shafts go "
|
||||||
.. "with it: the beams are lit by the sun's own map.",
|
.. "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
|
-- 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
|
-- knob on the look at all, it is what the look COSTS.
|
||||||
-- 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.
|
|
||||||
{ AntiAlias.setting,
|
{ AntiAlias.setting,
|
||||||
"Smooth the stair-stepped edges of the 3D world -- roof ridges, ledge "
|
"Smooth the stair-stepped edges of the 3D world -- roof ridges, ledge "
|
||||||
.. "lips, a tree against the sky -- by rendering the diorama larger than "
|
.. "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 "
|
.. "reads smoother rather than sharper. 2X costs half again as many "
|
||||||
.. "pixels in each direction and 4X twice, which makes this the most "
|
.. "pixels in each direction and 4X twice, which makes this the most "
|
||||||
.. "expensive row in the mod.",
|
.. "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
|
-- `full` for the same reason as AA: not a knob on the look, a question
|
||||||
-- about the hardware on the desk.
|
-- about the hardware on the desk.
|
||||||
{ VR.setting,
|
{ VR.setting,
|
||||||
@@ -588,6 +624,7 @@ local SETTINGS = {
|
|||||||
.. "Menus and dialogs float on a panel. Needs a Windows OpenXR runtime "
|
.. "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 mod running from a real folder; without them the row stays "
|
||||||
.. "and the game stays flat, with the reason on the console.",
|
.. "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
|
-- 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
|
-- says why); off Windows -- mobile above all -- there is no VR to have
|
||||||
-- and the row does not exist
|
-- 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 "
|
.. "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 "
|
.. "to make somebody ill in a headset. Turn it on if you have your sea "
|
||||||
.. "legs and want the continuity.",
|
.. "legs and want the continuity.",
|
||||||
|
cat = "vr",
|
||||||
-- and only under STANDARD: the stick turns a HEAD, and neither diorama
|
-- and only under STANDARD: the stick turns a HEAD, and neither diorama
|
||||||
-- mode has the player standing in the world to be turned
|
-- mode has the player standing in the world to be turned
|
||||||
when = function() return VR.enabled() and not VR.dioramaMode() end,
|
when = function() return VR.enabled() and not VR.dioramaMode() end,
|
||||||
full = true },
|
full = true },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsMenu.define(SETTINGS)
|
||||||
|
|
||||||
local schema = {}
|
local schema = {}
|
||||||
for _, entry in ipairs(SETTINGS) do
|
for _, entry in ipairs(SETTINGS) do
|
||||||
-- the VR rows are absent from the mod manager's page too where the
|
-- the VR rows are absent from the mod manager's page too where the
|
||||||
@@ -783,29 +823,31 @@ do
|
|||||||
end
|
end
|
||||||
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
|
-- This mod used to put FOURTEEN rows on the engine's OPTIONS list, in one
|
||||||
-- belongs with the other display modes; a mod's own ui.options.rows
|
-- block spliced in beside the pipeline rows. OptionRows shows four boxes at a
|
||||||
-- additions land at the END of the list. That left this mod's four rows in
|
-- time, so that was four screens of scrolling inside a list that already
|
||||||
-- two places with unrelated engine rows between them, which reads as two
|
-- carried twenty engine rows, and finding SHADOWS meant knowing it was in
|
||||||
-- unrelated features rather than one mode with settings.
|
-- there past the wireframe and the horizon bend.
|
||||||
--
|
--
|
||||||
-- So the plain settings are inserted directly after the last of this mod's
|
-- Now there is ONE row, and it leads the list. What it opens -- the
|
||||||
-- PIPELINE rows instead of appended. Nothing else moves: the block lands
|
-- categories, the screens, and why the split falls where it does -- is
|
||||||
-- where the engine already decided display modes go.
|
-- lib/SettingsMenu.lua. VOXEL and T-SHIFT go with it: they are this mod's
|
||||||
local function insertGrouped(out, extra)
|
-- display modes, the engine only spliced them beside TILT because it had
|
||||||
local anchor = nil
|
-- nowhere better, and TILT is not on the menu any more anyway (see below).
|
||||||
for i, row in ipairs(out) do
|
--
|
||||||
local id = type(row) == "table" and row.id
|
-- Two things it takes to move a pipeline row: the engine's descriptor is
|
||||||
if id == "pipeline:voxel" or id == "pipeline:tiltshift" then anchor = i end
|
-- 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
|
end
|
||||||
if not anchor then
|
return nil
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- FULL owns the settings that describe the LOOK, so while it is selected those
|
-- 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
|
if changed and game.writeOptions then pcall(game.writeOptions, game) end
|
||||||
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
|
-- call next() first and decorate what comes back, so every other mod's
|
||||||
-- rows survive this one
|
-- rows survive this one
|
||||||
mod.hooks:wrap("ui.options.rows", function(next, game, rows)
|
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)
|
OverworldBattle.forceOG(game)
|
||||||
dropRow(out, "battleLayout")
|
dropRow(out, "battleLayout")
|
||||||
end
|
end
|
||||||
local full = Voxel.isFull(Pipelines.level("voxel"))
|
if Voxel.isFull(Pipelines.level("voxel")) then
|
||||||
if full then
|
|
||||||
-- FULL owns the rows that PARAMETERISE the diorama -- the wireframe, the
|
-- FULL owns the rows that PARAMETERISE the diorama -- the wireframe, the
|
||||||
-- horizon bend, the blur, the hour -- so those come off the menu and
|
-- horizon bend, the blur, the hour -- so DAYTIME is held at SYNC while its
|
||||||
-- DAYTIME is held at SYNC while its row is unreachable.
|
-- 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)
|
DayNight.forceSync(game)
|
||||||
dropRow(out, "pipeline:tiltshift")
|
|
||||||
end
|
end
|
||||||
local extra = {}
|
-- The two pipeline rows move INTO the mod's own root menu: captured as the
|
||||||
for _, entry in ipairs(SETTINGS) do
|
-- engine built them, then dropped from here so they are not in two places.
|
||||||
-- Two things decide whether a row is offered.
|
local captured, voxelRow = {}, nil
|
||||||
--
|
for _, id in ipairs({ "pipeline:voxel", "pipeline:tiltshift" }) do
|
||||||
-- FULL: a preset that owns the look, so the rows that describe the look go
|
local row = captureRow(out, id)
|
||||||
-- with it. The BATTLE rows are not that -- 3D-BTL decides what a fight is
|
-- a pipeline the registry refused is simply not there, and the menu says
|
||||||
-- drawn OVER and BACK SPRITES how it is framed, and neither is a knob on
|
-- so by not offering it rather than by offering a hole
|
||||||
-- the diorama FULL is a preset for. FULL still SETS them on arrival (see
|
if row then captured[#captured + 1] = row end
|
||||||
-- applyFull); it does not hold them, so leaving them on the menu is the
|
if id == "pipeline:voxel" then voxelRow = row end
|
||||||
-- difference between a preset and a lock.
|
dropRow(out, id)
|
||||||
--
|
|
||||||
-- 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
|
|
||||||
end
|
end
|
||||||
-- and the ROM import, which is an ACTION and not a setting: there is no
|
SettingsMenu.setPipelineRows(captured)
|
||||||
-- rung to store, nothing for the mod manager's page to persist and nothing
|
-- ------- one row, and it leads the list
|
||||||
-- 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
|
-- At the TOP rather than spliced in beside the display modes it used to sit
|
||||||
-- menu rather than offering a button that cannot do anything.
|
-- with. This is a mod that replaces the whole look of the game, and a player
|
||||||
-- On EVERY platform. Where there is no file dialog it says WHERE? and
|
-- who installed it and went looking for its settings should not have to
|
||||||
-- shows the folder to put the cartridge in, which is the one thing a
|
-- scroll to find out where they went -- least of all past the engine rows it
|
||||||
-- player on a phone could not otherwise find out -- the row used to vanish
|
-- has quietly taken away.
|
||||||
-- there, which reads as the feature being missing rather than manual.
|
--
|
||||||
local okPick, importRow = pcall(function()
|
-- Inserted after next() has run, so it leads every OTHER mod's rows too. The
|
||||||
return V.require("StadiumRomPick").row()
|
-- second line is VOXEL's own value function, which makes the row say what
|
||||||
end)
|
-- the mode is currently doing without opening it -- and reuses the engine's
|
||||||
if okPick and importRow then extra[#extra + 1] = importRow end
|
-- label ladder rather than restating it.
|
||||||
return insertGrouped(out, extra)
|
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)
|
end)
|
||||||
|
|
||||||
-- The mod manager writes and persists on its own, so the only thing left
|
-- 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
|
if payload.key == entry[1].key then entry[1]:sync(payload.value) end
|
||||||
end
|
end
|
||||||
-- 3D-BTL switched on from the manager's page pins BATTLE LAYOUT exactly as
|
-- 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
|
-- the mod's own row does, and DAYTIME changed there while FULL owns it snaps
|
||||||
-- that has to follow it.
|
-- straight back to SYNC -- that row is off the mod's menus under FULL, but
|
||||||
if stagedBattles() then OverworldBattle.forceOG() end
|
-- the manager's page carries every setting unconditionally, and the pin has
|
||||||
-- and DAYTIME changed from the manager's page while FULL owns it snaps
|
-- to hold against both.
|
||||||
-- straight back to SYNC -- the OPTIONS row is hidden, but the manager's is
|
pinDependents()
|
||||||
-- 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
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- ------- keeping the geometry in step with the world
|
-- ------- 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
|
-- 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
|
-- 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.
|
-- 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
|
do
|
||||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||||
if not OptionsMenu.dramaticShapeFullHook then
|
if not OptionsMenu.dramaticShapeFullHook then
|
||||||
|
local OptionRows = require("src.ui.OptionRows")
|
||||||
local Pipelines = require("src.render.Pipelines")
|
local Pipelines = require("src.render.Pipelines")
|
||||||
local inner = OptionsMenu.update
|
local inner = OptionsMenu.update
|
||||||
|
local innerPalettes = OptionsMenu.sgbPalettes
|
||||||
|
|
||||||
local function idAt(menu, index)
|
local function idAt(menu, index)
|
||||||
local row = menu.rows and menu.rows[index or 1]
|
local row = menu.rows and menu.rows[index or 1]
|
||||||
return type(row) == "table" and row.id or nil
|
return type(row) == "table" and row.id or nil
|
||||||
end
|
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)
|
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 wasOn = idAt(self, self.index)
|
||||||
|
local before = self.dramaticShapeSig or signature()
|
||||||
inner(self, dt)
|
inner(self, dt)
|
||||||
local after = Pipelines.level("voxel")
|
local after = signature()
|
||||||
local crossedFull = after ~= before
|
self.dramaticShapeSig = after
|
||||||
and (Voxel.isFull(before) or Voxel.isFull(after))
|
if before ~= after then
|
||||||
if crossedFull or OverworldBattle.enabled() ~= hadBattles
|
|
||||||
or VR.enabled() ~= hadVR then
|
|
||||||
local rebuilt = OptionsMenu.new(self.game)
|
local rebuilt = OptionsMenu.new(self.game)
|
||||||
self.rows = rebuilt.rows
|
self.rows = rebuilt.rows
|
||||||
-- Follow the row the cursor was ON rather than the slot it was in:
|
-- Follow the row the cursor was ON rather than the slot it was in:
|
||||||
@@ -1095,6 +1182,36 @@ do
|
|||||||
end
|
end
|
||||||
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
|
OptionsMenu.dramaticShapeFullHook = true
|
||||||
end
|
end
|
||||||
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 Runtime = require("src.mods.Runtime")
|
||||||
local VoxelState = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelState")
|
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
|
-- ------- 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
|
-- 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.
|
-- go, which is what makes it a preset rather than a lock.
|
||||||
Pipelines.setLevel("voxel", VoxelState.FULL_LEVEL)
|
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,
|
local fullRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||||
{ data = Data },
|
{ data = Data },
|
||||||
{ { id = "tilt" }, { id = "pipeline:voxel" },
|
{ { id = "tilt" }, { id = "pipeline:voxel" },
|
||||||
{ id = "pipeline:tiltshift" } })
|
{ id = "pipeline:tiltshift" } })
|
||||||
local fullIds = {}
|
local fullIds = {}
|
||||||
for _, row in ipairs(fullRows) do fullIds[row.id] = true end
|
for _, row in ipairs(fullRows) do fullIds[row.id] = true end
|
||||||
T.check(fullIds["pipeline:voxel"], "FULL keeps the VOXEL row it lives on")
|
-- what the hook leaves on the ENGINE's list: one row, and the pipeline rows
|
||||||
T.check(not fullIds["pipeline:tiltshift"],
|
-- 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")
|
"FULL takes T-SHIFT off the menu -- it owns the blur")
|
||||||
T.check(not fullIds["DRAMATIC_SHAPE:grid"], "and V-GRID")
|
T.check(not fullOffered["DRAMATIC_SHAPE:grid"], "and V-GRID")
|
||||||
T.check(not fullIds["DRAMATIC_SHAPE:curve"], "and V-CURVE")
|
T.check(not fullOffered["DRAMATIC_SHAPE:curve"], "and V-CURVE")
|
||||||
T.check(not fullIds["DRAMATIC_SHAPE:viewbox"], "and RENDER DIST")
|
T.check(not fullOffered["DRAMATIC_SHAPE:viewbox"], "and RENDER DIST")
|
||||||
T.check(not fullIds["DRAMATIC_SHAPE:daytime"], "and DAYTIME")
|
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
|
-- 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
|
-- 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
|
-- 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(fullOffered["DRAMATIC_SHAPE:battles"],
|
||||||
T.check(fullIds["DRAMATIC_SHAPE:battleBack"], "and BACK SPRITES with it")
|
"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
|
-- 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
|
-- 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
|
-- 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
|
-- a preset for the diorama does not get to spend a second draw of the whole
|
||||||
-- world on a machine that cannot carry one
|
-- 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")
|
"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
|
-- VR survives FULL on AA's reasoning: whether a headset is on the desk is
|
||||||
-- not the diorama's to decide
|
-- 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
|
-- 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
|
-- 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
|
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["tilt"], "TILT is off the OPTIONS menu")
|
||||||
T.check(not fxIds["gbcfx"], "and so is GBC FX")
|
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")
|
"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,
|
T.eq(fxGame.save.options.tilt, 0,
|
||||||
"a save that had TILT on is pinned back to off, not left on with no row")
|
"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)
|
Pipelines.setLevel("voxel", 2)
|
||||||
end
|
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
|
-- What the engine's list carries is ONE row, and it leads. What that row opens
|
||||||
-- additions at the END of the list, which would leave this mode's four rows
|
-- is the mod: the two pipeline rows it took off the engine's list, and the
|
||||||
-- in two places with unrelated rows between them.
|
-- four categories.
|
||||||
Pipelines.setLevel("voxel", 2)
|
Pipelines.setLevel("voxel", 2)
|
||||||
|
do
|
||||||
local grouped = Runtime.call("ui.options.rows", function(_, r) return r end,
|
local grouped = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||||
{ data = Data },
|
{ data = Data },
|
||||||
{ { id = "tilt" }, { id = "pipeline:voxel" },
|
{ { id = "tilt" }, { id = "pipeline:voxel" },
|
||||||
{ id = "pipeline:tiltshift" },
|
{ id = "pipeline:tiltshift" },
|
||||||
{ id = "void_fill" } })
|
{ id = "void_fill" } })
|
||||||
local order = {}
|
-- the engine gave it four rows and got three back: three of ours are gone
|
||||||
for i, row in ipairs(grouped) do order[row.id] = i end
|
-- (tilt dropped, both pipelines moved) and one is added, at the front
|
||||||
T.check(order["pipeline:tiltshift"] < order["DRAMATIC_SHAPE:grid"],
|
-- the engine offered four and two come back: TILT dropped, both pipeline rows
|
||||||
"the mode's settings follow its pipeline rows")
|
-- moved onto the mod's own menu, and one row added at the front
|
||||||
-- V-GRID, V-CURVE, RENDER DIST, WATER, FOREST FX, then 3D-BTL
|
T.eq(#grouped, 2, "one row for the whole mod, and the engine's own beneath it")
|
||||||
T.eq(order["DRAMATIC_SHAPE:battles"] - order["pipeline:tiltshift"], 6,
|
T.eq(grouped[1].id, Menus.catId(Menus.lib.ROOT), "and it leads the list")
|
||||||
"and sit in one unbroken block, not scattered to the end of the list")
|
T.eq(grouped[2].id, "void_fill",
|
||||||
T.check(order["void_fill"] > order["DRAMATIC_SHAPE:battles"],
|
"with the engine's own rows following in the order it built them")
|
||||||
"with the engine's own later rows still after 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
|
-- ------- 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 menu reads its row list every frame but builds it once, so without a
|
||||||
-- a rebuild the rows FULL owns stay on screen until the menu is reopened --
|
-- rebuild the rows FULL owns stay on screen until it is reopened -- and
|
||||||
-- and stepping OFF FULL never brings them back.
|
-- stepping OFF FULL never brings them back.
|
||||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||||
local pressed = {}
|
local pressed = {}
|
||||||
local menuGame = {
|
local menuGame = {
|
||||||
@@ -343,17 +452,30 @@ local menuGame = {
|
|||||||
save = { options = { pipelines = {}, modOptions = {} } },
|
save = { options = { pipelines = {}, modOptions = {} } },
|
||||||
mods = { modOptions = {} },
|
mods = { modOptions = {} },
|
||||||
input = { wasPressed = function(_, k) return pressed[k] or false end },
|
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,
|
writeOptions = function() end,
|
||||||
}
|
}
|
||||||
|
|
||||||
Pipelines.setLevel("voxel", 2)
|
|
||||||
local menu = OptionsMenu.new(menuGame)
|
|
||||||
local function rowIndex(m, id)
|
local function rowIndex(m, id)
|
||||||
for i, row in ipairs(m.rows) do if row.id == id then return i end end
|
for i, row in ipairs(m.rows) do if row.id == id then return i end 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
|
-- step the VOXEL row from 15 down to FULL, the way the player would
|
||||||
menu.index = rowIndex(menu, "pipeline:voxel")
|
menu.index = rowIndex(menu, "pipeline:voxel")
|
||||||
@@ -361,10 +483,12 @@ pressed = { left = true }
|
|||||||
menu:update(0)
|
menu:update(0)
|
||||||
pressed = {}
|
pressed = {}
|
||||||
T.eq(Pipelines.level("voxel"), 1, "the step landed on FULL")
|
T.eq(Pipelines.level("voxel"), 1, "the step landed on FULL")
|
||||||
T.check(not rowIndex(menu, "DRAMATIC_SHAPE:grid"),
|
T.check(not rowIndex(menu, Menus.catId("world")),
|
||||||
"and the rows FULL owns left the OPEN menu at once")
|
"and the category FULL emptied left the OPEN menu at once")
|
||||||
T.check(not rowIndex(menu, "pipeline:tiltshift"), "T-SHIFT with them")
|
T.check(not rowIndex(menu, "pipeline:tiltshift"), "T-SHIFT with it")
|
||||||
T.check(menu.index <= #menu.rows + 1, "the cursor stayed in range")
|
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
|
-- and back off it again
|
||||||
menu.index = rowIndex(menu, "pipeline:voxel")
|
menu.index = rowIndex(menu, "pipeline:voxel")
|
||||||
@@ -372,16 +496,17 @@ pressed = { right = true }
|
|||||||
menu:update(0)
|
menu:update(0)
|
||||||
pressed = {}
|
pressed = {}
|
||||||
T.eq(Pipelines.level("voxel"), 2, "the step left FULL")
|
T.eq(Pipelines.level("voxel"), 2, "the step left FULL")
|
||||||
T.check(rowIndex(menu, "DRAMATIC_SHAPE:grid"),
|
T.check(rowIndex(menu, Menus.catId("world")),
|
||||||
"and the rows came straight back without reopening the menu")
|
"and the category came straight back without reopening the menu")
|
||||||
T.check(rowIndex(menu, "pipeline:tiltshift"), "T-SHIFT too")
|
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
|
-- The switch lives on the mod's BATTLES menu now, and the stack ticks its TOP
|
||||||
-- follow the row it was ON rather than the slot it was in -- otherwise the very
|
-- state only (src/core/StateStack.update). So the step happens while the
|
||||||
-- press that switched staged battles on would leave the cursor a row further
|
-- OPTIONS menu underneath is suspended, and a before/after pair taken around
|
||||||
-- down than the player left it.
|
-- 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
|
do
|
||||||
local Battles = run.loader.exports.DRAMATIC_SHAPE.lib.require("OverworldBattle")
|
local Battles = run.loader.exports.DRAMATIC_SHAPE.lib.require("OverworldBattle")
|
||||||
Battles.setting:setValue(false, menuGame) -- staged battles off
|
Battles.setting:setValue(false, menuGame) -- staged battles off
|
||||||
@@ -390,37 +515,214 @@ Pipelines.setLevel("voxel", 2)
|
|||||||
local layoutMenu = OptionsMenu.new(menuGame)
|
local layoutMenu = OptionsMenu.new(menuGame)
|
||||||
T.check(rowIndex(layoutMenu, "battleLayout"),
|
T.check(rowIndex(layoutMenu, "battleLayout"),
|
||||||
"with staged battles off, the engine's BATTLE LAYOUT row is on the menu")
|
"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 }
|
pressed = { right = true }
|
||||||
layoutMenu:update(0)
|
battleMenu:update(0)
|
||||||
pressed = {}
|
pressed = {}
|
||||||
T.eq(Battles.setting:get(), true, "the step switched staged battles on")
|
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(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")
|
"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
|
end
|
||||||
|
|
||||||
-- level 2 is the "15" rung: any rung that is not FULL, so the settings the
|
-- level 2 is the "15" rung: any rung that is not FULL, so the settings the
|
||||||
-- preset owns are back on the menu
|
-- preset owns are back on the menu
|
||||||
Pipelines.setLevel("voxel", 2)
|
Pipelines.setLevel("voxel", 2)
|
||||||
local hookedRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
do
|
||||||
{ data = Data }, { { id = "text_speed" } })
|
-- 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
|
-- 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
|
-- (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
|
-- on every platform, saying WHERE? rather than IMPORT where there is no file
|
||||||
-- dialog to open
|
-- dialog to open -- and the four category rows that carry them
|
||||||
T.eq(#hookedRows, 14, "the options hook added a row per setting, plus the "
|
do
|
||||||
.. "STADIUM ROM action row")
|
-- named rather than counted: a count says "something moved" and this says
|
||||||
local grid, curve, water = hookedRows[2], hookedRows[3], hookedRows[5]
|
-- WHAT is meant to be reachable. SMOOTH TURN is absent because VR is off,
|
||||||
local battles, backRow, daytime = hookedRows[7], hookedRows[8], hookedRows[10]
|
-- which is its own gate and is tested where VR is turned on.
|
||||||
-- the RENDER DIST row is hookedRows[4], FOREST FX hookedRows[6], LET'S GO
|
local want = { "grid", "curve", "viewbox", "water", "daytime",
|
||||||
-- hookedRows[9], SHADOWS hookedRows[11] and AA hookedRows[12]; all five are
|
"battles", "battleBack", "letsgo", "stadiumRom",
|
||||||
-- read where they are used rather than named here, because this chunk is one
|
"atmos", "shadows", "aa", "vr" }
|
||||||
-- main function and has 200 local slots to spend
|
local n = 0
|
||||||
T.eq(hookedRows[4].label, "RENDER DIST", "the viewport row carries its label")
|
for _ in pairs(hookedRows) do n = n + 1 end
|
||||||
T.eq(hookedRows[4].value(), "FIT",
|
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 "
|
"and defaults to FIT -- the cut IS the window the flat game already "
|
||||||
.. "framed, which is the whole claim the row makes")
|
.. "framed, which is the whole claim the row makes")
|
||||||
T.eq(water.label, "WATER", "the water row carries its label")
|
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)
|
water.step({ save = { options = {} }, mods = { modOptions = {} } }, 1)
|
||||||
T.eq(water.value(), "SKY",
|
T.eq(water.value(), "SKY",
|
||||||
"stepping down drops the screen-space march and keeps the sky, sun and moon")
|
"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["DRAMATIC_SHAPE:atmos"].label, "FOREST FX", "the atmosphere row carries its label")
|
||||||
T.eq(hookedRows[6].value(), "FULL",
|
T.eq(hookedRows["DRAMATIC_SHAPE:atmos"].value(), "FULL",
|
||||||
"and defaults to FULL -- it only spends anything on a map with an "
|
"and defaults to FULL -- it only spends anything on a map with an "
|
||||||
.. "atmosphere entry, which is one forest today")
|
.. "atmosphere entry, which is one forest today")
|
||||||
T.eq(daytime.label, "DAYTIME", "the day/night row carries its label")
|
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")
|
.. "map, so the classic slot is opt-in")
|
||||||
T.check(backRow.id ~= battles.id and backRow.id:find("battleBack", 1, true),
|
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")
|
"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["DRAMATIC_SHAPE:letsgo"].label, "LET'S GO", "the capture-mode row carries its label")
|
||||||
T.eq(hookedRows[9].value(), "OFF",
|
T.eq(hookedRows["DRAMATIC_SHAPE:letsgo"].value(), "OFF",
|
||||||
"and starts OFF -- a gameplay mode is opt-in, whatever the diorama does")
|
"and starts OFF -- a gameplay mode is opt-in, whatever the diorama does")
|
||||||
|
|
||||||
-- stepping writes through to the one place both rows read
|
-- 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 AntiAlias = run.loader.exports.DRAMATIC_SHAPE.lib.require("AntiAlias")
|
||||||
local VoxelGrid = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelGrid")
|
local VoxelGrid = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelGrid")
|
||||||
local aaGame = { save = { options = {} }, mods = { modOptions = {} } }
|
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.label, "AA", "the anti-aliasing row carries its label")
|
||||||
T.eq(aa.value(), "OFF",
|
T.eq(aa.value(), "OFF",
|
||||||
"and starts off -- supersampling is a cost knob, and a mod must not spend "
|
"and starts off -- supersampling is a cost knob, and a mod must not spend "
|
||||||
@@ -588,7 +890,7 @@ end
|
|||||||
;(function()
|
;(function()
|
||||||
local Shadows = run.loader.exports.DRAMATIC_SHAPE.lib.require("Shadows")
|
local Shadows = run.loader.exports.DRAMATIC_SHAPE.lib.require("Shadows")
|
||||||
local ShadowMap = run.loader.exports.DRAMATIC_SHAPE.lib.require("ShadowMap")
|
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 = {} } }
|
local shGame = { save = { options = {} }, mods = { modOptions = {} } }
|
||||||
T.eq(shadowRow.label, "SHADOWS", "the shadow row carries its label")
|
T.eq(shadowRow.label, "SHADOWS", "the shadow row carries its label")
|
||||||
T.eq(shadowRow.value(), "ON",
|
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
|
-- ...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
|
-- other absent rows come off: a row that no longer decides anything is worse
|
||||||
-- than no row
|
-- than no row
|
||||||
local offRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
local offRows = Menus.rows("battles", backGame)
|
||||||
backGame, { { id = "tilt" } })
|
T.check(offRows["DRAMATIC_SHAPE:battles"], "3D-BTL itself is still offered")
|
||||||
local offIds = {}
|
T.check(not offRows["DRAMATIC_SHAPE:battleBack"],
|
||||||
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"],
|
|
||||||
"but BACK SPRITES is off the menu while there is no staged fight to be about")
|
"but BACK SPRITES is off the menu while there is no staged fight to be about")
|
||||||
|
|
||||||
Battles.setting:setValue(true, backGame)
|
Battles.setting:setValue(true, backGame)
|
||||||
local onRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
-- switched back on it returns, and to the BATTLES menu -- the one the switch
|
||||||
backGame, { { id = "tilt" } })
|
-- it depends on is on, so the two are read together
|
||||||
local onAt = {}
|
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.check(onAt["DRAMATIC_SHAPE:battleBack"], "switched back on, so is the row")
|
||||||
T.eq(onAt["DRAMATIC_SHAPE:battleBack"] - onAt["DRAMATIC_SHAPE:battles"], 1,
|
T.eq(onAt["DRAMATIC_SHAPE:battleBack"] - onAt["DRAMATIC_SHAPE:battles"], 1,
|
||||||
"directly under the row it belongs to")
|
"directly under the row it belongs to")
|
||||||
@@ -5962,26 +6261,28 @@ end)()
|
|||||||
local VRMod = lib.require("VR")
|
local VRMod = lib.require("VR")
|
||||||
T.eq(VRMod.smoothTurn:get(), false, "SMOOTH TURN is off out of the box")
|
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
|
Pipelines.setLevel("voxel", 3) -- off FULL, which owns other rows
|
||||||
VRMod.setting:sync(false)
|
VRMod.setting:sync(false)
|
||||||
T.check(not optionRows()["DRAMATIC_SHAPE:smoothturn"],
|
T.check(not Menus.rows("vr")["DRAMATIC_SHAPE:smoothturn"],
|
||||||
"with VR off the row is not on the OPTIONS menu")
|
"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)
|
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")
|
T.check(smoothRow ~= nil, "and with VR on it is")
|
||||||
if smoothRow then
|
if smoothRow then
|
||||||
T.eq(smoothRow.label, "SMOOTH TURN", "under its own name")
|
T.eq(smoothRow.label, "SMOOTH TURN", "under its own name")
|
||||||
T.eq(smoothRow.value(), "OFF", "reading OFF until the player says otherwise")
|
T.eq(smoothRow.value(), "OFF", "reading OFF until the player says otherwise")
|
||||||
end
|
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)
|
VRMod.setting:sync(false)
|
||||||
|
|
||||||
-- ------- the gun's own bookkeeping
|
-- ------- 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