From 284172db7b7a1dcd1b53ee34b2e8baef5d1d6a9c Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Wed, 29 Jul 2026 22:50:54 -0400 Subject: [PATCH] add clock sync option --- CHANGELOG.md | 17 ++++++++------ README.md | 14 +++++++----- lib/DayNight.lua | 36 +++++++++++++++++++++++++----- main.lua | 5 +++-- tests/dramatic_shape_test.lua | 42 ++++++++++++++++++++++++++++++----- 5 files changed, 89 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74eda1e..0ab9c2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index aa0e410..2b15712 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ menu. | `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 | | `8`, or the **3D-BTL** options row | ON / OFF — fight on the map instead of on a white field | -| the **DAYTIME** options row | DAY / NIGHT / DUSK / DAWN / CYCLE — what time it is outdoors | +| the **DAYTIME** options row | DAY / NIGHT / DUSK / DAWN / SYNC / CYCLE — what time it is outdoors | **3D-BTL** is on by default and is independent of **VOXEL**: battles draw on the world whether or not the free-roam camera is pitched over. @@ -71,10 +71,14 @@ to the clock: The **DAYTIME** row is one twenty-minute clock — ten minutes of sun, ten of moon. `DAY`, `NIGHT`, `DUSK` and `DAWN` are pins on that dial (noon, -mid-night, sunset, sunrise) and `CYCLE` lets it run, picking up from -whichever pin was showing. 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` with the rest of the preset. +mid-night, sunset, sunrise), `CYCLE` lets it run, picking up from whichever +pin (or SYNC sky) was showing, and `SYNC` — the default — lays your +machine's own clock onto the dial: local noon is the DAY pin, midnight is +NIGHT, six and eighteen the twilights, so out of the box Kanto's evening +falls when yours does. +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` with the rest of the preset. The **sun and moon hang in the sky**, projected through the same matrix the geometry is drawn with, so each stands over the point on the horizon its diff --git a/lib/DayNight.lua b/lib/DayNight.lua index 086f3ff..f561c57 100644 --- a/lib/DayNight.lua +++ b/lib/DayNight.lua @@ -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 diff --git a/main.lua b/main.lua index dd28deb..301fc5a 100644 --- a/main.lua +++ b/main.lua @@ -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 = {} diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua index e4a0c28..f3caeb0 100644 --- a/tests/dramatic_shape_test.lua +++ b/tests/dramatic_shape_test.lua @@ -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 }