mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 09:00:51 +02:00
fix versioning, add sky blue on 75 degree tilt
This commit is contained in:
@@ -8,6 +8,63 @@ long as the thing throwing them is tall.
|
||||
|
||||
### Added
|
||||
|
||||
- A sky at the 75-degree rung. Pitched that far over, the horizon comes
|
||||
into frame and a good part of the picture is void, so the void gets
|
||||
filled instead of reading as the black plate it does at every rung below.
|
||||
No skybox and no geometry -- it is the colour the scene canvas clears to.
|
||||
|
||||
**Outdoor maps only.** A house, a cave or a gym is a room with a ceiling,
|
||||
and the void past its walls is the outside of a box rather than open air.
|
||||
The test is `Map.isOutdoor`, the same one the engine uses for door SFX
|
||||
and the town map, and the same one `Structures` already asks to decide
|
||||
whether a map rings with trees.
|
||||
|
||||
The colour is a four-shade ramp shaped like a world palette and run
|
||||
through `PaletteFX.effectiveColors`, so it answers to the display mode
|
||||
exactly as the baked terrain does: blue in the colour modes, grey under
|
||||
GRAY, green under CLASSIC, dark under GBC INV. A hardcoded blue would sit
|
||||
wrong in every mode that is not a colour mode. It fades across the
|
||||
approach to the top rung rather than switching at the keypress, so it
|
||||
arrives with the camera tween.
|
||||
|
||||
- The mod's four controls sit on adjacent keys, and the two that never had
|
||||
one now have a hotkey at all:
|
||||
|
||||
| key | control | was |
|
||||
| --- | --- | --- |
|
||||
| 3 | VOXEL, the camera ladder | 6 |
|
||||
| 5 | V-GRID, the wireframe | menu only |
|
||||
| 6 | T-SHIFT, the blur ladder | 9 |
|
||||
| 7 | V-CURVE, the horizon bend | menu only |
|
||||
|
||||
Only 6 arrives by the documented route. `Game:keypressed` answers the
|
||||
engine's own display keys first and returns -- 2 COLORS, 3 TILT, 4 ZOOM,
|
||||
5 GBC FX -- and only then offers the key to `Pipelines.hotkey`, expressly
|
||||
so that "a pipeline can never shadow one". Two of the four wanted keys are
|
||||
in that set, and V-GRID and V-CURVE own no render pass, so they have no
|
||||
registry to claim a key from in the first place. The mod wraps
|
||||
`Game:keypressed` to take the four. Polling the keyboard in `update`
|
||||
would not do: it fires alongside the engine's handler rather than instead
|
||||
of it, so 3 would cycle this mode AND the engine's TILT on one press.
|
||||
|
||||
**TILT (3) and GBC FX (5) are no longer reachable by key while this mod
|
||||
is enabled.** Both are still on the OPTIONS menu. Key 9 is now free.
|
||||
|
||||
So the VOXEL key turns both off itself, on every press. Both fight the
|
||||
diorama -- TILT is the flat fake of what this mode does for real, GBC FX
|
||||
a full-screen present pass laid over the top -- and with 3 the only key
|
||||
that now reaches either, it also has to be the way back from having left
|
||||
one on. Every press, not just the one that switches the mode on: the
|
||||
registry's own tilt exclusion covers switching ON, but the press that
|
||||
cycles the ladder round to OFF would otherwise leave both running with
|
||||
no key left to clear them.
|
||||
|
||||
The wrapper delegates rather than reimplements: `Pipelines.hotkey` still
|
||||
applies its own gate and ladder, the settings borrow the same free-roam
|
||||
gate the voxel pipeline uses, and a screen with its own key handler keeps
|
||||
the keyboard -- so typing a nickname cannot cycle a render mode behind
|
||||
the text box.
|
||||
|
||||
- `lib/ShadowMap.lua`: the scene rendered once from the sun into an
|
||||
orthographic depth map, which the main pass then samples per fragment.
|
||||
What the sun cannot see is in shadow, whatever surface it is, so a
|
||||
|
||||
@@ -15,15 +15,37 @@ overworld draws differently.
|
||||
|
||||
| control | does |
|
||||
| --- | --- |
|
||||
| `6`, or the **VOXEL** options row | OFF → 15 → 35 → 50 → 75 → OFF (camera pitch) |
|
||||
| `9`, or the **T-SHIFT** options row | OFF → 1 → 2 → 3 → OFF (miniature blur) |
|
||||
| the **V-GRID** options row | OFF / ON — a one-pixel wireframe on every voxel |
|
||||
| the **V-CURVE** options row | OFF → 1 → 2 → 3 — bend the world over the horizon |
|
||||
| `3`, or the **VOXEL** options row | OFF → 15 → 35 → 50 → 75 → OFF (camera pitch) |
|
||||
| `5`, or the **V-GRID** options row | OFF / ON — a one-pixel wireframe on every voxel |
|
||||
| `6`, or the **T-SHIFT** options row | OFF → 1 → 2 → 3 → OFF (miniature blur) |
|
||||
| `7`, or the **V-CURVE** options row | OFF → 1 → 2 → 3 — bend the world over the horizon |
|
||||
|
||||
The four keys sit together, and two of them are taken off the engine: `3`
|
||||
was **TILT** and `5` was **GBC FX**. Neither is reachable by key while this
|
||||
mod is enabled, and both are still on the OPTIONS menu. Every key is
|
||||
free-roam only, and a screen that handles its own keys keeps them, so
|
||||
nothing here fires while a menu or a text box has focus.
|
||||
|
||||
**`3` also turns TILT and GBC FX off**, on every press — including the one
|
||||
that cycles the ladder back round to OFF. Both fight the diorama: TILT is
|
||||
the flat fake of what this mode does for real, and GBC FX lays a
|
||||
full-screen present pass over the top of it. Since `3` is now the only key
|
||||
that reaches either, it is also the way back from having left one on.
|
||||
|
||||
75 degrees is a rung the engine's flat **TILT** could not have: it projects
|
||||
the world canvas as one rigid plane, which degenerates into a horizon line
|
||||
down there, where real geometry only gets more of itself to show.
|
||||
|
||||
That rung is also the one that gets a **sky**. Pitched that far over the
|
||||
horizon comes into frame, so the void behind the diorama is filled rather
|
||||
than left as the black plate it reads as at every rung below — enough to
|
||||
stand the world under something without a skybox to draw. Outdoors only: a
|
||||
house, a cave or a gym is a room with a ceiling, and the void past its
|
||||
walls is the outside of a box, not open air. The colour follows the display
|
||||
mode like everything else here — blue in the colour modes, grey under
|
||||
GRAY, green under CLASSIC — and fades in with the camera tween rather than
|
||||
snapping on at the keypress.
|
||||
|
||||
V-CURVE is the *curved world* — every vertex pushed down by the square of
|
||||
its horizontal distance from the camera's focus, so the ground under you
|
||||
stays flat and the map bends away over a near horizon. It is not a fisheye:
|
||||
@@ -45,9 +67,8 @@ vanilla path.
|
||||
|
||||
This is a *rendering pipeline* mod (see `docs/modding.md`). `main.lua`
|
||||
registers two records in the engine's `render_pipelines` registry and
|
||||
writes almost nothing else — the ladder, the options rows, the hotkeys,
|
||||
persistence and the free-roam gate are all engine plumbing driven from
|
||||
those records:
|
||||
writes almost nothing else — the ladder, the options rows, persistence and
|
||||
the free-roam gate are all engine plumbing driven from those records:
|
||||
|
||||
- **`voxel`** — a `drawWorld` pipeline. Replaces the world pass with a
|
||||
rendered 3D scene and returns it as one window-resolution canvas.
|
||||
@@ -93,9 +114,22 @@ voxel and shell counts are what `lib/Buildings.lua` is checked against.
|
||||
|
||||
## Engine seams it relies on
|
||||
|
||||
The mod reads a few engine internals (hence `"permissions":
|
||||
["engine_internals"]`), all of them deliberate read-only seams:
|
||||
The mod reaches into a few engine internals (hence `"permissions":
|
||||
["engine_internals"]`). All but the last are read-only:
|
||||
|
||||
- `Game:keypressed` — **wrapped, not read.** The only thing here that
|
||||
changes engine behaviour, and the one seam worth arguing about. The
|
||||
engine answers its own display keys and returns before a mod's pipeline
|
||||
is offered the key, expressly so a pipeline cannot shadow one — so `3`
|
||||
(TILT) and `5` (GBC FX) cannot be claimed the documented way, and V-GRID
|
||||
and V-CURVE own no render pass and have no registry to claim from at all.
|
||||
Only `6` arrives by the supported route. Polling the keyboard instead
|
||||
would fire *alongside* the engine's handler rather than instead of it, so
|
||||
`3` would cycle both this mode and TILT on one press. The wrapper
|
||||
delegates everything it can: `Pipelines.hotkey` still applies its own
|
||||
gate and ladder, the settings borrow the voxel pipeline's free-roam gate,
|
||||
unclaimed keys pass straight through, and a screen with its own handler
|
||||
keeps the keyboard.
|
||||
- `Player:pose()` / `NPC:pose()` — the sheet, position, facing and step
|
||||
phase a frame renders to, so 2D and 3D can never disagree about a pose.
|
||||
- `SpriteRenderer:resolveImage()` and `SpriteRenderer.STAND/WALK` — the
|
||||
|
||||
+9
-2
@@ -338,7 +338,10 @@ end
|
||||
-- Begin the 3D pass into a `w` x `h` pixel canvas centred on world
|
||||
-- (cx, cy), covering `vw` x `vh` world pixels. Returns false when the pass
|
||||
-- could not start, in which case the caller must not call endScene.
|
||||
function Voxel3D.beginScene(w, h, cx, cy, vw, vh)
|
||||
-- `sky` is an optional {r, g, b, a} in 0..1 to clear the void to, for the
|
||||
-- pitch where the horizon is in frame (VoxelScene.skyFor). nil leaves the
|
||||
-- void transparent, which is what every rung below it wants.
|
||||
function Voxel3D.beginScene(w, h, cx, cy, vw, vh, sky)
|
||||
-- the wireframe variant when the player has it on AND it built; either
|
||||
-- answer falls through to the plain scene rather than to no scene
|
||||
local grid = VoxelGrid.enabled()
|
||||
@@ -361,7 +364,11 @@ function Voxel3D.beginScene(w, h, cx, cy, vw, vh)
|
||||
pcall(love.graphics.setCanvas)
|
||||
return false
|
||||
end
|
||||
love.graphics.clear(0, 0, 0, 0, true, true)
|
||||
if sky then
|
||||
love.graphics.clear(sky[1], sky[2], sky[3], sky[4] or 1, true, true)
|
||||
else
|
||||
love.graphics.clear(0, 0, 0, 0, true, true)
|
||||
end
|
||||
love.graphics.setDepthMode("lequal", true)
|
||||
-- models mirror on X for right-facing and alternate walk steps, which
|
||||
-- flips winding; hidden faces are already culled at build time, so there
|
||||
|
||||
+54
-1
@@ -20,7 +20,9 @@ local VoxelModels = V.require("VoxelModels")
|
||||
local SpriteBillboards = V.require("SpriteBillboards")
|
||||
local TileShape = V.require("TileShape")
|
||||
local TerrainAtlas = V.require("TerrainAtlas")
|
||||
local Voxel = V.require("VoxelState")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local Map = require("src.world.Map")
|
||||
|
||||
local VoxelScene = {}
|
||||
|
||||
@@ -46,6 +48,55 @@ end
|
||||
|
||||
VoxelScene._modeColors = modeColors -- named for the suite
|
||||
|
||||
-- ------------------------------------------------------------------ sky --
|
||||
--
|
||||
-- At the top rung the camera is pitched far enough over that the horizon
|
||||
-- comes into frame and a good part of the picture is void -- so the void
|
||||
-- becomes the sky, and the diorama reads as standing under something
|
||||
-- rather than floating on a black plate. Below that rung the camera looks
|
||||
-- down steeply enough that the horizon is off-screen, and painting the
|
||||
-- void only tints the gaps between meshes, so it stays transparent.
|
||||
--
|
||||
-- INDOORS THERE IS NO SKY. A house, a cave or a gym is a room with a
|
||||
-- ceiling, and the void past its walls is the outside of a box, not open
|
||||
-- air. Map.isOutdoor is the same test the engine uses for door SFX and the
|
||||
-- town map, and the same one Structures already asks to decide whether a
|
||||
-- map rings with trees.
|
||||
--
|
||||
-- The colour is a four-shade ramp shaped like a world palette so the
|
||||
-- display mode can transform it exactly like one: GRAY gets a grey sky,
|
||||
-- CLASSIC a green one, GBC INV a dark one, and the colour modes the blue.
|
||||
-- A hardcoded blue would sit wrong in every non-colour mode -- the same
|
||||
-- mismatch the terrain bake had.
|
||||
local SKY_SHADES = { { 222, 242, 255 }, { 135, 196, 240 },
|
||||
{ 64, 120, 192 }, { 16, 40, 80 } }
|
||||
local SKY_SHADE = 2 -- the ramp's "sky" proper; 1 is its highlight
|
||||
|
||||
-- fade across the approach to the top rung, so the sky arrives with the
|
||||
-- camera tween instead of popping in on the keypress
|
||||
local function skyStrength(angleRad)
|
||||
local deg = math.deg(angleRad or 0)
|
||||
local from = Voxel.ANGLES_DEG[Voxel.MAX_LEVEL] or 50 -- the rung below
|
||||
local to = Voxel.ANGLES_DEG[Voxel.MAX_LEVEL + 1] or 75 -- the top rung
|
||||
if to <= from then return deg >= to and 1 or 0 end
|
||||
local t = (deg - from) / (to - from)
|
||||
if t < 0 then return 0 end
|
||||
if t > 1 then return 1 end
|
||||
return t
|
||||
end
|
||||
|
||||
local function skyFor(map)
|
||||
if not (map and map.def and Map.isOutdoor(map.def)) then return nil end
|
||||
local t = skyStrength(Voxel.angle)
|
||||
if t <= 0 then return nil end
|
||||
local shades = PaletteFX.effectiveColors(SKY_SHADES) or SKY_SHADES
|
||||
local c = shades[SKY_SHADE] or SKY_SHADES[SKY_SHADE]
|
||||
return { c[1] / 255, c[2] / 255, c[3] / 255, t }
|
||||
end
|
||||
|
||||
VoxelScene._skyFor = skyFor -- named for the suite
|
||||
VoxelScene._skyStrength = skyStrength
|
||||
|
||||
-- A facing is a yaw about +Y. Models are carved facing +Z (south, "down"),
|
||||
-- and Mat4.rotateY(+90 deg) maps +Z to +X, so east is +90 and west is -90.
|
||||
-- Nothing is mirrored: yawing to face east shows the model's east side,
|
||||
@@ -403,7 +454,9 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor)
|
||||
local posed = posesOf(state, spriteColors)
|
||||
castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh, atlasFor)
|
||||
|
||||
if not Voxel3D.beginScene(w, h, cx, cy, vw, vh) then return nil end
|
||||
if not Voxel3D.beginScene(w, h, cx, cy, vw, vh, skyFor(state.map)) then
|
||||
return nil
|
||||
end
|
||||
|
||||
Voxel3D.draw(terrain, atlasFor(state.map), nil)
|
||||
for i, nb in ipairs(state.neighbors or {}) do
|
||||
|
||||
@@ -92,8 +92,9 @@ end
|
||||
mod.content.render_pipelines:register("voxel", {
|
||||
label = "VOXEL",
|
||||
levels = Voxel.ANGLE_LABELS,
|
||||
-- 2/3/4/5 are the engine's own display hotkeys
|
||||
hotkey = "6",
|
||||
-- 3 is the engine's TILT key, which this mode supersedes -- see the
|
||||
-- hotkey block near the bottom of this file for how it is claimed
|
||||
hotkey = "3",
|
||||
-- above tiltshift, so the two sort together in the options list with the
|
||||
-- mode first and its post-process under it
|
||||
priority = 20,
|
||||
@@ -163,7 +164,9 @@ mod.content.render_pipelines:register("voxel", {
|
||||
mod.content.render_pipelines:register("tiltshift", {
|
||||
label = "T-SHIFT",
|
||||
levels = TiltShift.LABELS,
|
||||
hotkey = "9",
|
||||
-- 6 is free: no engine branch claims it, so this one alone reaches the
|
||||
-- registry by the documented route
|
||||
hotkey = "6",
|
||||
priority = 10,
|
||||
|
||||
update = function(dt, level)
|
||||
@@ -202,6 +205,88 @@ for i, entry in ipairs(SETTINGS) do
|
||||
end
|
||||
mod.options:define(schema)
|
||||
|
||||
-- ------- this mod's hotkeys
|
||||
--
|
||||
-- 3 VOXEL cycle the camera ladder (was 6)
|
||||
-- 5 V-GRID toggle the wireframe (new)
|
||||
-- 6 T-SHIFT cycle the blur ladder (was 9)
|
||||
-- 7 V-CURVE cycle the horizon bend (new)
|
||||
--
|
||||
-- Only 6 arrives by the documented route. Game:keypressed answers the
|
||||
-- engine's own display keys FIRST and returns -- 2 COLORS, 3 TILT, 4 ZOOM,
|
||||
-- 5 GBC FX -- and only then offers the key to Pipelines.hotkey, expressly
|
||||
-- so "a pipeline can never shadow one" (Schemas, render_pipelines.hotkey).
|
||||
-- 3 and 5 are two of those, and 7 belongs to a pair of plain mod settings
|
||||
-- that own no pass and so have no registry to claim a key from at all.
|
||||
--
|
||||
-- So this wraps Game:keypressed. It is the invasive option and it is the
|
||||
-- only one: polling the keyboard in update() would fire alongside the
|
||||
-- engine's handler rather than instead of it, so 3 would cycle this mode
|
||||
-- AND the engine's TILT on the same press.
|
||||
--
|
||||
-- Consequences worth being explicit about: while this mod is enabled, TILT
|
||||
-- (3) and GBC FX (5) are unreachable by key. Both are still reachable on
|
||||
-- the OPTIONS menu, and TILT is the one this mode supersedes anyway -- the
|
||||
-- registry already forces it off whenever a world pipeline takes the pass.
|
||||
--
|
||||
-- Everything the engine does around a pipeline hotkey has to happen here
|
||||
-- too, so the work is DELEGATED rather than reimplemented: Pipelines.hotkey
|
||||
-- applies its own gate and ladder, and the three lines after it are the
|
||||
-- engine's own (syncOptions, the tilt exclusion, writeOptions).
|
||||
|
||||
local HOTKEYS = {
|
||||
["3"] = "pipeline", -- voxel, by its declared hotkey
|
||||
["6"] = "pipeline", -- tiltshift, likewise
|
||||
["5"] = VoxelGrid.setting,
|
||||
["7"] = WorldCurve.setting,
|
||||
}
|
||||
|
||||
do
|
||||
local Game = require("src.core.Game")
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
local inner = Game.keypressed
|
||||
|
||||
function Game:keypressed(key)
|
||||
local claim = HOTKEYS[key]
|
||||
local top = self.stack and self.stack:top()
|
||||
-- A screen with its own key handler gets the key first, exactly as the
|
||||
-- engine's first branch does: typing a nickname must not toggle a
|
||||
-- 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
|
||||
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.
|
||||
-- A player who left either running before enabling the mod would
|
||||
-- otherwise have no way back to off, and both fight the diorama:
|
||||
-- TILT is the flat fake of what this mode does for real, and GBC
|
||||
-- FX is a full-screen present pass over the top of it. So the
|
||||
-- VOXEL key clears them on EVERY press, not just the press that
|
||||
-- switches the mode on -- cycling back round to OFF leaves them
|
||||
-- off too, which is the state the key is now the only route to.
|
||||
if key == "3" then
|
||||
self.save.options.tilt = 0
|
||||
self.save.options.gbcfx = 0
|
||||
require("src.render.GBCFX").setLevel(0)
|
||||
end
|
||||
require("src.render.Tilt").setLevel(self.save.options.tilt or 0)
|
||||
self:writeOptions()
|
||||
return
|
||||
end
|
||||
elseif Pipelines.canToggle("voxel", top, self.overworld) then
|
||||
-- Both settings parameterise the voxel pass, so they answer to the
|
||||
-- same free-roam gate it does -- borrowed from the registry rather
|
||||
-- than restated, so a press mid-warp or mid-cutscene is refused for
|
||||
-- the wireframe exactly when it would be for the mode itself.
|
||||
claim:cycle(self)
|
||||
return
|
||||
end
|
||||
end
|
||||
return inner(self, key)
|
||||
end
|
||||
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)
|
||||
|
||||
+2
-2
@@ -6,12 +6,12 @@
|
||||
"entry": "main.lua",
|
||||
"profile": "content",
|
||||
"category": "GRAPHICS",
|
||||
"game_version": "<2.0.0",
|
||||
"game_version": "0.0.0-dev || >=0.1.28 <2.0.0",
|
||||
"priority": 100,
|
||||
"dependencies": [],
|
||||
"optional_dependencies": [],
|
||||
"conflicts": [],
|
||||
"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. Registers two render pipelines (hotkeys 6 and 9); presentational only."
|
||||
"description": "A full 3D diorama overworld: extruded terrain, depth-buffered occlusion, voxel characters and a tilt-shift miniature pass. Registers two render pipelines and claims hotkeys 3, 5, 6 and 7 -- 3 and 5 displace the engine's TILT and GBC FX keys, both still reachable on the OPTIONS menu. Presentational only."
|
||||
}
|
||||
|
||||
@@ -10,10 +10,14 @@ return {
|
||||
"with VOXEL on, the overworld draws as 3D geometry instead of flat tiles",
|
||||
"occlusion comes from a depth buffer rather than a y-sort, so buildings really hide what is behind them",
|
||||
"VOXEL and the engine's TILT are mutually exclusive -- turning one on switches the other off",
|
||||
"hotkeys 3 and 5 are taken over from the engine's TILT and GBC FX; both remain on the OPTIONS menu",
|
||||
"the VOXEL key (3) turns TILT and GBC FX off on every press -- both fight the diorama, and 3 is now the only key that reaches either",
|
||||
},
|
||||
added = {
|
||||
"VOXEL options row and hotkey 6 (OFF / 15 / 35 / 50 degrees)",
|
||||
"T-SHIFT options row and hotkey 9 (OFF / 1 / 2 / 3), the miniature blur",
|
||||
"VOXEL options row and hotkey 3 (OFF / 15 / 35 / 50 degrees)",
|
||||
"T-SHIFT options row and hotkey 6 (OFF / 1 / 2 / 3), the miniature blur",
|
||||
"V-GRID on hotkey 5 and V-CURVE on hotkey 7",
|
||||
"a sky behind the diorama at the 75-degree rung, outdoor maps only, coloured by the active palette mode",
|
||||
"carved voxel models for 67 overworld sprites (assets/voxels/)",
|
||||
"a hand-authored tile shape profile (data/voxel_heights.lua) a mod can extend",
|
||||
},
|
||||
|
||||
@@ -28,8 +28,8 @@ 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.eq(defs.voxel.hotkey, "3", "voxel claims hotkey 3")
|
||||
T.eq(defs.tiltshift.hotkey, "6", "tiltshift claims hotkey 6")
|
||||
T.check(type(defs.voxel.drawWorld) == "function",
|
||||
"voxel is a world pipeline (drawWorld)")
|
||||
T.check(type(defs.tiltshift.worldPresent) == "function",
|
||||
@@ -501,6 +501,208 @@ T.eq(modeColors(function() return nil end), nil,
|
||||
"a map with no world palette bakes no colour, as on the flat path")
|
||||
T.eq(modeColors(nil), nil, "and a pipeline given no paletteFor at all is safe")
|
||||
|
||||
-- ------- the hotkeys this mod claims
|
||||
--
|
||||
-- 3 VOXEL cycle the camera ladder
|
||||
-- 5 V-GRID toggle the wireframe
|
||||
-- 6 T-SHIFT cycle the blur ladder
|
||||
-- 7 V-CURVE cycle the horizon bend
|
||||
--
|
||||
-- Only 6 reaches the pipeline registry the documented way. Game:keypressed
|
||||
-- answers the engine's own display keys first and returns -- 3 is TILT and
|
||||
-- 5 is GBC FX -- expressly so a pipeline cannot shadow one, and 7 belongs
|
||||
-- to settings that own no pass and so have no registry to claim from. The
|
||||
-- mod therefore wraps Game:keypressed, and these pin what that wrapper is
|
||||
-- allowed to take.
|
||||
|
||||
local Game = require("src.core.Game")
|
||||
Pipelines.reset()
|
||||
Pipelines.setLevel("voxel", 0)
|
||||
Pipelines.setLevel("tiltshift", 0)
|
||||
|
||||
-- a free-roam game: Zoom.gateOK wants the top screen to BE the overworld,
|
||||
-- not transitioning and not running a script
|
||||
local keyGame
|
||||
local overworld = { transitioning = false }
|
||||
keyGame = {
|
||||
overworld = overworld,
|
||||
stack = { top = function() return overworld end },
|
||||
save = { options = { modOptions = {} } },
|
||||
mods = { modOptions = {} },
|
||||
writeOptions = function() end,
|
||||
}
|
||||
|
||||
local VoxelGrid = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelGrid")
|
||||
local Curve = run.loader.exports.DRAMATIC_SHAPE.lib.require("WorldCurve")
|
||||
|
||||
Game.keypressed(keyGame, "3")
|
||||
T.eq(Pipelines.level("voxel"), 1, "3 cycles the voxel camera ladder")
|
||||
Game.keypressed(keyGame, "3")
|
||||
T.eq(Pipelines.level("voxel"), 2, "and keeps climbing it")
|
||||
|
||||
Game.keypressed(keyGame, "6")
|
||||
T.eq(Pipelines.level("tiltshift"), 1, "6 cycles the tilt-shift blur")
|
||||
|
||||
Game.keypressed(keyGame, "5")
|
||||
T.eq(VoxelGrid.setting:get(), true, "5 toggles V-GRID on")
|
||||
Game.keypressed(keyGame, "5")
|
||||
T.eq(VoxelGrid.setting:get(), false, "and off again")
|
||||
|
||||
local curveBefore = Curve.setting:get()
|
||||
Game.keypressed(keyGame, "7")
|
||||
T.neq(Curve.setting:get(), curveBefore, "7 cycles V-CURVE")
|
||||
|
||||
-- 3 also clears the two engine modes it displaced. Without this a player
|
||||
-- who left TILT or GBC FX on before enabling the mod has no key left to
|
||||
-- turn them off with, and both fight the diorama -- TILT is the flat fake
|
||||
-- of what this mode does for real, GBC FX a present pass over the top.
|
||||
local GBCFX = require("src.render.GBCFX")
|
||||
Tilt.setLevel(2)
|
||||
GBCFX.setLevel(3)
|
||||
keyGame.save.options.tilt = 2
|
||||
keyGame.save.options.gbcfx = 3
|
||||
|
||||
Game.keypressed(keyGame, "3")
|
||||
T.eq(keyGame.save.options.tilt, 0, "3 turns TILT off in the save")
|
||||
T.eq(Tilt.level, 0, "and on the live renderer")
|
||||
T.eq(keyGame.save.options.gbcfx, 0, "3 turns GBC FX off in the save")
|
||||
T.eq(GBCFX.level, 0, "and on the live renderer")
|
||||
|
||||
-- Every press, not just the one that switches the mode on. This is the
|
||||
-- half the registry does NOT cover: its tilt exclusion fires when a world
|
||||
-- pipeline takes the pass, so a press that switches voxel ON would clear
|
||||
-- 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"))
|
||||
Tilt.setLevel(3)
|
||||
GBCFX.setLevel(4)
|
||||
keyGame.save.options.tilt = 3
|
||||
keyGame.save.options.gbcfx = 4
|
||||
|
||||
Game.keypressed(keyGame, "3")
|
||||
T.eq(Pipelines.level("voxel"), 0, "the press wraps the ladder back to OFF")
|
||||
T.eq(keyGame.save.options.tilt, 0, "the press that wraps to OFF still clears TILT")
|
||||
T.eq(Tilt.level, 0, "with the renderer agreeing")
|
||||
T.eq(keyGame.save.options.gbcfx, 0, "and still clears GBC FX")
|
||||
T.eq(GBCFX.level, 0, "with the renderer agreeing there too")
|
||||
|
||||
-- but 6 must not: T-SHIFT is a post-process that composes with TILT, and
|
||||
-- the registry deliberately leaves it alone
|
||||
Tilt.setLevel(2)
|
||||
keyGame.save.options.tilt = 2
|
||||
Game.keypressed(keyGame, "6")
|
||||
T.eq(keyGame.save.options.tilt, 2, "6 leaves TILT alone -- the blur composes with it")
|
||||
Tilt.setLevel(0)
|
||||
keyGame.save.options.tilt = 0
|
||||
|
||||
-- the engine's own keys the mod did NOT claim must still reach it: 4 is
|
||||
-- ZOOM, and taking it would be a bug rather than a feature
|
||||
local zoomKeyReached = false
|
||||
local realZoomGate = require("src.render.Zoom").gateOK
|
||||
require("src.render.Zoom").gateOK = function() zoomKeyReached = true; return false end
|
||||
Game.keypressed(keyGame, "4")
|
||||
require("src.render.Zoom").gateOK = realZoomGate
|
||||
T.check(zoomKeyReached, "a key this mod does not claim still reaches the engine")
|
||||
|
||||
-- A screen with its own key handler owns the keyboard: typing a nickname
|
||||
-- must not cycle a render mode behind the text box.
|
||||
local gridBefore = VoxelGrid.setting:get()
|
||||
local voxelBefore = Pipelines.level("voxel")
|
||||
local typed = {}
|
||||
local menu = { onKeyPressed = function(_, k) typed[#typed + 1] = k end }
|
||||
keyGame.stack.top = function() return menu end
|
||||
for _, k in ipairs({ "3", "5", "6", "7" }) do Game.keypressed(keyGame, k) end
|
||||
T.eq(#typed, 4, "every claimed key goes to a screen that handles keys itself")
|
||||
T.eq(VoxelGrid.setting:get(), gridBefore, "V-GRID is untouched while a screen has focus")
|
||||
T.eq(Pipelines.level("voxel"), voxelBefore, "and so is the voxel ladder")
|
||||
|
||||
-- and the free-roam gate the engine applies to its own display keys applies
|
||||
-- to the settings too: no flipping the wireframe mid-cutscene
|
||||
keyGame.stack.top = function() return overworld end
|
||||
overworld.transitioning = true
|
||||
local midWarp = VoxelGrid.setting:get()
|
||||
Game.keypressed(keyGame, "5")
|
||||
T.eq(VoxelGrid.setting:get(), midWarp, "V-GRID refuses mid-transition, as the mode does")
|
||||
overworld.transitioning = false
|
||||
|
||||
-- ------- the sky at the top rung
|
||||
--
|
||||
-- At 75 degrees the camera is pitched far enough over that the horizon is
|
||||
-- in frame, so the void behind the diorama becomes a sky rather than the
|
||||
-- black plate it reads as at every rung below. Outdoors only: a house or a
|
||||
-- cave is a room with a ceiling, and the void past its walls is the
|
||||
-- outside of a box, not open air.
|
||||
|
||||
local Voxel = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelState")
|
||||
local skyFor = VoxelScene._skyFor
|
||||
local skyStrength = VoxelScene._skyStrength
|
||||
T.check(type(skyFor) == "function", "the scene exposes its sky resolve")
|
||||
|
||||
local outside = { def = { id = "PALLET_TOWN", tileset = "OVERWORLD" } }
|
||||
local inside = { def = { id = "REDS_HOUSE_1F", tileset = "HOUSE" } }
|
||||
local TOP = math.rad(Voxel.ANGLES_DEG[Voxel.MAX_LEVEL + 1])
|
||||
|
||||
-- the ladder, by angle: only the top rung paints anything
|
||||
for level = 0, Voxel.MAX_LEVEL do
|
||||
Voxel.angle = math.rad(Voxel.ANGLES_DEG[level + 1])
|
||||
local sky = skyFor(outside)
|
||||
if level == Voxel.MAX_LEVEL then
|
||||
T.check(sky ~= nil, "the 75-degree rung paints a sky outdoors")
|
||||
T.eq(sky[4], 1, "and paints it at full strength")
|
||||
else
|
||||
T.eq(sky, nil, "rung " .. level .. " leaves the void alone")
|
||||
end
|
||||
end
|
||||
|
||||
-- indoors, never -- at any rung, including the top one
|
||||
for level = 0, Voxel.MAX_LEVEL do
|
||||
Voxel.angle = math.rad(Voxel.ANGLES_DEG[level + 1])
|
||||
T.eq(skyFor(inside), nil, "an interior has no sky at rung " .. level)
|
||||
end
|
||||
Voxel.angle = TOP
|
||||
T.eq(skyFor(inside), nil, "not even at 75 degrees, where outdoors would")
|
||||
|
||||
-- a map record with nothing to ask is not a crash
|
||||
T.eq(skyFor(nil), nil, "no map, no sky")
|
||||
T.eq(skyFor({}), nil, "a map with no def is not an outdoor map")
|
||||
|
||||
-- it fades in with the camera tween rather than popping on the keypress
|
||||
T.eq(skyStrength(math.rad(50)), 0, "the rung below the top is still skyless")
|
||||
T.eq(skyStrength(TOP), 1, "the top rung is full sky")
|
||||
local mid = skyStrength(math.rad(62.5))
|
||||
T.check(mid > 0 and mid < 1, "and the tween between them is partial")
|
||||
T.check(skyStrength(math.rad(70)) > skyStrength(math.rad(60)),
|
||||
"strengthening as the camera pitches over")
|
||||
T.eq(skyStrength(math.rad(15)), 0, "a shallow pitch paints nothing at all")
|
||||
|
||||
-- the colour answers to the display mode, exactly as the terrain does: a
|
||||
-- hardcoded blue would sit wrong in the modes that are not colour modes
|
||||
Voxel.angle = TOP
|
||||
local function skyRGB(mode)
|
||||
local prev = PaletteFX.mode
|
||||
PaletteFX.mode = mode
|
||||
local c = skyFor(outside)
|
||||
PaletteFX.mode = prev
|
||||
return c
|
||||
end
|
||||
|
||||
local blue = skyRGB("gbc")
|
||||
T.check(blue[3] > blue[1], "in a colour mode the sky is blue -- more blue than red")
|
||||
|
||||
local grey = skyRGB("og")
|
||||
T.check(math.abs(grey[1] - grey[2]) < 1e-6 and math.abs(grey[2] - grey[3]) < 1e-6,
|
||||
"GRAY paints a grey sky, not a blue one")
|
||||
|
||||
local green = skyRGB("classic")
|
||||
T.check(green[2] > green[1] and green[2] > green[3],
|
||||
"CLASSIC paints a green sky, matching its green world")
|
||||
|
||||
T.check(skyRGB("gbc_inv")[3] ~= blue[3],
|
||||
"GBC INV does not paint the same sky as GBC")
|
||||
|
||||
Voxel.angle = 0
|
||||
|
||||
Pipelines.reset()
|
||||
run.release()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user