mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 15:30:51 +02:00
174 lines
7.6 KiB
Lua
174 lines
7.6 KiB
Lua
-- Dramatic Shape Voxel Mod's own SDK suite: the mod loads clean, both
|
|
-- render pipelines land in the "render_pipelines" registry with the shape
|
|
-- the engine dispatches on, and the whole thing stays inert on a machine
|
|
-- that cannot run the 3D pass (which is exactly what the headless harness
|
|
-- is).
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.modkit")
|
|
local Pipelines = require("src.render.Pipelines")
|
|
|
|
local Data = T.fixtures.load()
|
|
local run = T.sdk.loadMod("mods/DRAMATIC_SHAPE", { data = Data })
|
|
|
|
T.eq(#run.errors, 0,
|
|
"DRAMATIC_SHAPE loads clean: " .. table.concat(run.errors, "; "))
|
|
|
|
-- Game:load does this after the merge; the SDK harness merges into a
|
|
-- fixture dataset instead, so point the dispatcher at that one.
|
|
Pipelines.install(Data)
|
|
|
|
-- ------- the records reached the registry
|
|
|
|
local defs = Data.render_pipelines
|
|
T.check(type(defs) == "table", "the merge created the render_pipelines namespace")
|
|
T.check(type(defs.voxel) == "table", "the voxel pipeline is registered")
|
|
T.check(type(defs.tiltshift) == "table", "the tiltshift pipeline is registered")
|
|
|
|
T.eq(defs.voxel.label, "VOXEL", "voxel carries its options-row label")
|
|
T.eq(defs.tiltshift.label, "T-SHIFT", "tiltshift carries its options-row label")
|
|
T.eq(defs.voxel.hotkey, "6", "voxel claims hotkey 6")
|
|
T.eq(defs.tiltshift.hotkey, "9", "tiltshift claims hotkey 9")
|
|
T.check(type(defs.voxel.drawWorld) == "function",
|
|
"voxel is a world pipeline (drawWorld)")
|
|
T.check(type(defs.tiltshift.worldPresent) == "function",
|
|
"tiltshift is a world post-process (worldPresent)")
|
|
T.check(defs.tiltshift.drawWorld == nil,
|
|
"tiltshift does not claim the world pass")
|
|
|
|
-- provenance: a callback that throws at play time must be attributable to
|
|
-- this mod, not reported as an engine fault
|
|
T.eq(defs._owners and defs._owners.voxel, "DRAMATIC_SHAPE",
|
|
"the merge stamped the pipeline's owning mod")
|
|
|
|
-- ------- the ladders the engine drives
|
|
|
|
T.eq(#defs.voxel.levels, 5, "voxel exposes a five-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")
|
|
|
|
-- ------- gating: inert until switched on, and inert without a GPU
|
|
|
|
T.eq(Pipelines.level("voxel"), 0, "the mode starts switched off")
|
|
T.eq(Pipelines.worldPipeline(), nil,
|
|
"nothing owns the world pass while every pipeline is off")
|
|
|
|
Pipelines.setLevel("voxel", 2)
|
|
T.eq(Pipelines.level("voxel"), 2, "the engine can set the mode's level")
|
|
-- the headless love stub has no depth canvas, so `available` says no and
|
|
-- the engine keeps the vanilla 2D path -- the property that makes the mod
|
|
-- safe to ship enabled
|
|
T.eq(Pipelines.worldPipeline(), nil,
|
|
"an unavailable pipeline never takes the world pass")
|
|
|
|
-- one world pipeline at a time, and never alongside the engine's tilt
|
|
local Tilt = require("src.render.Tilt")
|
|
Tilt.setLevel(3)
|
|
Pipelines.setLevel("voxel", 1)
|
|
T.eq(Tilt.level, 0, "switching a world pipeline on switches TILT off")
|
|
|
|
-- a post-process is not a world pipeline, so it composes with tilt
|
|
Tilt.setLevel(2)
|
|
Pipelines.setLevel("tiltshift", 3)
|
|
T.eq(Tilt.level, 2, "a worldPresent pipeline leaves TILT alone")
|
|
|
|
-- ------- persistence round-trip
|
|
|
|
local opts = { tilt = 0, pipelines = {} }
|
|
Pipelines.syncOptions(opts)
|
|
T.eq(opts.pipelines.voxel, 1, "the level is written back to save.options")
|
|
T.eq(opts.pipelines.tiltshift, 3, "every pipeline's level is written back")
|
|
|
|
Pipelines.reset()
|
|
T.eq(Pipelines.level("voxel"), 0, "reset clears the live levels")
|
|
Pipelines.applyOptions(opts)
|
|
T.eq(Pipelines.level("voxel"), 1, "a restored save restores the mode")
|
|
T.eq(Pipelines.level("tiltshift"), 3, "a restored save restores the blur")
|
|
|
|
-- ------- the options rows the menu splices in
|
|
|
|
local rows = Pipelines.rows({ save = { options = opts } })
|
|
T.eq(#rows, 2, "each pipeline contributes exactly one options row")
|
|
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")
|
|
|
|
-- ------- this mod's own settings
|
|
--
|
|
-- Neither is a pipeline (they parameterise the voxel pass rather than
|
|
-- owning one), so they reach the same menu through the ui.options.rows
|
|
-- hook, and store themselves where the mod manager's settings page looks.
|
|
|
|
local Runtime = require("src.mods.Runtime")
|
|
local hookedRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
|
{ data = Data }, { { id = "text_speed" } })
|
|
T.eq(#hookedRows, 3, "the options hook added a row per setting")
|
|
local grid, curve = hookedRows[2], hookedRows[3]
|
|
T.eq(grid.label, "V-GRID", "the grid row carries its label")
|
|
T.eq(grid.value(), "OFF", "the grid starts off")
|
|
T.eq(curve.label, "V-CURVE", "the curve row carries its label")
|
|
T.eq(curve.value(), "OFF", "the curve starts off")
|
|
|
|
-- stepping writes through to the one place both rows read
|
|
local settingGame = { save = { options = {} }, mods = { modOptions = {} } }
|
|
grid.step(settingGame)
|
|
T.eq(grid.value(), "ON", "stepping the row toggles the grid")
|
|
T.eq(settingGame.save.options.modOptions.DRAMATIC_SHAPE.grid, true,
|
|
"the toggle lands in options.modOptions, where the mod manager reads it")
|
|
T.eq(settingGame.mods.modOptions.DRAMATIC_SHAPE.grid, true,
|
|
"and in the loader's live copy, which mod.options:get reads")
|
|
grid.step(settingGame)
|
|
T.eq(grid.value(), "OFF", "stepping again toggles it back")
|
|
|
|
-- the curve is a four-rung ladder rather than a toggle, and wraps
|
|
curve.step(settingGame, 1)
|
|
T.eq(curve.value(), "1", "stepping the curve climbs its ladder")
|
|
T.eq(settingGame.save.options.modOptions.DRAMATIC_SHAPE.curve, 1,
|
|
"the curve level persists alongside the grid, not over it")
|
|
T.eq(settingGame.save.options.modOptions.DRAMATIC_SHAPE.grid, false,
|
|
"and the grid it shares a bucket with is untouched")
|
|
curve.step(settingGame, 1)
|
|
curve.step(settingGame, 1)
|
|
T.eq(curve.value(), "3", "the ladder reaches its top rung")
|
|
curve.step(settingGame, 1)
|
|
T.eq(curve.value(), "OFF", "and wraps back to OFF")
|
|
curve.step(settingGame, -1)
|
|
T.eq(curve.value(), "3", "stepping down from OFF wraps to the top")
|
|
curve.step(settingGame, 1)
|
|
|
|
-- the strength scales with the view height, so a rung looks the same at
|
|
-- every zoom -- and is exactly zero when the setting is off
|
|
local WorldCurve = run.loader.exports.DRAMATIC_SHAPE.lib.require("WorldCurve")
|
|
T.eq(WorldCurve.k(154), 0, "an OFF curve bends nothing")
|
|
curve.step(settingGame, 1)
|
|
T.check(math.abs(WorldCurve.k(154) - WorldCurve.AMOUNTS[2] / 154) < 1e-9,
|
|
"rung 1's coefficient is its amount over the view height")
|
|
T.check(WorldCurve.AMOUNTS[2] < WorldCurve.AMOUNTS[3]
|
|
and WorldCurve.AMOUNTS[3] < WorldCurve.AMOUNTS[4],
|
|
"the ladder climbs")
|
|
T.check(math.abs(WorldCurve.k(308) * 2 - WorldCurve.k(154)) < 1e-9,
|
|
"halving the zoom halves the coefficient, so the bend looks the same")
|
|
-- the CPU copy Voxel3D.project uses must agree with the shader's quadratic
|
|
local k = WorldCurve.k(154)
|
|
T.eq(WorldCurve.drop(k, 100, 100, 100, 100), 0,
|
|
"nothing drops at the focus, so the ground underfoot stays flat")
|
|
T.check(math.abs(WorldCurve.drop(k, 0, 0, 3, 4) - 25 * k) < 1e-9,
|
|
"the drop is the squared distance times the coefficient")
|
|
T.check(WorldCurve.drop(k, 0, 0, 20, 0) > 4 * WorldCurve.drop(k, 0, 0, 10, 0)
|
|
- 1e-9,
|
|
"and accelerates, so the far edge rolls away faster than the near one")
|
|
curve.step(settingGame, 1)
|
|
curve.step(settingGame, 1)
|
|
curve.step(settingGame, 1)
|
|
T.eq(curve.value(), "OFF", "the curve is left off for the rows below")
|
|
|
|
Pipelines.reset()
|
|
run.release()
|
|
|
|
T.finish("DRAMATIC_SHAPE")
|