mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 10:00:50 +02:00
+29
-19
@@ -57,13 +57,16 @@
|
||||
horizon above the frame, so the arena keeps exactly the flat sky it had.
|
||||
|
||||
- **A day/night cycle**, on a new **DAYTIME** options row: `DAY`, `NIGHT`,
|
||||
`DUSK`, `DAWN`, `CYCLE`. One twenty-minute clock underneath all five -- ten
|
||||
minutes of sun, ten of moon -- where the four named settings are PINS on
|
||||
that dial (noon, mid-night, sunset, sunrise) and `CYCLE` lets it run,
|
||||
picking up from whichever pin the player was just looking at. Everything is
|
||||
a pure function of the clock, so the pinned DUSK is exactly the running
|
||||
cycle stopped at sunset. Setting **VOXEL** to `FULL` switches DAYTIME to
|
||||
`CYCLE` along with the rest of the preset.
|
||||
`DUSK`, `DAWN`, `SYNC`, `CYCLE`. One twenty-minute clock underneath all of
|
||||
them -- ten minutes of sun, ten of moon -- where the four named settings
|
||||
are PINS on that dial (noon, mid-night, sunset, sunrise), `CYCLE` lets it
|
||||
run, picking up from whichever pin or SYNC sky the player was just looking
|
||||
at, and `SYNC` -- the DEFAULT -- lays the machine's own clock onto the
|
||||
dial: local noon is the DAY pin, midnight is NIGHT, six and eighteen the
|
||||
twilights, an hour of the real day is fifty seconds of dial. Everything is a pure function of
|
||||
the clock, so the pinned DUSK is exactly the running cycle stopped at
|
||||
sunset. Setting **VOXEL** to `FULL` switches DAYTIME to `CYCLE` along with
|
||||
the rest of the preset.
|
||||
|
||||
**The sun and the moon are in the sky**, and their positions are honest:
|
||||
the disc is the light's own direction projected through the same matrix the
|
||||
@@ -130,14 +133,18 @@
|
||||
at any angle, in free-roam and staged battles alike, with no geometry
|
||||
work.
|
||||
|
||||
By day a thin diagonal glint crosses the panes WHILE THE VIEW MOVES: the
|
||||
sweep's phase is fed by the camera's own travel and its strength fades out
|
||||
within a beat of standing still -- a reflection is something the viewpoint
|
||||
does, so still camera means still glass. It lifts the texels toward
|
||||
sky-white and leaves the diagonal shine art visible through it. The mask
|
||||
is consulted only by meshes textured from the tileset atlas
|
||||
(Voxel3D.glass), never by sprite sheets, whose coordinates would land on
|
||||
the panes' atlas positions by accident.
|
||||
By day a thin glint crosses the panes WHILE THE VIEW MOVES: the sweep's
|
||||
phase is fed by the camera's own travel and its strength fades out within
|
||||
a beat of standing still -- a reflection is something the viewpoint does,
|
||||
so still camera means still glass. The sweep pattern lives in the pane's
|
||||
OWN texels, not the screen's: a screen-anchored pattern has the world
|
||||
sliding through it at zoom speed whenever the camera pans, which strobed
|
||||
(worst walking against the sweep); anchored to the glass, panning moves
|
||||
nothing and a step advances the glint a fraction of a texel, the same in
|
||||
every direction. It lifts the texels toward sky-white and leaves the
|
||||
shine art visible through it. The mask is consulted only by meshes
|
||||
textured from the tileset atlas (Voxel3D.glass), never by sprite sheets,
|
||||
whose coordinates would land on the panes' atlas positions by accident.
|
||||
After dark the panes are LIT: the texel's own pattern carried into a warm
|
||||
lamp colour, replacing the shaded answer entirely -- a lit window ignores
|
||||
the sun, every shadow and the hour's tint, exactly as a window with a lamp
|
||||
@@ -202,10 +209,13 @@
|
||||
own ray (`ShadowMap.snug`): moving along the ray changes nothing about
|
||||
where a shadow falls, but storing the card shallower takes three quarters
|
||||
of the forgiveness back for the shadow it throws -- and for nothing else:
|
||||
no terrain moved, so the acne margin is untouched where it matters, and
|
||||
the quarter left keeps the card off the float-equality knife edge against
|
||||
its own stored depth. The shadow root lands back under the feet at every
|
||||
hour.
|
||||
no terrain moved, so the acne margin is untouched where it matters. The
|
||||
obligation that comes with it: every snugged caster's LIT draw hands the
|
||||
same snugged transform to its own shadow lookup (Voxel3D.draw's
|
||||
`sunModel`), so stored and lookup agree exactly and the compare keeps its
|
||||
full margin -- read un-snugged, the missing nine tenths showed up as
|
||||
diagonal moire bands crawling across every sprite. The shadow root lands
|
||||
back under the feet at every hour.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
+7
-3
@@ -416,8 +416,10 @@ function BattleScene.render(state, arena, textures, token)
|
||||
-- tileset atlas, so the mask's coordinates mean nothing on them
|
||||
Voxel3D.glass(false)
|
||||
for _, card in ipairs(monCards(arena, groundY, textures)) do
|
||||
-- the sun stored this card snugged (castShadows), so its own shadow
|
||||
-- lookup must read the same snugged transform -- see ShadowMap.snug
|
||||
Voxel3D.draw(BattleBillboard.mesh(), card.tex, card.model,
|
||||
BattleBillboard.PULL)
|
||||
BattleBillboard.PULL, ShadowMap.snug(card.model))
|
||||
end
|
||||
Voxel3D.glass(true)
|
||||
Voxel3D.seams(true)
|
||||
@@ -433,10 +435,12 @@ function BattleScene.render(state, arena, textures, token)
|
||||
Mat4.translate(nb.ox, 0, nb.oy), pull)
|
||||
end
|
||||
local fpull = math.max(0, pull - 8 * math.sin(math.max(pitch, 0.05)))
|
||||
Voxel3D.draw(ChunkMesher.flowers(host), atlasFor(host), nil, fpull)
|
||||
Voxel3D.draw(ChunkMesher.flowers(host), atlasFor(host), nil, fpull,
|
||||
ShadowMap.snug(nil))
|
||||
for _, nb in ipairs(neighbors) do
|
||||
Voxel3D.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map),
|
||||
Mat4.translate(nb.ox, 0, nb.oy), fpull)
|
||||
Mat4.translate(nb.ox, 0, nb.oy), fpull,
|
||||
ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
|
||||
end
|
||||
local canvas = Voxel3D.endScene()
|
||||
if not canvas then return end
|
||||
|
||||
+30
-6
@@ -63,11 +63,14 @@ DayNight.T = { dawn = 0, day = 300, dusk = 600, night = 900 }
|
||||
DayNight.KEY = "daytime"
|
||||
DayNight.LABEL = "DAYTIME"
|
||||
|
||||
-- "day" first: an unset or unreadable value is DAY, per the row's contract
|
||||
-- (ModSetting values[1] is the default)
|
||||
-- "sync" first: an unset or unreadable value follows the machine's own
|
||||
-- clock, per the row's contract (ModSetting values[1] is the default).
|
||||
-- "cycle" stays LAST: the FULL preset reaches for it by position.
|
||||
DayNight.setting = ModSetting.new(DayNight.KEY, DayNight.LABEL,
|
||||
{ "day", "night", "dusk", "dawn", "cycle" },
|
||||
{ "DAY", "NIGHT", "DUSK", "DAWN", "CYCLE" })
|
||||
{ "sync", "day", "night", "dusk",
|
||||
"dawn", "cycle" },
|
||||
{ "SYNC", "DAY", "NIGHT", "DUSK",
|
||||
"DAWN", "CYCLE" })
|
||||
|
||||
DayNight.clock = DayNight.T.day -- the running cycle's own position
|
||||
|
||||
@@ -311,10 +314,28 @@ local function mode()
|
||||
return DayNight.setting:get() or "day"
|
||||
end
|
||||
|
||||
-- The effective time: the pin, or the running clock under CYCLE.
|
||||
-- Where SYNC reads the real clock: local hours, 0..24 with the minutes as
|
||||
-- fraction. A named seam rather than a bare os.date call, so the suite can
|
||||
-- hand it a fixed hour.
|
||||
function DayNight.hours()
|
||||
local d = os.date("*t")
|
||||
return d.hour + d.min / 60 + d.sec / 3600
|
||||
end
|
||||
|
||||
-- SYNC: the machine's own time of day laid onto the dial. Local noon is
|
||||
-- the DAY pin, midnight the NIGHT pin, six and eighteen the twilights --
|
||||
-- an hour of the real day is fifty seconds of dial, and Kanto's evening
|
||||
-- falls when the player's does.
|
||||
function DayNight.syncTime()
|
||||
return ((DayNight.hours() - 6) * (DayNight.CYCLE / 24)) % DayNight.CYCLE
|
||||
end
|
||||
|
||||
-- The effective time: the pin, the running clock under CYCLE, or the wall
|
||||
-- clock under SYNC.
|
||||
function DayNight.time()
|
||||
local m = mode()
|
||||
if m == "cycle" then return DayNight.clock end
|
||||
if m == "sync" then return DayNight.syncTime() end
|
||||
return DayNight.T[m] or DayNight.T.day
|
||||
end
|
||||
|
||||
@@ -326,8 +347,11 @@ end
|
||||
function DayNight.update(dt)
|
||||
local m = mode()
|
||||
if m ~= lastMode then
|
||||
if m == "cycle" and DayNight.T[lastMode] then
|
||||
if m == "cycle" then
|
||||
-- from a pin, its time; from SYNC, wherever the real sky already was
|
||||
DayNight.clock = DayNight.T[lastMode]
|
||||
or (lastMode == "sync" and DayNight.syncTime())
|
||||
or DayNight.clock
|
||||
end
|
||||
lastMode = m
|
||||
end
|
||||
|
||||
+10
-2
@@ -390,8 +390,16 @@ ShadowMap.SNUG = 0.9
|
||||
-- terrain moved, so the acne margin the slack exists for is intact where
|
||||
-- it matters. For sprite cards and other thin stand-ins only.
|
||||
--
|
||||
-- Valid between begin() and finish(), which is when `slack` matches the
|
||||
-- frustum the map is being drawn with.
|
||||
-- ONE OBLIGATION comes with it: the caster's LIT draw must hand this same
|
||||
-- snugged transform to its shadow lookup (Voxel3D.draw's `sunModel`).
|
||||
-- Stored and lookup then agree exactly, as they did before snugging, and
|
||||
-- the compare keeps its full acne margin. A caster stored snugged but read
|
||||
-- un-snugged is 0.9 of the margin short, and the loss shows up as diagonal
|
||||
-- moire bands crawling across the card.
|
||||
--
|
||||
-- Valid between begin() and the next begin(): `slack` and the sun hold
|
||||
-- still between redraws of the map, so a lit frame that reuses last
|
||||
-- frame's map computes the same displacement it was stored with.
|
||||
function ShadowMap.snug(model)
|
||||
local f = sunDir()
|
||||
local s = -ShadowMap.slack * ShadowMap.SNUG
|
||||
|
||||
+13
-2
@@ -205,6 +205,7 @@ local SHADER = [[
|
||||
uniform float ghost; // 0 = shade normally, 1 = flatten to it
|
||||
uniform vec3 dayTint; // the hour's light on the world; 1,1,1 = noon
|
||||
uniform Image glassMask; // opaque where the atlas texel is window glass
|
||||
uniform vec2 glassSize; // the mask's dimensions: tc -> atlas texels
|
||||
uniform float glassNight; // 0 = daylight .. 1 = the lamps are on
|
||||
uniform float glassPhase; // the glint's phase: advances with TRAVEL
|
||||
uniform float glassGlint; // and its strength: 0 while standing still
|
||||
@@ -241,7 +242,13 @@ local SHADER = [[
|
||||
// cast with lamplight at night.
|
||||
float glass = Texel(glassMask, tc).a * glassOn;
|
||||
if (glass > 0.0) {
|
||||
float sweep = sin((sc.x + sc.y) * 0.04 - glassPhase);
|
||||
// the sweep lives in the PANE's own space (atlas texels), not the
|
||||
// screen's: a pattern anchored to the screen has the world sliding
|
||||
// through it at zoom speed whenever the camera pans, which strobed --
|
||||
// worst where the pan and the phase ran opposite ways. Anchored to
|
||||
// the glass, panning moves nothing; only the phase does, a fraction
|
||||
// of a texel per step, the same in every walking direction.
|
||||
float sweep = sin(tc.x * glassSize.x * 0.8 - glassPhase);
|
||||
float glint = pow(max(sweep, 0.0), 20.0) * 0.55 * glassGlint;
|
||||
vec3 pane = mix(rgb, vec3(0.93, 0.97, 1.0), glint * glass);
|
||||
float shine = dot(p.rgb, vec3(0.299, 0.587, 0.114));
|
||||
@@ -607,7 +614,11 @@ function Voxel3D.beginScene(w, h, cx, cy, vw, vh, sky, slot)
|
||||
-- declared either way, and unbound is a driver-dependent crash), how lit
|
||||
-- the panes are, and the movement-fed glint as the caller last set it
|
||||
local mask = Voxel3D.glassMask or GlassMask.blank()
|
||||
if mask then pcall(sh.send, sh, "glassMask", mask) end
|
||||
if mask then
|
||||
pcall(sh.send, sh, "glassMask", mask)
|
||||
local ok, mw, mh = pcall(mask.getDimensions, mask)
|
||||
pcall(sh.send, sh, "glassSize", { ok and mw or 1, ok and mh or 1 })
|
||||
end
|
||||
pcall(sh.send, sh, "glassNight", Voxel3D.glassNight or 0)
|
||||
pcall(sh.send, sh, "glassPhase", Voxel3D.glassPhase or 0)
|
||||
pcall(sh.send, sh, "glassGlint", Voxel3D.glassGlint or 0)
|
||||
|
||||
+19
-9
@@ -307,12 +307,13 @@ local function drawEntity(sprite, px, py, facing, phase, flip, gh, colors,
|
||||
-- drift): lets the leaned-back head win against the wall it leans
|
||||
-- OVER while a character genuinely BEHIND a building is dozens of
|
||||
-- pixels deeper and still loses, so real occlusion works.
|
||||
-- the same card UNLEANED is what the sun saw (castShadows draws
|
||||
-- exactly this mesh), so that is where each vertex asks whether the
|
||||
-- light reached it -- see Voxel3D.draw
|
||||
-- the same card UNLEANED -- and SNUGGED, exactly as the sun stored it
|
||||
-- (castShadows draws this mesh through ShadowMap.snug) -- is where each
|
||||
-- vertex asks whether the light reached it; see ShadowMap.snug for why
|
||||
-- the lookup must match the stored transform to the letter
|
||||
Voxel3D.draw(mesh, tex, billboardMatrix(px, py, y, mirror),
|
||||
billboardPull(),
|
||||
Voxel3D.casterMatrix(px, py, y, mirror))
|
||||
ShadowMap.snug(Voxel3D.casterMatrix(px, py, y, mirror)))
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -464,7 +465,11 @@ end
|
||||
-- with distance covered and its strength fades in over a few steps of
|
||||
-- walking and back out within a beat of standing still. Stand still and
|
||||
-- the glass is still; move and the light crosses it.
|
||||
VoxelScene.GLINT_RATE = 0.22 -- radians of sweep per world pixel travelled
|
||||
-- The rate is slow on purpose: the sweep pattern lives in the pane's own
|
||||
-- texels (see the scene shader), so this is a FRACTION of a texel per world
|
||||
-- pixel walked -- one full pass of the glint across a pane per eight or so
|
||||
-- cells of travel, with no frame ever jumping it far enough to strobe.
|
||||
VoxelScene.GLINT_RATE = 0.05 -- radians of sweep per world pixel travelled
|
||||
VoxelScene.GLINT_IN = 0.12 -- strength gained per moving frame
|
||||
VoxelScene.GLINT_OUT = 0.08 -- and lost per resting frame
|
||||
|
||||
@@ -699,11 +704,13 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor)
|
||||
-- wins the overlap, which is the order the flat game draws them in.
|
||||
local figPull = billboardPull()
|
||||
eachFigure(state.map, 0, 0, function(mesh, model, caster)
|
||||
Voxel3D.draw(mesh, atlasFor(state.map), model, figPull, caster)
|
||||
Voxel3D.draw(mesh, atlasFor(state.map), model, figPull,
|
||||
ShadowMap.snug(caster))
|
||||
end)
|
||||
for _, nb in ipairs(state.neighbors or {}) do
|
||||
eachFigure(nb.map, nb.ox, nb.oy, function(mesh, model, caster)
|
||||
Voxel3D.draw(mesh, atlasFor(nb.map), model, figPull, caster)
|
||||
Voxel3D.draw(mesh, atlasFor(nb.map), model, figPull,
|
||||
ShadowMap.snug(caster))
|
||||
end)
|
||||
end
|
||||
-- and the seams are back on for the terrain art that follows: grass and
|
||||
@@ -733,11 +740,14 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor)
|
||||
-- ON, while the nearest flower of the cell south (+20) stays in front
|
||||
-- and keeps overdrawing their feet.
|
||||
local fpull = math.max(0, pull - 8 * math.sin(math.max(Voxel.angle, 0.05)))
|
||||
-- flowers are snugged casters too, so they read their own shadowing
|
||||
-- through the same snugged transform the sun stored them with
|
||||
Voxel3D.draw(ChunkMesher.flowers(state.map), atlasFor(state.map), nil,
|
||||
fpull)
|
||||
fpull, ShadowMap.snug(nil))
|
||||
for _, nb in ipairs(state.neighbors or {}) do
|
||||
Voxel3D.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map),
|
||||
Mat4.translate(nb.ox, 0, nb.oy), fpull)
|
||||
Mat4.translate(nb.ox, 0, nb.oy), fpull,
|
||||
ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
|
||||
end
|
||||
|
||||
return Voxel3D.endScene()
|
||||
|
||||
@@ -321,8 +321,9 @@ local SETTINGS = {
|
||||
.. "shot over the shoulder with a slow parallax drift." },
|
||||
{ DayNight.setting,
|
||||
"What time it is outdoors: pin the sky to DAY, NIGHT, DUSK or DAWN, "
|
||||
.. "or let CYCLE run it -- ten minutes of sun, ten of moon, with the "
|
||||
.. "shadows, the sky and the light following." },
|
||||
.. "let CYCLE run it -- ten minutes of sun, ten of moon, with the "
|
||||
.. "shadows, the sky and the light following -- or SYNC it to the "
|
||||
.. "clock on the wall, so Kanto's evening falls when yours does." },
|
||||
}
|
||||
|
||||
local schema = {}
|
||||
|
||||
@@ -297,8 +297,8 @@ T.eq(#hookedRows, 5, "the options hook added a row per setting")
|
||||
local grid, curve, battles = hookedRows[2], hookedRows[3], hookedRows[4]
|
||||
local daytime = hookedRows[5]
|
||||
T.eq(daytime.label, "DAYTIME", "the day/night row carries its label")
|
||||
T.eq(daytime.value(), "DAY",
|
||||
"and starts pinned to DAY -- no time set means it is day")
|
||||
T.eq(daytime.value(), "SYNC",
|
||||
"and defaults to SYNC -- no value set follows the clock on the wall")
|
||||
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")
|
||||
@@ -1055,6 +1055,11 @@ local skyFor = VoxelScene._skyFor
|
||||
local skyStrength = VoxelScene._skyStrength
|
||||
T.check(type(skyFor) == "function", "the scene exposes its sky resolve")
|
||||
|
||||
-- pinned to DAY for every sky assertion below: the row ships defaulting to
|
||||
-- SYNC -- the machine's own clock -- which would hand these tests whatever
|
||||
-- palette the hour of the test run happened to be
|
||||
run.loader.exports.DRAMATIC_SHAPE.lib.require("DayNight").setting:sync("day")
|
||||
|
||||
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])
|
||||
@@ -1928,7 +1933,10 @@ local Voxel3D = run.loader.exports.DRAMATIC_SHAPE.lib.require("Voxel3D")
|
||||
local Voxel = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelState")
|
||||
|
||||
-- the dial and its pins
|
||||
T.eq(DayNight.setting:get(), "day", "no time set means DAY: the default pin")
|
||||
T.eq(DayNight.setting.values[1], "sync",
|
||||
"no value set means SYNC: values[1] is the row's contract default, and "
|
||||
.. "it follows the machine's clock")
|
||||
DayNight.setting:sync("day")
|
||||
T.eq(DayNight.time(), 300, "and DAY is noon on the dial")
|
||||
local PINS = { day = 300, night = 900, dusk = 600, dawn = 0 }
|
||||
for name, t in pairs(PINS) do
|
||||
@@ -2087,6 +2095,30 @@ modApi.save:set(DayNight.SAVE_KEY, nil)
|
||||
DayNight.restore()
|
||||
T.eq(DayNight.clock, 300, "a save with no clock in it starts at day")
|
||||
|
||||
-- SYNC lays the machine's own clock onto the dial: local noon is the DAY
|
||||
-- pin, midnight is NIGHT, six and eighteen the twilights
|
||||
local hoursWas = DayNight.hours
|
||||
DayNight.setting:sync("sync")
|
||||
DayNight.hours = function() return 12 end
|
||||
T.eq(DayNight.time(), 300, "local noon is the DAY pin")
|
||||
DayNight.hours = function() return 18 end
|
||||
T.eq(DayNight.time(), 600, "six in the evening is DUSK")
|
||||
DayNight.hours = function() return 0 end
|
||||
T.eq(DayNight.time(), 900, "midnight is mid-night")
|
||||
DayNight.hours = function() return 6.5 end
|
||||
T.check(math.abs(DayNight.time() - 25) < 1e-9,
|
||||
"half past six in the morning is just after dawn")
|
||||
-- and stepping from SYNC onto CYCLE picks up from the sky already showing
|
||||
DayNight.update(0)
|
||||
DayNight.setting:sync("cycle")
|
||||
DayNight.update(0)
|
||||
T.check(math.abs(DayNight.clock - 25) < 1e-9,
|
||||
"CYCLE picks up from wherever SYNC's sky already was")
|
||||
DayNight.hours = hoursWas
|
||||
T.eq(DayNight.setting.values[#DayNight.setting.values], "cycle",
|
||||
"cycle stays the ladder's last rung -- the FULL preset reaches for it "
|
||||
.. "by position")
|
||||
|
||||
-- arriving at FULL sets the clock going
|
||||
local Game = require("src.core.Game")
|
||||
local hadSave = Game.save
|
||||
@@ -2098,8 +2130,8 @@ T.eq(DayNight.setting:get(), "cycle",
|
||||
"FULL switches DAYTIME to CYCLE -- the diorama gets its running sky")
|
||||
Game.save = hadSave
|
||||
|
||||
-- put the room back the way it was found
|
||||
DayNight.setting:sync("day")
|
||||
-- put the room back the way it was found (SYNC is the shipped default)
|
||||
DayNight.setting:sync("sync")
|
||||
DayNight.clock = 300
|
||||
DayNight.applyRig(false)
|
||||
Voxel3D.tint = { 1, 1, 1 }
|
||||
|
||||
Reference in New Issue
Block a user