DramaticShapeVoxelMod archival mirror · read-only
2e0ae37bcdb1bec886e4250c1ecc8e7c9c8d2c5e

fixed full screen flashing in battles

DramaticShape · 2026-07-28 · parent 2447046aee

fixed full screen flashing in battles

Changed files

CHANGELOG.md                  |  9 +++++++++
 lib/BattleScene.lua           | 13 +++++++++++++
 lib/OverworldBattle.lua       | 33 ++++++++++++++++++++++++++++-----
 lib/Voxel3D.lua               | 18 ++++++++++++++++++
 tests/dramatic_shape_test.lua | 18 ++++++++++++++++++
 5 files changed, 86 insertions(+), 5 deletions(-)

Diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3226b1..6f76a85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,15 @@
   Leaving it deliberately undoes nothing -- reverting would discard whatever
   had been changed since.
 
+### Fixed
+
+- **The hit flash whited out the whole screen.** The engine draws it as a
+  full-screen white rectangle, which is a flash on a white battle field and
+  a whiteout of the map, the HUD and the text box over a world. It is now
+  dropped on the way past and put back where it was ever about: the two
+  Pokemon go solid white for those frames, silhouette and all, and nothing
+  else in the frame moves.
+
 ### Changed
 
 - **Hotkey `3` walks the angle rungs only and steps over `FULL`.** The key is
diff --git a/lib/BattleScene.lua b/lib/BattleScene.lua
index 7cb945f..42b452f 100644
--- a/lib/BattleScene.lua
+++ b/lib/BattleScene.lua
@@ -278,6 +278,11 @@ end
 -- yet (the terrain mesh is still building, the driver has no depth support).
 -- nil is not a failure: the caller simply leaves the battle screen as the
 -- engine drew it for that frame.
+-- White, for the hit flash. The shader replaces the card's colour outright
+-- rather than multiplying it, so this is the sprite's own silhouette turned
+-- solid white -- not a lightened picture of itself.
+BattleScene.FLASH_COLOR = { 1, 1, 1 }
+
 function BattleScene.render(state, arena, textures, token)
   if not (state and state.map and arena) then return nil end
   if not Voxel3D.available() then return nil end
@@ -358,10 +363,18 @@ function BattleScene.render(state, arena, textures, token)
     -- front of it, and the alpha discard cuts the sprite's own outline out of
     -- the card. A small camera-ward pull keeps a card rooted to the ground
     -- plane from z-fighting the tile it is standing on.
+    -- The engine's hit flash is a full-screen white rectangle, which on a
+    -- white battle field is a flash and over a world is a whiteout of the
+    -- map, the HUD and the text box alike. It is dropped on the way past
+    -- (see OverworldBattle) and put back HERE, on the two things it was ever
+    -- about: the mons themselves go solid white for those frames.
+    local flashing = textures and textures.flash
+    if flashing then Voxel3D.flatten(BattleScene.FLASH_COLOR) end
     for _, card in ipairs(monCards(arena, groundY, textures)) do
       Voxel3D.draw(BattleBillboard.mesh(), card.tex, card.model,
                    BattleBillboard.PULL)
     end
+    if flashing then Voxel3D.flatten(nil) end
     -- grass and flowers ride the same camera-ward pull the free-roam pass
     -- gives them, measured against THIS camera's pitch rather than the
     -- orbit's -- there is no character here for them to overdraw, but the
diff --git a/lib/OverworldBattle.lua b/lib/OverworldBattle.lua
index 672e445..0cf2ab7 100644
--- a/lib/OverworldBattle.lua
+++ b/lib/OverworldBattle.lua
@@ -340,11 +340,21 @@ local function withoutBackgroundFill(battle, fn)
     if mode == "fill" and x == 0 and y == 0
        and w == BattleScene.GB_W and h == BattleScene.GB_H then
       local r, gr, b, a = g.getColor()
-      if r > 0.99 and gr > 0.99 and b > 0.99 and a > 0.99 then
-        local target = g.getCanvas()
-        if target ~= nil
-           and (target == battle.bgCanvas or target == battle.waveCanvas) then
-          g.clear(0, 0, 0, 0)
+      if r > 0.99 and gr > 0.99 and b > 0.99 then
+        -- Two different full-frame whites, both replaced rather than drawn.
+        --
+        -- OPAQUE is the battle's background, and on the offscreen canvases it
+        -- doubles as their clear, so there it becomes a transparent one.
+        --
+        -- TRANSLUCENT is the hit flash. Over a white field that reads as a
+        -- flash; over a world it whites out the map, the HUD and the text box
+        -- together. BattleScene puts it back on the mons alone.
+        if a > 0.99 then
+          local target = g.getCanvas()
+          if target ~= nil
+             and (target == battle.bgCanvas or target == battle.waveCanvas) then
+            g.clear(0, 0, 0, 0)
+          end
         end
         return
       end
@@ -471,6 +481,18 @@ function OverworldBattle.sideTexture(battle, side)
   return { canvas = canvas, ax = ax, ay = ay, trainer = trainer }
 end
 
+-- Whether the hit flash is showing this frame.
+--
+-- Mirrors BattleState:draw's own test, because the flash is a DRAW-time
+-- decision there (a counter plus the frame parity that makes it flicker) and
+-- there is no seam that reports it. Read-only, so the worst a future engine
+-- change can do is flash on a frame the engine would not have.
+function OverworldBattle.flashing(battle)
+  local fx = battle and battle.fx
+  if not (fx and fx.flash and fx.flash > 0) then return false end
+  return (battle.frame or 0) % 4 < 2
+end
+
 -- Both sides, or nil when neither has anything to show.
 function OverworldBattle.textures(battle)
   if not battle then return nil end
@@ -480,6 +502,7 @@ function OverworldBattle.textures(battle)
   out.enemy = okE and enemy or nil
   out.player = okP and player or nil
   if not (out.enemy or out.player) then return nil end
+  out.flash = OverworldBattle.flashing(battle)
   return out
 end
 
diff --git a/lib/Voxel3D.lua b/lib/Voxel3D.lua
index 7a22f0d..cb4344f 100644
--- a/lib/Voxel3D.lua
+++ b/lib/Voxel3D.lua
@@ -537,6 +537,24 @@ function Voxel3D.beginGhost()
   end
 end
 
+-- Flatten whatever is drawn next to one solid colour, or nil to stop.
+--
+-- The same `ghost` path the silhouette uses, WITHOUT beginGhost's inverted
+-- depth test and half alpha -- this is for something drawn normally that
+-- simply wants to come out one colour, which is what a hit flash on a sprite
+-- is. beginScene resets the uniform every frame, so a pass that forgets to
+-- clear it cannot leak into the next one.
+function Voxel3D.flatten(color)
+  if not (active and activeShader) then return end
+  local sh = activeShader
+  if color then
+    pcall(sh.send, sh, "ghostColor", color)
+    pcall(sh.send, sh, "ghost", 1)
+  else
+    pcall(sh.send, sh, "ghost", 0)
+  end
+end
+
 function Voxel3D.endGhost()
   if not active then return end
   pcall(love.graphics.setDepthMode, "lequal", true)
diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua
index 397e779..582a4d8 100644
--- a/tests/dramatic_shape_test.lua
+++ b/tests/dramatic_shape_test.lua
@@ -1374,6 +1374,24 @@ T.check(math.abs(sx - px) < 0.2,
 -- arena floor out from under the two mons pinned to it
 T.eq(rig.curve, 0, "the battle camera switches the world curve off")
 
+-- ------- the hit flash belongs to the mons, not the screen
+--
+-- The engine draws it as a full-screen white rectangle, which is a flash on
+-- a white battle field and a whiteout of the map, the HUD and the text box
+-- over a world. It is dropped on the way past and put back on the two cards.
+local Battles = run.loader.exports.DRAMATIC_SHAPE.lib.require("OverworldBattle")
+T.eq(Battles.flashing(nil), false, "no battle, no flash")
+T.eq(Battles.flashing({ fx = {}, frame = 0 }), false,
+  "a battle with no flash counter is not flashing")
+T.eq(Battles.flashing({ fx = { flash = 0 }, frame = 0 }), false,
+  "nor one whose counter has run out")
+T.eq(Battles.flashing({ fx = { flash = 16 }, frame = 0 }), true,
+  "a live counter flashes on the frames the engine would")
+T.eq(Battles.flashing({ fx = { flash = 16 }, frame = 2 }), false,
+  "and is dark on the others, which is what makes it flicker")
+T.eq(Battles.flashing({ fx = { flash = 16 }, frame = 5 }), true,
+  "on a four-frame cycle")
+
 -- ------- the wireframe is forced on in a battle
 --
 -- A fight is a staged shot rather than the world being walked through, so it