fix versioning, add sky blue on 75 degree tilt

This commit is contained in:
DramaticShape
2026-07-26 17:47:26 -04:00
parent ccc0105f3f
commit 7046ead648
8 changed files with 463 additions and 21 deletions
+57
View File
@@ -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
+43 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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
+88 -3
View File
@@ -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
View File
@@ -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."
}
+6 -2
View File
@@ -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",
},
+204 -2
View File
@@ -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()