Merge pull request #14 from DramaticShape/3d-battle-mod

fix issue with fight transition in oak's lab
This commit is contained in:
DramaticShape
2026-07-29 23:14:58 -04:00
committed by GitHub
5 changed files with 98 additions and 17 deletions
+18 -4
View File
@@ -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
+1 -1
View File
@@ -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.
+14 -2
View File
@@ -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
+13 -4
View File
@@ -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
+52 -6
View File
@@ -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