DramaticShapeVoxelMod archival mirror · read-only
14f844ad8eb32a61a862e04ec6071400d9b124e8

fix issue with fight transition in oak's lab

DramaticShape · 2026-07-29 · parent 50f8f6bfe3

fix issue with fight transition in oak's lab

Changed files

CHANGELOG.md                  | 22 ++++++++++++++++++----
 README.md                     |  2 +-
 lib/DayNight.lua              | 16 ++++++++++++++--
 main.lua                      | 17 +++++++++++++----
 tests/dramatic_shape_test.lua | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
 5 files changed, 98 insertions(+), 17 deletions(-)

Diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ab9c2e..ca54fbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -63,10 +63,13 @@
   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.
+  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. While **VOXEL** sits on `FULL` the DAYTIME row is
+  HELD at `SYNC` and taken off the menu with the other rows the preset owns
+  (DayNight.forceSync, enforced from the preset, the rows hook and the
+  manager's options_changed -- the same three places BATTLE LAYOUT's pin
+  lives): the full diorama runs on the real sky.
 
   **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
@@ -198,6 +201,17 @@
   Pokemon go solid white for those frames, silhouette and all, and nothing
   else in the frame moves.
 
+- **A scripted battle cut straight in with no transition** (an ENGINE seam,
+  fixed in `src/script/Commands.lua` rather than in this mod): the rival in
+  Oak's lab, and every `start_battle` script, pushed the BattleState bare --
+  no flash, no wipe, the theme starting late -- where the original wipes
+  into scripted fights like any other. `start_battle` now routes through the
+  overworld's own `pushBattle`, which is also the path this mod wraps, so a
+  scripted fight gets its arena staged and the cast culled BEFORE the wipe
+  instead of catching up behind it. A battle scripted with no overworld
+  under it still starts bare, and no music plays twice (BattleState's own
+  start is a same-song no-op).
+
 - **A standing figure's shadow detached from its feet under a low sun.** The
   shadow compare forgives `slack` world pixels so lit ground does not acne
   against its own texels, and that same forgiveness lit the first `slack` of
diff --git a/README.md b/README.md
index d89bbd7..e87e059 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 | SYNC / DAY / NIGHT / DUSK / DAWN / CYCLE — what time it is outdoors; held at SYNC (and off the menu) while VOXEL is FULL |
 
 **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.
\ No newline at end of file
diff --git a/lib/DayNight.lua b/lib/DayNight.lua
index f561c57..3be50e5 100644
--- a/lib/DayNight.lua
+++ b/lib/DayNight.lua
@@ -64,14 +64,26 @@ DayNight.KEY = "daytime"
 DayNight.LABEL = "DAYTIME"
 
 -- "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.
+-- clock, per the row's contract (ModSetting values[1] is the default) --
+-- and forceSync below reaches for it by the same position.
 DayNight.setting = ModSetting.new(DayNight.KEY, DayNight.LABEL,
                                   { "sync", "day", "night", "dusk",
                                     "dawn", "cycle" },
                                   { "SYNC", "DAY", "NIGHT", "DUSK",
                                     "DAWN", "CYCLE" })
 
+-- The one writer for the FULL pin. While VOXEL sits on FULL the DAYTIME
+-- row is off the menu with the rest of the rows the preset owns, and the
+-- value is held HERE at SYNC -- the diorama preset's sky follows the clock
+-- on the wall, whatever was chosen before. Called from every path that can
+-- arrive at or act under FULL (main.lua: the preset itself, the rows hook,
+-- the manager's options_changed), mirroring OverworldBattle.forceOG.
+function DayNight.forceSync(game)
+  if DayNight.setting:get() ~= "sync" then
+    DayNight.setting:setIndex(1, game)
+  end
+end
+
 DayNight.clock = DayNight.T.day     -- the running cycle's own position
 
 -- ------- the two arcs
diff --git a/main.lua b/main.lua
index 301fc5a..24f860d 100644
--- a/main.lua
+++ b/main.lua
@@ -293,10 +293,11 @@ applyFull = function(level)
   -- is solved against (OverworldBattle.forceOG); FULL has just switched staged
   -- fights on, so the layout follows them.
   OverworldBattle.forceOG(Game)
-  -- FULL is the whole diorama, and a diorama with a running sky is more of
-  -- one: the clock is set going (CYCLE is the ladder's last rung). Set, not
-  -- held, like everything else here -- the player can pin it back afterwards.
-  DayNight.setting:setIndex(#DayNight.setting.values, Game)
+  -- and the sky on the clock on the wall: FULL pins DAYTIME to SYNC. Unlike
+  -- the rest of the preset this one IS held, not just set -- the row is off
+  -- the menu while FULL owns it (the rows hook below), so a value changed
+  -- under it could never be seen or changed back.
+  DayNight.forceSync(Game)
   if Game.writeOptions then pcall(Game.writeOptions, Game) end
 end
 
@@ -492,6 +493,9 @@ mod.hooks:wrap("ui.options.rows", function(next, game, rows)
     dropRow(out, "battleLayout")
   end
   if Voxel.isFull(Pipelines.level("voxel")) then
+    -- FULL keeps every mod row off the menu (the early return skips the
+    -- insert below), and holds DAYTIME at SYNC while the row is unreachable
+    DayNight.forceSync(game)
     return dropRow(out, "pipeline:tiltshift")
   end
   local extra = {}
@@ -510,6 +514,11 @@ mod.events:on("mod.options_changed", function(payload)
   -- the OPTIONS row does. The manager persists its own value; this is the one
   -- that has to follow it.
   if stagedBattles() then OverworldBattle.forceOG() end
+  -- and DAYTIME changed from the manager's page while FULL owns it snaps
+  -- straight back to SYNC -- the OPTIONS row is hidden, but the manager's is
+  -- not, and FULL's pin must hold against both
+  local Pipelines = require("src.render.Pipelines")
+  if Voxel.isFull(Pipelines.level("voxel")) then DayNight.forceSync() end
 end)
 
 -- ------- keeping the geometry in step with the world
diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua
index f3caeb0..89ff230 100644
--- a/tests/dramatic_shape_test.lua
+++ b/tests/dramatic_shape_test.lua
@@ -134,6 +134,19 @@ T.check(not fullIds["pipeline:tiltshift"],
 T.check(not fullIds["DRAMATIC_SHAPE:grid"], "and V-GRID")
 T.check(not fullIds["DRAMATIC_SHAPE:curve"], "and V-CURVE")
 T.check(not fullIds["DRAMATIC_SHAPE:battles"], "and 3D-BTL")
+T.check(not fullIds["DRAMATIC_SHAPE:daytime"], "and DAYTIME")
+
+-- DAYTIME is not only hidden under FULL, it is HELD at SYNC: the row cannot
+-- be reached while FULL owns it, so a value changed underneath (the mod
+-- manager's page, an edited options file) snaps back when the menu asks
+do
+  local DayNight = run.loader.exports.DRAMATIC_SHAPE.lib.require("DayNight")
+  DayNight.setting:sync("night")
+  Runtime.call("ui.options.rows", function(_, r) return r end,
+               { data = Data }, { { id = "tilt" } })
+  T.eq(DayNight.setting:get(), "sync",
+    "under FULL the rows hook pins DAYTIME back to SYNC, whatever was chosen")
+end
 
 -- ------- BATTLE LAYOUT is pinned to OG while a fight can be staged on the map
 --
@@ -2115,19 +2128,16 @@ 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
+-- arriving at FULL pins the sky to the clock on the wall
 local Game = require("src.core.Game")
 local hadSave = Game.save
 Game.save = { options = {} }
 DayNight.setting:sync("day")
 defs.voxel.update(0, 2)               -- any rung that is not FULL
 defs.voxel.update(0, 1)               -- and the arrival
-T.eq(DayNight.setting:get(), "cycle",
-  "FULL switches DAYTIME to CYCLE -- the diorama gets its running sky")
+T.eq(DayNight.setting:get(), "sync",
+  "FULL pins DAYTIME to SYNC, whatever was chosen before")
 Game.save = hadSave
 
 -- put the room back the way it was found (SYNC is the shipped default)
@@ -2138,6 +2148,42 @@ Voxel3D.tint = { 1, 1, 1 }
 Voxel3D.vp = nil
 end
 
+-- ------- a scripted fight wipes in like a walked-into one
+--
+-- An engine seam this mod leans on: Commands.start_battle used to push the
+-- BattleState bare, so the rival in Oak's lab CUT to battle with no
+-- transition -- no flash, no wipe, the theme starting late. It now routes
+-- through the overworld's own pushBattle, the same path a grass encounter
+-- takes (and the path this mod wraps to stage the arena before the wipe).
+do
+local Commands = require("src.script.Commands")
+local realBS = package.loaded["src.battle.BattleState"]
+package.loaded["src.battle.BattleState"] = {
+  newWild = function() return { kind = "wild" } end,
+  newTrainer = function() return { kind = "trainer" } end,
+}
+local pushed, viaOverworld = nil, nil
+local runner = { yield = function() end, resume = function() end }
+local ctx = {
+  runner = runner,
+  game = { stack = { push = function(_, s) pushed = s end } },
+  overworld = {
+    pushBattle = function(_, b) viaOverworld = b end,
+    afterBattle = function() end,
+  },
+}
+Commands.start_battle(ctx, "trainer", "RIVAL1", 1)
+T.check(viaOverworld ~= nil and pushed == nil,
+  "a scripted trainer goes through pushBattle: the flash, the wipe and the "
+  .. "theme, like any fight walked into")
+T.eq(viaOverworld.kind, "trainer", "with the battle it was asked to start")
+ctx.overworld = nil
+Commands.start_battle(ctx, "wild", "PIDGEY", 5)
+T.check(pushed ~= nil,
+  "and a battle scripted with no overworld under it still starts bare")
+package.loaded["src.battle.BattleState"] = realBS
+end
+
 -- ------- night falls in the forest
 --
 -- Viridian Forest is not outdoor (no sky, and the light through the leaves