mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 08:11:11 +02:00
added battle transition into overworld
This commit is contained in:
@@ -16,6 +16,28 @@
|
||||
|
||||
### Added
|
||||
|
||||
- **A fade out of a battle, where there used to be a hard cut.** The engine
|
||||
wipes INTO a fight with one of the original's eight transitions and cuts
|
||||
straight out of it: `BattleState:finish` pops itself and the map is simply
|
||||
there on the next frame. Between a white field and a tile map the original got
|
||||
away with that; between a placed camera looking across an arena and a diorama
|
||||
looking down on a walking player it reads as a glitch. The battle now fades to
|
||||
black, closes behind it, and the map fades up out of it -- twelve frames each
|
||||
way, registered as a `voxel_battle_exit` transitions record so the timing is
|
||||
retunable in data like the wipes it answers.
|
||||
|
||||
Only while voxel mode is on, and then for EVERY battle, including one that
|
||||
found no arena and drew on the flat battle screen: what is being smoothed over
|
||||
is the return to the map, and the map is a diorama either way. With the mode
|
||||
off, the vanilla cut is untouched.
|
||||
|
||||
One black rectangle over the FINISHED composite does the fading, so the world,
|
||||
the letterbox bars and the battle's own text box all darken by the same amount
|
||||
-- the renderer's existing warp-fade overlay is painted between the world and
|
||||
the UI, which would have left the text box bright over the black. A blackout's
|
||||
own warp fade or an evolution prompt still owns the way out when it takes the
|
||||
screen: the fade stops at the cut rather than fading in over the top of it.
|
||||
|
||||
- **A `FULL` rung on the VOXEL row**, directly after `OFF`. One choice that
|
||||
puts the whole mode in its intended state -- the 35-degree camera, the
|
||||
miniature blur at maximum, the horizon flat, the view fitted, and battles
|
||||
|
||||
@@ -46,6 +46,14 @@ of the two pics' feet, and `WIDE` re-lays that screen out on a 304x144
|
||||
surface, which moves every anchor it is solved against. Switching **3D-BTL**
|
||||
off hands the row straight back with `WIDE` selectable again.
|
||||
|
||||
Leaving a battle fades rather than cuts while **VOXEL** is on: the battle
|
||||
screen fades to black, closes behind it, and the map fades up out of it. The
|
||||
engine wipes *into* a fight and cut straight out of one, which is a big jump
|
||||
between the arena's placed camera and the walking diorama. It applies to every
|
||||
battle while the mode is on — including one that found no arena and drew on
|
||||
the flat battle screen — and not at all while the mode is off. The timing is a
|
||||
`voxel_battle_exit` transitions record, so a data pack can retune it.
|
||||
|
||||
The two HP boxes snap to the window's own edges while a battle is staged —
|
||||
the foe's to the left, yours to the right — instead of huddling in the middle
|
||||
of the frame with map showing either side of them. Same tiles, same size,
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
-- Leaving a battle: the fade the way back to the map never had.
|
||||
--
|
||||
-- Going IN is a whole production -- one of the original's eight wipes, picked by
|
||||
-- three bits, over a flash (src/render/BattleTransition.lua). Coming OUT was a
|
||||
-- hard cut: BattleState:finish pops itself and the map is simply THERE on the
|
||||
-- next frame. On the flat battle screen that is a cut between a white field and
|
||||
-- a tile map, which the original got away with. In this mode it is a cut between
|
||||
-- a placed camera looking across an arena and a diorama looking down on a
|
||||
-- walking player, and a jump that big reads as a glitch rather than as an edit.
|
||||
--
|
||||
-- So the battle fades out, closes behind the black, and the map fades up out of
|
||||
-- it. The timing is a transitions record this mod registers rather than a
|
||||
-- constant in here, so it is retunable in data like the engine's own eight.
|
||||
--
|
||||
-- WHEN. Only while voxel mode is on: this is the diorama's own exit, and a
|
||||
-- vanilla battle keeps the cut it always had. While the mode IS on, every battle
|
||||
-- gets it -- including one that found no arena and drew on the flat battle
|
||||
-- screen -- because what is being smoothed over is the return to the MAP, and
|
||||
-- the map is a diorama either way.
|
||||
--
|
||||
-- HOW IT IS DRAWN, which is the part worth reading. Not by this state: it draws
|
||||
-- nothing at all. It owns a NUMBER, and one black rectangle over the FINISHED
|
||||
-- composite in a wrap around Renderer:endFrame paints it -- after the world
|
||||
-- blit, after the letterbox, after the UI blit, which is the only point where a
|
||||
-- single rect covers everything on screen at once.
|
||||
--
|
||||
-- The renderer's own fade (worldFadeAlpha, which the warp fade uses) is painted
|
||||
-- BETWEEN the world and the UI, because a warp has no UI over it. A fade that
|
||||
-- borrowed it would darken the arena and leave the battle's text box sitting
|
||||
-- bright on top of the black -- and the letterbox bars of a flat battle screen,
|
||||
-- painted by the renderer's clear before any state draws, would not darken at
|
||||
-- all.
|
||||
|
||||
-- the mod namespace (see main.lua): V.require loads a sibling module
|
||||
local V = ...
|
||||
|
||||
local BattleExit = {}
|
||||
BattleExit.__index = BattleExit
|
||||
|
||||
-- The battle underneath keeps drawing while this is up -- what fades is its own
|
||||
-- last live frame, camera drift, HUD and all. Only the top state UPDATES, so
|
||||
-- nothing the battle does can outrun the fade either.
|
||||
BattleExit.isOpaque = false
|
||||
|
||||
-- The registered record's id, and the fallback timing if it is missing (a
|
||||
-- headless caller, or a total conversion that dropped the namespace). Per HALF,
|
||||
-- matching the engine's warp fade, so the whole edit is 24 frames.
|
||||
BattleExit.ID = "voxel_battle_exit"
|
||||
BattleExit.FRAMES = 12
|
||||
|
||||
-- The fade in progress, or nil. Kept here rather than on the state so the
|
||||
-- endFrame wrap has one place to look and nothing can go stale the frame after
|
||||
-- the state leaves the stack.
|
||||
local live = nil
|
||||
|
||||
-- How black the composite is this frame: 0 on the battle's last live frame, 1 at
|
||||
-- the cut, 0 again once the map is up. nil when no fade is running, which is
|
||||
-- every other frame the game ever draws.
|
||||
function BattleExit.veil()
|
||||
if not live then return nil end
|
||||
-- A fade can be taken off the stack by something that is not the fade: a
|
||||
-- script or a shot driver popping down to the overworld, a state teardown.
|
||||
-- Then it is not running, whatever its counter says -- and a veil left behind
|
||||
-- would black the game out for good, because nothing is going to fade it back
|
||||
-- in. Checked here rather than trusted, because this is the one place the
|
||||
-- answer is used. The walk only happens while a fade is live.
|
||||
local stack = live.game and live.game.stack
|
||||
local states = stack and stack.states
|
||||
local onStack = false
|
||||
for i = #(states or {}), 1, -1 do
|
||||
if states[i] == live then onStack = true break end
|
||||
end
|
||||
if not onStack then live = nil; return nil end
|
||||
local a = live.t / live.frames
|
||||
if live.phase == "in" then a = 1 - a end
|
||||
return math.max(0, math.min(1, a))
|
||||
end
|
||||
|
||||
local function framesFor(game)
|
||||
local records = game and game.data and game.data.transitions
|
||||
local record = records and records[BattleExit.ID]
|
||||
local frames = record and record.frames
|
||||
if type(frames) == "number" and frames > 0 then return frames end
|
||||
return BattleExit.FRAMES
|
||||
end
|
||||
|
||||
function BattleExit.new(game, battle, onMidpoint)
|
||||
return setmetatable({ game = game, battle = battle, onMidpoint = onMidpoint,
|
||||
frames = framesFor(game), t = 0, phase = "out" },
|
||||
BattleExit)
|
||||
end
|
||||
|
||||
-- Push the fade over the battle it is closing.
|
||||
function BattleExit.start(battle, onMidpoint)
|
||||
local game = battle.game
|
||||
local self = BattleExit.new(game, battle, onMidpoint)
|
||||
live = self
|
||||
game.stack:push(self)
|
||||
return self
|
||||
end
|
||||
|
||||
function BattleExit:update()
|
||||
self.t = self.t + 1
|
||||
if self.t < self.frames then return end
|
||||
self.t = 0
|
||||
|
||||
if self.phase == "in" then
|
||||
live = nil
|
||||
self.game.stack:pop()
|
||||
return
|
||||
end
|
||||
|
||||
-- ------- the cut, at full black
|
||||
--
|
||||
-- Off the stack FIRST. BattleState:finish pops whatever is on TOP, and while
|
||||
-- this fade is up that is the fade -- so a fade that stayed would eat the
|
||||
-- battle's own pop and leave the battle running underneath, finished but
|
||||
-- still on the stack. Popping ourselves hands the top back to the battle so
|
||||
-- its pop lands on itself.
|
||||
self.phase = "in"
|
||||
local stack = self.game.stack
|
||||
stack:pop()
|
||||
if self.onMidpoint then self.onMidpoint() end
|
||||
if stack:top() == self.game.overworld then
|
||||
stack:push(self) -- and the map comes up out of it
|
||||
return
|
||||
end
|
||||
|
||||
-- We are not going back to the map after all. Either the battle did not
|
||||
-- actually leave -- finish() can be a false start, and wanted() mirrors the
|
||||
-- one the engine has today -- or something else took the screen on the way
|
||||
-- out: a blackout's own warp fade, an evolution prompt. Whatever it is owns
|
||||
-- the transition from here, so this one ends at the cut instead of fading in
|
||||
-- over the top of it. The flag goes back too, so a second finish() that does
|
||||
-- leave gets its own fade.
|
||||
live = nil
|
||||
if self.battle then self.battle.dramaticShapeLeaving = nil end
|
||||
end
|
||||
|
||||
-- "Voxel mode is on", as the ENGINE answers it: switched on, not retired by a
|
||||
-- fault, and runnable on this machine. A function on the table rather than an
|
||||
-- inline call so a driver or a headless test can pin it -- the test harness has
|
||||
-- no depth buffer, where the honest answer is no on every rung.
|
||||
function BattleExit.modeOn()
|
||||
return require("src.render.Pipelines").eligible("voxel") and true or false
|
||||
end
|
||||
|
||||
-- Whether this ending gets the fade.
|
||||
function BattleExit.wanted(battle)
|
||||
local game = battle and battle.game
|
||||
if not (game and game.stack) then return false end
|
||||
-- finish() is not always the end: an unpaid PAY DAY prints its takings and
|
||||
-- comes back through here a moment later (BattleState:finish's first branch).
|
||||
-- Mirrored read-only, so the fade starts on the call that really leaves rather
|
||||
-- than fading to black and snapping back for one more message.
|
||||
if battle.payDay and battle.result == "win" then return false end
|
||||
return BattleExit.modeOn()
|
||||
end
|
||||
|
||||
-- ------- engine seams
|
||||
--
|
||||
-- Two wraps, each idempotent so a hot reload cannot stack them.
|
||||
function BattleExit.install()
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
if not BattleState.dramaticShapeExitHook then
|
||||
local inner = BattleState.finish
|
||||
-- The one place a battle ends. Wrapped rather than listened for: the
|
||||
-- battle.ended event is emitted AFTER the pop, and by then the battle
|
||||
-- screen is gone and there is nothing left to fade out.
|
||||
function BattleState:finish()
|
||||
if self.dramaticShapeLeaving or not BattleExit.wanted(self) then
|
||||
return inner(self)
|
||||
end
|
||||
self.dramaticShapeLeaving = true
|
||||
BattleExit.start(self, function() inner(self) end)
|
||||
end
|
||||
BattleState.dramaticShapeExitHook = true
|
||||
end
|
||||
|
||||
local Renderer = require("src.render.Renderer")
|
||||
if not Renderer.dramaticShapeExitHook then
|
||||
local inner = Renderer.endFrame
|
||||
function Renderer:endFrame(zones, worldZones)
|
||||
inner(self, zones, worldZones)
|
||||
local a = BattleExit.veil()
|
||||
if not a or a <= 0 then return end
|
||||
-- The composite is on the screen by now, in LOVE units, so one rect over
|
||||
-- the window darkens the world, the letterbox bars, the text box and
|
||||
-- anything a present pass put on top, all by the same amount. Left to
|
||||
-- last on purpose: this is a shutter closing on the finished frame, not a
|
||||
-- layer inside it.
|
||||
local w, h = love.graphics.getDimensions()
|
||||
love.graphics.setColor(0, 0, 0, a)
|
||||
love.graphics.rectangle("fill", 0, 0, w, h)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
Renderer.dramaticShapeExitHook = true
|
||||
end
|
||||
end
|
||||
|
||||
return BattleExit
|
||||
+9
-1
@@ -2134,7 +2134,8 @@ function Structures.buildObject(S, map, region, cluster,
|
||||
end
|
||||
for _, c in ipairs(cluster.tiles) do
|
||||
local k = keyOf(c[1], c[2])
|
||||
if support and support.class == "wall" then
|
||||
if support and (support.class == "wall" or support.class == "cliff"
|
||||
or support.art == "bookcase") then
|
||||
-- a figure drawn above a FULL-HEIGHT block (the gym statue on its
|
||||
-- plinth) is a statue on a pillar with ONE cell of footprint: the
|
||||
-- block below already carries the whole base, so the drawn cell
|
||||
@@ -2142,6 +2143,13 @@ function Structures.buildObject(S, map, region, cluster,
|
||||
-- the base backwards. Furniture supports (a monitor on its desk)
|
||||
-- keep the box-extension below -- their drawn cell is the
|
||||
-- furniture's own upper rows, and floor there would amputate it.
|
||||
--
|
||||
-- STRUCTURE, not height, decides which: `cliff` and `bookcase` are
|
||||
-- full-height blocks like `wall` and belong here, while `desk` is
|
||||
-- 24px and still furniture. The Plateau's statues on stacked
|
||||
-- pilasters found this -- taking the furniture branch turned each
|
||||
-- statue's own two rows into a 32px box wearing the pilaster's art,
|
||||
-- so every one of them stood inside a slab of its own plinth.
|
||||
S.skip[k] = true
|
||||
S.ground[k] = best
|
||||
elseif support then
|
||||
|
||||
@@ -78,6 +78,7 @@ local ChunkMesher = V.require("ChunkMesher")
|
||||
local VoxelGrid = V.require("VoxelGrid")
|
||||
local WorldCurve = V.require("WorldCurve")
|
||||
local OverworldBattle = V.require("OverworldBattle")
|
||||
local BattleExit = V.require("BattleExit")
|
||||
|
||||
-- Forward declaration: the voxel pipeline's update hook (registered below)
|
||||
-- calls this, and it is defined further down with the settings it drives.
|
||||
@@ -672,6 +673,23 @@ mod.events:on("battle.ended", function()
|
||||
OverworldBattle.finish()
|
||||
end)
|
||||
|
||||
-- ------- and the way back out
|
||||
--
|
||||
-- The engine wipes INTO a battle with one of the original's eight transitions
|
||||
-- and cuts straight OUT of it. That cut is between two very different cameras
|
||||
-- in this mode, so while voxel mode is on the battle fades out, closes behind
|
||||
-- the black, and the map fades up. The two seams it needs -- BattleState:finish
|
||||
-- and Renderer:endFrame -- and the reasoning for each live in lib/BattleExit.lua.
|
||||
--
|
||||
-- Declared as a transitions record rather than a constant in that file, so the
|
||||
-- fade is retunable in data exactly like the eight wipes it answers, and a total
|
||||
-- conversion can make it as long or as short as its own pacing wants.
|
||||
mod.content.transitions:register(BattleExit.ID, {
|
||||
frames = BattleExit.FRAMES,
|
||||
})
|
||||
|
||||
BattleExit.install()
|
||||
|
||||
mod.exports.version = "1.1.1"
|
||||
-- exposed so a companion mod can pin its own tiles' shapes or read the
|
||||
-- camera without reaching into this mod's file layout
|
||||
|
||||
@@ -1567,6 +1567,108 @@ T.check(e[2] + e[4] <= hudRect.player[2],
|
||||
T.eq(e[1], 0, "the bands are full width")
|
||||
T.eq(e[3], 160, "so a shaken HUD or a long name is carried out with its block")
|
||||
|
||||
-- ------- the way out of a battle is a fade, not a cut
|
||||
--
|
||||
-- The engine wipes INTO a battle and cuts straight out of it. While voxel mode
|
||||
-- is on that cut is between a placed camera looking across an arena and a
|
||||
-- diorama looking down on a walking player, so the battle fades out, closes
|
||||
-- behind the black, and the map fades up.
|
||||
--
|
||||
-- The pop ORDER is the part that has to be right: BattleState:finish pops
|
||||
-- whatever is on top, which is the fade while it is up, so the fade has to be
|
||||
-- off the stack before the battle finishes and back on it afterwards.
|
||||
local Exit = run.loader.exports.DRAMATIC_SHAPE.lib.require("BattleExit")
|
||||
|
||||
T.eq(Data.transitions and Data.transitions[Exit.ID] and
|
||||
Data.transitions[Exit.ID].frames, Exit.FRAMES,
|
||||
"the fade's timing is a registered transitions record, retunable in data")
|
||||
|
||||
local function fakeStack(...)
|
||||
local s = { states = { ... } }
|
||||
function s:top() return self.states[#self.states] end
|
||||
function s:push(state) self.states[#self.states + 1] = state end
|
||||
function s:pop() return table.remove(self.states) end
|
||||
return s
|
||||
end
|
||||
|
||||
-- headless has no depth buffer, so the real gate answers no on every rung;
|
||||
-- pin it, which is what the seam is there for
|
||||
local realModeOn = Exit.modeOn
|
||||
Exit.modeOn = function() return true end
|
||||
|
||||
T.eq(Exit.wanted(nil), false, "no battle, no fade")
|
||||
T.eq(Exit.wanted({ game = { stack = {} } }), true, "a battle in voxel mode fades")
|
||||
T.eq(Exit.wanted({ game = { stack = {} }, payDay = 100, result = "win" }), false,
|
||||
"but not on an unpaid PAY DAY -- that finish() prints a message and comes "
|
||||
.. "back, so the fade belongs to the call that really leaves")
|
||||
Exit.modeOn = function() return false end
|
||||
T.eq(Exit.wanted({ game = { stack = {} } }), false,
|
||||
"and with voxel mode off the battle keeps the cut it always had")
|
||||
Exit.modeOn = function() return true end
|
||||
|
||||
local exitOw = { isOverworld = true }
|
||||
local exitGame = { data = Data, overworld = exitOw }
|
||||
local exitBattle = { game = exitGame }
|
||||
exitGame.stack = fakeStack(exitOw, exitBattle)
|
||||
|
||||
local finished = 0
|
||||
local fade = Exit.start(exitBattle, function()
|
||||
finished = finished + 1
|
||||
exitGame.stack:pop() -- what BattleState:finish does: pops itself
|
||||
end)
|
||||
T.eq(exitGame.stack:top(), fade, "the fade goes on top of the battle it closes")
|
||||
T.eq(Exit.veil(), 0, "and starts on the battle's own last live frame")
|
||||
|
||||
for _ = 1, fade.frames - 1 do fade:update() end
|
||||
T.check(Exit.veil() > 0.5, "the veil climbs while the battle is still up")
|
||||
T.eq(exitGame.stack:top(), fade, "which is a frozen battle: the fade is on top")
|
||||
T.eq(finished, 0, "and nothing has finished yet")
|
||||
|
||||
fade:update() -- the frame the cut lands on
|
||||
T.eq(finished, 1, "at full black the battle finishes for real")
|
||||
T.eq(Exit.veil(), 1, "with the screen fully black over the swap")
|
||||
T.eq(#exitGame.stack.states, 2, "the battle left the stack")
|
||||
T.eq(exitGame.stack.states[1], exitOw, "the map is under it")
|
||||
T.eq(exitGame.stack:top(), fade,
|
||||
"and the fade went back on top of the map to bring it up")
|
||||
|
||||
for _ = 1, fade.frames - 1 do fade:update() end
|
||||
T.check(Exit.veil() < 0.5, "the veil falls away over the map")
|
||||
fade:update()
|
||||
T.eq(Exit.veil(), nil, "and the fade is done -- no veil left on the screen")
|
||||
T.eq(exitGame.stack:top(), exitOw, "with the map back on top, playable")
|
||||
T.eq(finished, 1, "the battle finished exactly once")
|
||||
|
||||
-- ------- a blackout (or an evolution prompt) owns the way out itself
|
||||
--
|
||||
-- Those push their own transition on the way through onFinish, so this fade
|
||||
-- stops at the cut rather than fading in over the top of somebody else's.
|
||||
local other = { isSomeoneElse = true }
|
||||
local blackout = { game = exitGame }
|
||||
exitGame.stack = fakeStack(exitOw, blackout)
|
||||
local warpFade = Exit.start(blackout, function()
|
||||
exitGame.stack:pop() -- the battle leaves
|
||||
exitGame.stack:push(other) -- and a warp fade takes the screen
|
||||
end)
|
||||
for _ = 1, warpFade.frames do warpFade:update() end
|
||||
T.eq(exitGame.stack:top(), other, "the state that took over is on top")
|
||||
T.eq(Exit.veil(), nil, "and this fade let go of the screen at the cut")
|
||||
T.eq(blackout.dramaticShapeLeaving, nil,
|
||||
"with the flag cleared, so a finish() that really leaves fades again")
|
||||
|
||||
-- ------- a stack cleared from under a fade cannot black the game out
|
||||
--
|
||||
-- A script (or the shot driver) pops down to the overworld without asking. The
|
||||
-- fade is gone, so the veil has to go with it -- nothing is left to fade it in.
|
||||
exitGame.stack = fakeStack(exitOw, exitBattle)
|
||||
local orphan = Exit.start(exitBattle, function() end)
|
||||
orphan:update()
|
||||
T.check(Exit.veil() > 0, "a live fade veils the frame")
|
||||
while exitGame.stack:top() ~= exitOw do exitGame.stack:pop() end
|
||||
T.eq(Exit.veil(), nil, "and a fade popped from under itself veils nothing")
|
||||
|
||||
Exit.modeOn = realModeOn
|
||||
|
||||
Pipelines.reset()
|
||||
run.release()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user