mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 08:31:06 +02:00
added FULL menu option for voxel mod
This commit is contained in:
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Added
|
||||
|
||||
- **A `FULL` rung on the VOXEL row**, directly after `OFF`. One choice that
|
||||
puts the whole mode in its intended state -- the 50-degree camera, the
|
||||
miniature blur at maximum, the horizon flat, the view fitted, and battles
|
||||
on the map -- rather than making a player assemble it from four rows.
|
||||
|
||||
While it is selected, every row it owns comes OFF the menu: V-GRID,
|
||||
V-CURVE, 3D-BTL and T-SHIFT. A row that no longer decides anything is
|
||||
worse than no row. Stepping onto or off `FULL` rebuilds the open menu in
|
||||
place, so the rows leave and return under the cursor instead of waiting
|
||||
for the menu to be reopened.
|
||||
|
||||
It applies its settings when the row ARRIVES at `FULL`, not every frame:
|
||||
holding them would make the zoom keys and the wheel dead while it was on.
|
||||
Leaving it deliberately undoes nothing -- reverting would discard whatever
|
||||
had been changed since.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Hotkey `3` walks the angle rungs only and steps over `FULL`.** The key is
|
||||
a display-mode cycler -- it should change the camera and nothing else --
|
||||
and `FULL` reaches in and rewrites four other settings. Landing on it
|
||||
mid-walk would silently push the blur to maximum and flatten the horizon
|
||||
with nothing on screen saying a keypress had done it. `FULL` stays on the
|
||||
OPTIONS row, where a preset that changes other rows belongs.
|
||||
|
||||
A press FROM `FULL` goes to `75`. `FULL` is already the 50-degree camera,
|
||||
so stepping to the rung of that name would look like the key had done
|
||||
nothing.
|
||||
|
||||
- **The mode's four options are one block in the menu.** The engine splices
|
||||
a pipeline row in beside TILT and lands a mod's own rows at the end of the
|
||||
list, which had these four in two places with unrelated engine rows
|
||||
between them. The settings now follow the pipeline rows directly.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Added
|
||||
|
||||
@@ -139,8 +139,10 @@ return {
|
||||
|
||||
-- ------- the remaining routes
|
||||
["ROUTE_19"] = { x = 8, y = 6, shape = "narrow" },
|
||||
["ROUTE_20"] = { x = 53, y = 3, shape = "wide" },
|
||||
["ROUTE_21"] = { x = 16, y = 4, shape = "wide" },
|
||||
-- the two surf routes fight AFLOAT, in the middle of their own sea rather
|
||||
-- than on the rim of beach the land search would otherwise find
|
||||
["ROUTE_20"] = { x = 23, y = 7, shape = "wide" },
|
||||
["ROUTE_21"] = { x = 8, y = 46, shape = "wide" },
|
||||
["ROUTE_22"] = { x = 35, y = 7, shape = "wide" },
|
||||
["ROUTE_23"] = { x = 4, y = 36, shape = "wide" },
|
||||
["ROUTE_24"] = { x = 13, y = 15, shape = "wide" },
|
||||
|
||||
+8
-1
@@ -291,7 +291,14 @@ function BattleArena.find(map, fromX, fromY, surfing)
|
||||
host = (ok and other) or nil
|
||||
end
|
||||
if shape and host then
|
||||
local grid, gw = openGrid(host, surfing)
|
||||
-- An authored spot is checked with WATER COUNTING AS GROUND, whatever
|
||||
-- the player is doing. The surfing test exists to stop the automatic
|
||||
-- search staging a walker's fight out at sea; an authored entry was
|
||||
-- chosen and looked at by a person, so if it is on water that is the
|
||||
-- point of it -- the surf routes fight in the middle of their own
|
||||
-- ocean rather than on a scrap of beach at the edge of the map. Land
|
||||
-- entries are unaffected: land passes the test either way.
|
||||
local grid, gw = openGrid(host, true)
|
||||
if fits(grid, gw, pick.x, pick.y, shape.w, shape.h) then
|
||||
local arena = place(shape, pick.x, pick.y)
|
||||
arena.map = host
|
||||
|
||||
@@ -40,6 +40,7 @@ local TerrainAtlas = V.require("TerrainAtlas")
|
||||
local VoxelScene = V.require("VoxelScene")
|
||||
local BattleCam = V.require("BattleCam")
|
||||
local BattleBillboard = V.require("BattleBillboard")
|
||||
local VoxelGrid = V.require("VoxelGrid")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
|
||||
local BattleScene = {}
|
||||
@@ -333,6 +334,12 @@ function BattleScene.render(state, arena, textures, token)
|
||||
-- free-roam world it shares this module with keeps its own weight
|
||||
local sunWas = Voxel3D.SHADOW_ALPHA
|
||||
Voxel3D.SHADOW_ALPHA = BattleScene.SHADOW_ALPHA
|
||||
-- and the wireframe is ON for a battle whatever the V-GRID row says. The
|
||||
-- arena is a staged shot rather than the world being walked through, and
|
||||
-- the seams are what make it read as built rather than photographed. Forced
|
||||
-- through the override so the player's own row is never written to.
|
||||
local gridWas = VoxelGrid.override
|
||||
VoxelGrid.override = true
|
||||
local out = nil
|
||||
local ok, err = pcall(function()
|
||||
-- its own canvas slot: this renders at the window's pixel size and the
|
||||
@@ -408,6 +415,7 @@ function BattleScene.render(state, arena, textures, token)
|
||||
-- renders (the free-roam pipeline, next frame) must find the orbit back
|
||||
Voxel3D.camera = nil
|
||||
Voxel3D.SHADOW_ALPHA = sunWas
|
||||
VoxelGrid.override = gridWas
|
||||
if not ok then
|
||||
-- endScene never ran, so the canvas is still bound and the shader still
|
||||
-- set; put the frame back the way it was found before rethrowing
|
||||
|
||||
@@ -48,7 +48,19 @@ VoxelGrid.WIDTH = 1.0
|
||||
VoxelGrid.setting = ModSetting.new(VoxelGrid.KEY, VoxelGrid.LABEL,
|
||||
{ false, true }, { "OFF", "ON" })
|
||||
|
||||
-- A pass that needs the wireframe whatever the player left the row on sets
|
||||
-- this for the length of its own draw and puts it back after. nil means
|
||||
-- "follow the setting", which is every frame outside such a pass.
|
||||
--
|
||||
-- The overworld battle is the one user: a fight is a STAGED shot, not the
|
||||
-- world being walked around in, and the seams are what make it read as
|
||||
-- constructed rather than as a photograph of somewhere. The row still owns
|
||||
-- what free-roam looks like, and is not written to -- switching the mode off
|
||||
-- mid-battle would silently rewrite the player's own setting.
|
||||
VoxelGrid.override = nil
|
||||
|
||||
function VoxelGrid.enabled()
|
||||
if VoxelGrid.override ~= nil then return VoxelGrid.override end
|
||||
return VoxelGrid.setting:get() and true or false
|
||||
end
|
||||
|
||||
|
||||
+51
-2
@@ -23,10 +23,59 @@
|
||||
|
||||
local Voxel = {}
|
||||
|
||||
Voxel.ANGLES_DEG = { 0, 15, 35, 50, 75 }
|
||||
Voxel.ANGLE_LABELS = { "OFF", "15", "35", "50", "75" }
|
||||
-- FULL is a PRESET, not another angle: one rung that puts the whole mode in
|
||||
-- its intended state at once -- this camera, the miniature blur at full, the
|
||||
-- horizon flat, the view fitted -- so a player who wants "the diorama" picks
|
||||
-- it rather than assembling it from four rows. It sits directly after OFF
|
||||
-- because that is the order those two get used in.
|
||||
--
|
||||
-- Its ANGLE is 50 degrees, the same as the rung of that name. The duplicate
|
||||
-- in the table is deliberate: the ladder is a list of what each rung LOOKS
|
||||
-- like, and two rungs may look the same while meaning different things.
|
||||
Voxel.ANGLES_DEG = { 0, 50, 15, 35, 50, 75 }
|
||||
Voxel.ANGLE_LABELS = { "OFF", "FULL", "15", "35", "50", "75" }
|
||||
Voxel.MAX_LEVEL = #Voxel.ANGLES_DEG - 1
|
||||
|
||||
-- the rung FULL sits on, so nothing has to hunt for it by label
|
||||
Voxel.FULL_LEVEL = 1
|
||||
|
||||
function Voxel.isFull(level)
|
||||
return (level or Voxel.level) == Voxel.FULL_LEVEL
|
||||
end
|
||||
|
||||
-- ------- what the hotkey walks
|
||||
--
|
||||
-- The ANGLE rungs only, with FULL left out. The key is a display-mode
|
||||
-- cycler: pressing it should change the camera and nothing else, and FULL
|
||||
-- reaches in and rewrites four other settings. Landing on it by accident,
|
||||
-- mid-walk, would silently turn the blur to maximum and flatten the horizon
|
||||
-- with no indication that a keypress had done so. FULL stays on the OPTIONS
|
||||
-- row, which is where a preset that changes other rows belongs.
|
||||
Voxel.HOTKEY_ORDER = { 0, 2, 3, 4, 5 } -- OFF, 15, 35, 50, 75
|
||||
|
||||
-- The rung a press moves to from `level`.
|
||||
--
|
||||
-- A level that is not on the key's path -- FULL, reached from the menu --
|
||||
-- steps on from whichever rung shows the SAME camera it does. FULL is 50
|
||||
-- degrees, so a press from it goes to 75 rather than back to 50, and the key
|
||||
-- never appears to do nothing.
|
||||
function Voxel.nextHotkeyLevel(level)
|
||||
level = level or Voxel.level
|
||||
local order = Voxel.HOTKEY_ORDER
|
||||
local at = nil
|
||||
for i, rung in ipairs(order) do
|
||||
if rung == level then at = i break end
|
||||
end
|
||||
if not at then
|
||||
local deg = Voxel.ANGLES_DEG[level + 1]
|
||||
for i, rung in ipairs(order) do
|
||||
if Voxel.ANGLES_DEG[rung + 1] == deg then at = i break end
|
||||
end
|
||||
end
|
||||
if not at then return order[1] end
|
||||
return order[at % #order + 1]
|
||||
end
|
||||
|
||||
Voxel.level = 0
|
||||
Voxel.angle = 0
|
||||
Voxel.from = 0
|
||||
|
||||
@@ -79,6 +79,12 @@ local VoxelGrid = V.require("VoxelGrid")
|
||||
local WorldCurve = V.require("WorldCurve")
|
||||
local OverworldBattle = V.require("OverworldBattle")
|
||||
|
||||
-- Forward declaration: the voxel pipeline's update hook (registered below)
|
||||
-- calls this, and it is defined further down with the settings it drives.
|
||||
-- Declared rather than left global -- a mod writing to _G would leak into
|
||||
-- every other mod's namespace.
|
||||
local applyFull
|
||||
|
||||
-- The last VOID FILL the terrain was meshed under; see the update hook.
|
||||
-- The scene canvas's size, in FRAMEBUFFER PIXELS.
|
||||
--
|
||||
@@ -145,6 +151,11 @@ mod.content.render_pipelines:register("voxel", {
|
||||
-- pump slice -- so stepping out of a door lands on terrain that is
|
||||
-- already there instead of a flat flash.
|
||||
update = function(dt, level)
|
||||
-- FULL is a preset, so it is applied ON THE PRESS rather than held every
|
||||
-- frame: it SETS the other rows and then leaves them alone. Holding them
|
||||
-- would make the zoom keys and the wheel dead while the mode was on, and
|
||||
-- would fight anyone who changed one deliberately.
|
||||
applyFull(level)
|
||||
Voxel.update(dt, level)
|
||||
-- The overworld battle rides this hook rather than owning a pipeline of
|
||||
-- its own, because it owns no pass of the FRAME: it draws under a battle
|
||||
@@ -233,6 +244,46 @@ mod.content.render_pipelines:register("tiltshift", {
|
||||
-- instead -- see ModSetting for where they persist and how the two rows
|
||||
-- each ends up on stay in step.
|
||||
|
||||
-- ------- the FULL preset
|
||||
--
|
||||
-- Everything the mode wants switched to at once. Applied when the VOXEL row
|
||||
-- ARRIVES at FULL and not again, so the player can still move the camera or
|
||||
-- the zoom afterwards -- it is a starting point, not a lock.
|
||||
--
|
||||
-- Leaving FULL deliberately does NOT undo any of it. A preset that reverted
|
||||
-- would throw away whatever the player had changed since, and "put it back
|
||||
-- how it was" is not a thing this can know.
|
||||
local fullWas = nil
|
||||
|
||||
applyFull = function(level)
|
||||
local isFull = Voxel.isFull(level)
|
||||
local was = fullWas
|
||||
fullWas = isFull
|
||||
if not isFull or was == true or was == nil then return end
|
||||
|
||||
local Game = require("src.core.Game")
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
local Zoom = require("src.render.Zoom")
|
||||
local opts = Game.save and Game.save.options
|
||||
if not opts then return end
|
||||
|
||||
-- the miniature blur at its strongest: FULL is the diorama look, and the
|
||||
-- tilt-shift is most of what makes it read as a model
|
||||
Pipelines.setLevel("tiltshift", Pipelines.maxLevel("tiltshift"))
|
||||
Pipelines.syncOptions(opts)
|
||||
-- the horizon flat. The curve bends the world away from a walking player,
|
||||
-- which fights a fixed diorama framing
|
||||
WorldCurve.setting:setIndex(1, Game)
|
||||
-- and the view fitted to the window
|
||||
opts.zoom = 0
|
||||
Zoom.applyOptions(opts)
|
||||
-- battles on the map too: FULL means the whole mode, and a fight is where
|
||||
-- half of it is spent. Set rather than forced -- the row is gone from the
|
||||
-- menu while FULL is on, but a save that already had it off gets it on.
|
||||
OverworldBattle.setting:setIndex(1, Game)
|
||||
if Game.writeOptions then pcall(Game.writeOptions, Game) end
|
||||
end
|
||||
|
||||
local SETTINGS = {
|
||||
{ VoxelGrid.setting, "One-pixel wireframe along every voxel edge." },
|
||||
{ WorldCurve.setting,
|
||||
@@ -250,7 +301,7 @@ mod.options:define(schema)
|
||||
|
||||
-- ------- this mod's hotkeys
|
||||
--
|
||||
-- 3 VOXEL cycle the camera ladder (was 6)
|
||||
-- 3 VOXEL cycle the camera ladder (was 6; skips FULL)
|
||||
-- 5 V-GRID toggle the wireframe (new)
|
||||
-- 6 T-SHIFT cycle the blur ladder (was 9)
|
||||
-- 7 V-CURVE cycle the horizon bend (new)
|
||||
@@ -299,7 +350,20 @@ do
|
||||
-- render mode. Only free-roam presses are ours to take.
|
||||
if claim and not (top and top.onKeyPressed) then
|
||||
if claim == "pipeline" then
|
||||
if Pipelines.hotkey(key, top, self.overworld) then
|
||||
-- 3 walks the ANGLE rungs and steps over FULL (Voxel.HOTKEY_ORDER),
|
||||
-- so the registry's plain "advance one and wrap" is not what it
|
||||
-- wants; 6 still is. The gate is the registry's own either way.
|
||||
local stepped = false
|
||||
if key == "3" then
|
||||
if Pipelines.canToggle("voxel", top, self.overworld) then
|
||||
Pipelines.setLevel("voxel",
|
||||
Voxel.nextHotkeyLevel(Pipelines.level("voxel")))
|
||||
stepped = true
|
||||
end
|
||||
else
|
||||
stepped = Pipelines.hotkey(key, top, self.overworld) and true
|
||||
end
|
||||
if stepped then
|
||||
Pipelines.syncOptions(self.save.options)
|
||||
-- 3 is the key that used to turn TILT on and sits next to the one
|
||||
-- that used to turn GBC FX on, and this mod has taken both away.
|
||||
@@ -336,15 +400,54 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
-- ------- the mode's rows, kept together
|
||||
--
|
||||
-- The engine splices a pipeline's row in beside TILT, because a display mode
|
||||
-- belongs with the other display modes; a mod's own ui.options.rows
|
||||
-- additions land at the END of the list. That left this mod's four rows in
|
||||
-- two places with unrelated engine rows between them, which reads as two
|
||||
-- unrelated features rather than one mode with settings.
|
||||
--
|
||||
-- So the plain settings are inserted directly after the last of this mod's
|
||||
-- PIPELINE rows instead of appended. Nothing else moves: the block lands
|
||||
-- where the engine already decided display modes go.
|
||||
local function insertGrouped(out, extra)
|
||||
local anchor = nil
|
||||
for i, row in ipairs(out) do
|
||||
local id = type(row) == "table" and row.id
|
||||
if id == "pipeline:voxel" or id == "pipeline:tiltshift" then anchor = i end
|
||||
end
|
||||
if not anchor then
|
||||
for _, row in ipairs(extra) do out[#out + 1] = row end
|
||||
return out
|
||||
end
|
||||
for i, row in ipairs(extra) do table.insert(out, anchor + i, row) end
|
||||
return out
|
||||
end
|
||||
|
||||
-- FULL owns every one of those settings, so while it is selected they are
|
||||
-- taken off the menu rather than left to be changed under it -- including
|
||||
-- T-SHIFT, which is a pipeline row the engine put there. A row that no
|
||||
-- longer decides anything is worse than no row.
|
||||
local function dropRow(out, id)
|
||||
for i = #out, 1, -1 do
|
||||
if type(out[i]) == "table" and out[i].id == id then table.remove(out, i) end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- call next() first and decorate what comes back, so every other mod's
|
||||
-- rows survive this one
|
||||
mod.hooks:wrap("ui.options.rows", function(next, game, rows)
|
||||
local out = next(game, rows)
|
||||
if type(out) ~= "table" then return out end
|
||||
for _, entry in ipairs(SETTINGS) do
|
||||
out[#out + 1] = entry[1]:row()
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
if Voxel.isFull(Pipelines.level("voxel")) then
|
||||
return dropRow(out, "pipeline:tiltshift")
|
||||
end
|
||||
return out
|
||||
local extra = {}
|
||||
for _, entry in ipairs(SETTINGS) do extra[#extra + 1] = entry[1]:row() end
|
||||
return insertGrouped(out, extra)
|
||||
end)
|
||||
|
||||
-- The mod manager writes and persists on its own, so the only thing left
|
||||
@@ -436,6 +539,42 @@ mod.events:on("map.reloaded", function(payload)
|
||||
if mapId then ChunkMesher.invalidate(mapId) end
|
||||
end)
|
||||
|
||||
-- ------- FULL takes rows off the menu, so the menu has to notice
|
||||
--
|
||||
-- OptionsMenu builds its row list ONCE, when it is opened, and then reads
|
||||
-- that list every frame. So stepping the VOXEL row onto or off FULL changed
|
||||
-- which rows the hook would return but not which rows were on screen -- the
|
||||
-- settings FULL owns stayed visible until the menu was closed and reopened,
|
||||
-- and a player who stepped off FULL could not see the rows come back.
|
||||
--
|
||||
-- Rebuilt in place, and only on a step that crosses FULL: every other rung
|
||||
-- returns the same list, and rebuilding on all of them would rerun every
|
||||
-- mod's ui.options.rows hook once per keypress. The cursor is clamped rather
|
||||
-- than reset, so it stays on the VOXEL row it was just used on instead of
|
||||
-- jumping to the top when the list below it shortens.
|
||||
do
|
||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||
if not OptionsMenu.dramaticShapeFullHook then
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
local inner = OptionsMenu.update
|
||||
|
||||
function OptionsMenu:update(dt)
|
||||
local before = Pipelines.level("voxel")
|
||||
inner(self, dt)
|
||||
local after = Pipelines.level("voxel")
|
||||
if after ~= before
|
||||
and (Voxel.isFull(before) or Voxel.isFull(after)) then
|
||||
local rebuilt = OptionsMenu.new(self.game)
|
||||
self.rows = rebuilt.rows
|
||||
local cancel = #self.rows + 1
|
||||
if (self.index or 1) > cancel then self.index = cancel end
|
||||
end
|
||||
end
|
||||
|
||||
OptionsMenu.dramaticShapeFullHook = true
|
||||
end
|
||||
end
|
||||
|
||||
-- ------- battles on the map
|
||||
--
|
||||
-- The wraps this needs -- OverworldState:pushBattle, BattleState:draw and
|
||||
@@ -481,7 +620,7 @@ mod.events:on("battle.ended", function()
|
||||
OverworldBattle.finish()
|
||||
end)
|
||||
|
||||
mod.exports.version = "1.1.0"
|
||||
mod.exports.version = "1.2.0"
|
||||
-- exposed so a companion mod can pin its own tiles' shapes or read the
|
||||
-- camera without reaching into this mod's file layout
|
||||
mod.exports.lib = V
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "DRAMATIC_SHAPE",
|
||||
"name": "Dramatic Shape Voxel Mod",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"api": 2,
|
||||
"entry": "main.lua",
|
||||
"profile": "content",
|
||||
@@ -11,7 +11,9 @@
|
||||
"dependencies": [],
|
||||
"optional_dependencies": [],
|
||||
"conflicts": [],
|
||||
"permissions": ["engine_internals"],
|
||||
"permissions": [
|
||||
"engine_internals"
|
||||
],
|
||||
"affects_link": false,
|
||||
"description": "A full 3D diorama overworld: extruded terrain, depth-buffered occlusion, voxel characters and a tilt-shift miniature pass -- and battles fought on the map itself, shot over the shoulder at the nearest clear ground with a slow parallax drift and a depth-of-field pass. Registers two render pipelines and claims hotkeys 3, 5, 6, 7 and 8 -- 3 and 5 displace the engine's TILT and GBC FX keys, both still reachable on the OPTIONS menu. Presentational only: it changes what a battle is drawn over, never where anybody stands."
|
||||
}
|
||||
|
||||
+65
-5
@@ -16,9 +16,11 @@
|
||||
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/arena_pick.lua love .
|
||||
--
|
||||
-- ARENA_FROM / ARENA_COUNT slice the map list so several runs can share the
|
||||
-- work; ARENA_MAPS=ID,ID,... does an explicit set instead. SHOT_DIR must
|
||||
-- already exist -- the capture writes with io.open, which does not create
|
||||
-- directories.
|
||||
-- work; ARENA_MAPS=ID,ID,... does an explicit set instead. ARENA_COUNT=0
|
||||
-- lists the maps and stops. ARENA_SURF=1 stages the fight out on the water,
|
||||
-- centred in the map's biggest body of it, which is what the surf routes
|
||||
-- want. SHOT_DIR must already exist -- the capture writes with io.open,
|
||||
-- which does not create directories.
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or ".scratchpad/arenas"
|
||||
@@ -37,6 +39,51 @@ return function(game)
|
||||
local Arena = lib.require("BattleArena")
|
||||
local Battles = lib.require("OverworldBattle")
|
||||
|
||||
-- ------- staging out on the water
|
||||
--
|
||||
-- A surf route's land is a rim of beach round the edge of the map, so the
|
||||
-- ordinary search always picks the rim and the fight happens on sand at
|
||||
-- the corner of a sea. ARENA_SURF=1 says stage this map afloat: water
|
||||
-- counts as ground, and the search starts from the middle of the map's
|
||||
-- BIGGEST body of water rather than the middle of the map, so the arena
|
||||
-- lands out in the open sea instead of against the first shoreline it
|
||||
-- finds.
|
||||
--
|
||||
-- Biggest body, not all water at once: a map with a lake and an ocean has
|
||||
-- a centroid between them that is on neither, and the arena would be
|
||||
-- pinned to whichever shore that landed nearest.
|
||||
local function waterCentre(map)
|
||||
local w, h = map.widthCells, map.heightCells
|
||||
local seen, best = {}, nil
|
||||
for y0 = 0, h - 1 do
|
||||
for x0 = 0, w - 1 do
|
||||
if not seen[y0 * w + x0] and map:isWaterCell(x0, y0) then
|
||||
-- one connected body, flooded from this cell
|
||||
local stack, n, sx, sy = { { x0, y0 } }, 0, 0, 0
|
||||
seen[y0 * w + x0] = true
|
||||
while #stack > 0 do
|
||||
local cell = table.remove(stack)
|
||||
local cx, cy = cell[1], cell[2]
|
||||
n, sx, sy = n + 1, sx + cx, sy + cy
|
||||
for _, d in ipairs({ { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }) do
|
||||
local nx, ny = cx + d[1], cy + d[2]
|
||||
local key = ny * w + nx
|
||||
if nx >= 0 and ny >= 0 and nx < w and ny < h and not seen[key]
|
||||
and map:isWaterCell(nx, ny) then
|
||||
seen[key] = true
|
||||
stack[#stack + 1] = { nx, ny }
|
||||
end
|
||||
end
|
||||
end
|
||||
if not best or n > best.n then
|
||||
best = { n = n, x = sx / n, y = sy / n }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
-- Which maps a battle can actually happen on: anything with a wild
|
||||
-- encounter table or an object that fights. Everything else -- a shop
|
||||
-- floor, a stairwell, a bedroom -- would be authoring a spot for a fight
|
||||
@@ -134,9 +181,22 @@ return function(game)
|
||||
clear = any and Arena.clearance(any.map or map, any) or false
|
||||
end
|
||||
end
|
||||
local surf = os.getenv("ARENA_SURF")
|
||||
surf = surf ~= nil and surf ~= "" and surf ~= "0"
|
||||
if not any then
|
||||
clear = Arena.search(map, cx, cy, false, true)
|
||||
any = clear or Arena.search(map, cx, cy, false)
|
||||
local ox, oy = cx, cy
|
||||
if surf then
|
||||
local sea = waterCentre(map)
|
||||
if sea then
|
||||
ox, oy = sea.x, sea.y
|
||||
U.log(("SEA %s: biggest body is %d cells, centre %.1f,%.1f")
|
||||
:format(id, sea.n, sea.x, sea.y))
|
||||
else
|
||||
U.log("SEA " .. id .. ": no water on this map")
|
||||
end
|
||||
end
|
||||
clear = Arena.search(map, ox, oy, surf, true)
|
||||
any = clear or Arena.search(map, ox, oy, surf)
|
||||
end
|
||||
if not any then
|
||||
U.log(("NONE %s -- no arena of either shape"):format(id))
|
||||
|
||||
@@ -49,11 +49,13 @@ T.eq(defs._owners and defs._owners.voxel, "DRAMATIC_SHAPE",
|
||||
|
||||
-- ------- the ladders the engine drives
|
||||
|
||||
T.eq(#defs.voxel.levels, 5, "voxel exposes a five-rung ladder")
|
||||
T.eq(#defs.voxel.levels, 6, "voxel exposes a six-rung ladder")
|
||||
T.eq(defs.voxel.levels[1], "OFF", "rung 0 is OFF")
|
||||
T.eq(defs.voxel.levels[5], "75", "the top rung is the 75-degree camera")
|
||||
T.eq(Pipelines.maxLevel("voxel"), 4, "the engine reads the ladder height")
|
||||
T.eq(Pipelines.levelLabel("voxel", 2), "35", "the engine reads the rung labels")
|
||||
T.eq(defs.voxel.levels[2], "FULL",
|
||||
"FULL is the first rung after OFF -- the order those two get used in")
|
||||
T.eq(defs.voxel.levels[6], "75", "the top rung is the 75-degree camera")
|
||||
T.eq(Pipelines.maxLevel("voxel"), 5, "the engine reads the ladder height")
|
||||
T.eq(Pipelines.levelLabel("voxel", 3), "35", "the engine reads the rung labels")
|
||||
|
||||
-- ------- gating: inert until switched on, and inert without a GPU
|
||||
|
||||
@@ -101,7 +103,7 @@ local byLabel = {}
|
||||
for _, row in ipairs(rows) do byLabel[row.label] = row end
|
||||
T.check(byLabel.VOXEL ~= nil, "the VOXEL row is offered")
|
||||
T.check(byLabel["T-SHIFT"] ~= nil, "the T-SHIFT row is offered")
|
||||
T.eq(byLabel.VOXEL.value(), "15", "the row renders the current rung's label")
|
||||
T.eq(byLabel.VOXEL.value(), "FULL", "the row renders the current rung's label")
|
||||
|
||||
-- ------- this mod's own settings
|
||||
--
|
||||
@@ -112,6 +114,95 @@ T.eq(byLabel.VOXEL.value(), "15", "the row renders the current rung's label")
|
||||
-- settings page looks.
|
||||
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local VoxelState = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelState")
|
||||
|
||||
-- ------- FULL is a preset that owns the other rows
|
||||
--
|
||||
-- While it is selected the settings it drives come OFF the menu -- including
|
||||
-- T-SHIFT, which is a pipeline row the engine spliced in. A row that no
|
||||
-- longer decides anything is worse than no row.
|
||||
Pipelines.setLevel("voxel", VoxelState.FULL_LEVEL)
|
||||
local fullRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data },
|
||||
{ { id = "tilt" }, { id = "pipeline:voxel" },
|
||||
{ id = "pipeline:tiltshift" } })
|
||||
local fullIds = {}
|
||||
for _, row in ipairs(fullRows) do fullIds[row.id] = true end
|
||||
T.check(fullIds["pipeline:voxel"], "FULL keeps the VOXEL row it lives on")
|
||||
T.check(not fullIds["pipeline:tiltshift"],
|
||||
"FULL takes T-SHIFT off the menu -- it owns the blur")
|
||||
T.check(not fullIds["DRAMATIC_SHAPE:grid"], "and V-GRID")
|
||||
T.check(not fullIds["DRAMATIC_SHAPE:curve"], "and V-CURVE")
|
||||
T.check(not fullIds["DRAMATIC_SHAPE:battles"], "and 3D-BTL")
|
||||
|
||||
-- ------- and off FULL, the rows come back, grouped with the mode
|
||||
--
|
||||
-- The engine splices a pipeline row in beside TILT and lands a mod's own
|
||||
-- additions at the END of the list, which would leave this mode's four rows
|
||||
-- in two places with unrelated rows between them.
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
local grouped = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data },
|
||||
{ { id = "tilt" }, { id = "pipeline:voxel" },
|
||||
{ id = "pipeline:tiltshift" },
|
||||
{ id = "void_fill" } })
|
||||
local order = {}
|
||||
for i, row in ipairs(grouped) do order[row.id] = i end
|
||||
T.check(order["pipeline:tiltshift"] < order["DRAMATIC_SHAPE:grid"],
|
||||
"the mode's settings follow its pipeline rows")
|
||||
T.eq(order["DRAMATIC_SHAPE:battles"] - order["pipeline:tiltshift"], 3,
|
||||
"and sit in one unbroken block, not scattered to the end of the list")
|
||||
T.check(order["void_fill"] > order["DRAMATIC_SHAPE:battles"],
|
||||
"with the engine's own later rows still after them")
|
||||
|
||||
-- ------- the open menu notices when FULL is stepped onto or off
|
||||
--
|
||||
-- OptionsMenu reads its row list every frame but builds it once, so without
|
||||
-- a rebuild the rows FULL owns stay on screen until the menu is reopened --
|
||||
-- and stepping OFF FULL never brings them back.
|
||||
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||
local pressed = {}
|
||||
local menuGame = {
|
||||
data = Data,
|
||||
save = { options = { pipelines = {}, modOptions = {} } },
|
||||
mods = { modOptions = {} },
|
||||
input = { wasPressed = function(_, k) return pressed[k] or false end },
|
||||
stack = { pop = function() end },
|
||||
writeOptions = function() end,
|
||||
}
|
||||
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
local menu = OptionsMenu.new(menuGame)
|
||||
local function rowIndex(m, id)
|
||||
for i, row in ipairs(m.rows) do if row.id == id then return i end end
|
||||
end
|
||||
T.check(rowIndex(menu, "DRAMATIC_SHAPE:grid"),
|
||||
"off FULL the menu opens with the mode's settings on it")
|
||||
|
||||
-- step the VOXEL row from 15 down to FULL, the way the player would
|
||||
menu.index = rowIndex(menu, "pipeline:voxel")
|
||||
pressed = { left = true }
|
||||
menu:update(0)
|
||||
pressed = {}
|
||||
T.eq(Pipelines.level("voxel"), 1, "the step landed on FULL")
|
||||
T.check(not rowIndex(menu, "DRAMATIC_SHAPE:grid"),
|
||||
"and the rows FULL owns left the OPEN menu at once")
|
||||
T.check(not rowIndex(menu, "pipeline:tiltshift"), "T-SHIFT with them")
|
||||
T.check(menu.index <= #menu.rows + 1, "the cursor stayed in range")
|
||||
|
||||
-- and back off it again
|
||||
menu.index = rowIndex(menu, "pipeline:voxel")
|
||||
pressed = { right = true }
|
||||
menu:update(0)
|
||||
pressed = {}
|
||||
T.eq(Pipelines.level("voxel"), 2, "the step left FULL")
|
||||
T.check(rowIndex(menu, "DRAMATIC_SHAPE:grid"),
|
||||
"and the rows came straight back without reopening the menu")
|
||||
T.check(rowIndex(menu, "pipeline:tiltshift"), "T-SHIFT too")
|
||||
|
||||
-- level 2 is the "15" rung: any rung that is not FULL, so the settings the
|
||||
-- preset owns are back on the menu
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
local hookedRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data }, { { id = "text_speed" } })
|
||||
T.eq(#hookedRows, 4, "the options hook added a row per setting")
|
||||
@@ -734,10 +825,33 @@ keyGame = {
|
||||
local VoxelGrid = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelGrid")
|
||||
local Curve = run.loader.exports.DRAMATIC_SHAPE.lib.require("WorldCurve")
|
||||
|
||||
-- ------- 3 walks the ANGLE rungs and steps over FULL
|
||||
--
|
||||
-- The key is a display-mode cycler: it should change the camera and nothing
|
||||
-- else. FULL reaches in and rewrites four other settings, so landing on it
|
||||
-- mid-walk would silently turn the blur to maximum and flatten the horizon
|
||||
-- with nothing on screen saying a keypress had done it.
|
||||
Pipelines.setLevel("voxel", 0)
|
||||
local walk = {}
|
||||
for _ = 1, 6 do
|
||||
Game.keypressed(keyGame, "3")
|
||||
walk[#walk + 1] = Pipelines.levelLabel("voxel")
|
||||
end
|
||||
T.eq(table.concat(walk, ","), "15,35,50,75,OFF,15",
|
||||
"3 walks OFF -> 15 -> 35 -> 50 -> 75 and wraps, never touching FULL")
|
||||
|
||||
-- FULL is 50 degrees, so a press from it goes ON to 75 rather than back to
|
||||
-- the rung that shows the same camera -- the key never appears to do nothing
|
||||
Pipelines.setLevel("voxel", VoxelState.FULL_LEVEL)
|
||||
Game.keypressed(keyGame, "3")
|
||||
T.eq(Pipelines.level("voxel"), 1, "3 cycles the voxel camera ladder")
|
||||
T.eq(Pipelines.levelLabel("voxel"), "75",
|
||||
"a press from FULL goes to 75, since FULL already IS the 50 camera")
|
||||
|
||||
Pipelines.setLevel("voxel", 0)
|
||||
Game.keypressed(keyGame, "3")
|
||||
T.eq(Pipelines.level("voxel"), 2, "and keeps climbing it")
|
||||
T.eq(Pipelines.level("voxel"), 2, "3 cycles the voxel camera ladder")
|
||||
Game.keypressed(keyGame, "3")
|
||||
T.eq(Pipelines.level("voxel"), 3, "and keeps climbing it")
|
||||
|
||||
Game.keypressed(keyGame, "6")
|
||||
T.eq(Pipelines.level("tiltshift"), 1, "6 cycles the tilt-shift blur")
|
||||
@@ -780,7 +894,8 @@ T.eq(GBCFX.level, 0, "and on the live renderer")
|
||||
-- TILT with or without us. Park the ladder on its top rung and turn both
|
||||
-- back on, so the single press under test is the one that wraps to OFF --
|
||||
-- where nothing else is going to clear them.
|
||||
Pipelines.setLevel("voxel", Pipelines.maxLevel("voxel"))
|
||||
-- 5 is the "75" rung, the last one the key walks before it wraps to OFF
|
||||
Pipelines.setLevel("voxel", 5)
|
||||
Tilt.setLevel(3)
|
||||
GBCFX.setLevel(4)
|
||||
keyGame.save.options.tilt = 3
|
||||
@@ -1256,6 +1371,26 @@ T.check(math.abs(sx - px) < 0.2,
|
||||
-- arena floor out from under the two mons pinned to it
|
||||
T.eq(rig.curve, 0, "the battle camera switches the world curve off")
|
||||
|
||||
-- ------- the wireframe is forced on in a battle
|
||||
--
|
||||
-- A fight is a staged shot rather than the world being walked through, so it
|
||||
-- always wears the seams. The player's own V-GRID row must not be touched by
|
||||
-- that -- an override, not a write, or switching the mode off mid-battle
|
||||
-- would quietly rewrite a setting they chose.
|
||||
local Grid = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelGrid")
|
||||
Grid.override = nil
|
||||
local rowWas = Grid.setting:get()
|
||||
T.eq(Grid.enabled(), rowWas and true or false,
|
||||
"with no override the wireframe follows the row")
|
||||
Grid.override = true
|
||||
T.eq(Grid.enabled(), true, "an override forces it on")
|
||||
T.eq(Grid.setting:get(), rowWas, "and leaves the player's row alone")
|
||||
Grid.override = false
|
||||
T.eq(Grid.enabled(), false, "an override can force it off too")
|
||||
Grid.override = nil
|
||||
T.eq(Grid.enabled(), rowWas and true or false,
|
||||
"and clearing it hands the answer back to the row")
|
||||
|
||||
-- ------- the depth of field is measured off the two marks
|
||||
--
|
||||
-- The slab held sharp is the one the mons are standing in, so the band has
|
||||
|
||||
Reference in New Issue
Block a user