fixed full screen flashing in battles

This commit is contained in:
DramaticShape
2026-07-28 15:22:13 -04:00
parent 2447046aee
commit 2e0ae37bcd
5 changed files with 86 additions and 5 deletions
+9
View File
@@ -20,6 +20,15 @@
Leaving it deliberately undoes nothing -- reverting would discard whatever Leaving it deliberately undoes nothing -- reverting would discard whatever
had been changed since. 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 ### Changed
- **Hotkey `3` walks the angle rungs only and steps over `FULL`.** The key is - **Hotkey `3` walks the angle rungs only and steps over `FULL`.** The key is
+13
View File
@@ -278,6 +278,11 @@ end
-- yet (the terrain mesh is still building, the driver has no depth support). -- 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 -- nil is not a failure: the caller simply leaves the battle screen as the
-- engine drew it for that frame. -- 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) function BattleScene.render(state, arena, textures, token)
if not (state and state.map and arena) then return nil end if not (state and state.map and arena) then return nil end
if not Voxel3D.available() 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 -- 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 -- the card. A small camera-ward pull keeps a card rooted to the ground
-- plane from z-fighting the tile it is standing on. -- 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 for _, card in ipairs(monCards(arena, groundY, textures)) do
Voxel3D.draw(BattleBillboard.mesh(), card.tex, card.model, Voxel3D.draw(BattleBillboard.mesh(), card.tex, card.model,
BattleBillboard.PULL) BattleBillboard.PULL)
end end
if flashing then Voxel3D.flatten(nil) end
-- grass and flowers ride the same camera-ward pull the free-roam pass -- grass and flowers ride the same camera-ward pull the free-roam pass
-- gives them, measured against THIS camera's pitch rather than the -- gives them, measured against THIS camera's pitch rather than the
-- orbit's -- there is no character here for them to overdraw, but the -- orbit's -- there is no character here for them to overdraw, but the
+28 -5
View File
@@ -340,11 +340,21 @@ local function withoutBackgroundFill(battle, fn)
if mode == "fill" and x == 0 and y == 0 if mode == "fill" and x == 0 and y == 0
and w == BattleScene.GB_W and h == BattleScene.GB_H then and w == BattleScene.GB_W and h == BattleScene.GB_H then
local r, gr, b, a = g.getColor() local r, gr, b, a = g.getColor()
if r > 0.99 and gr > 0.99 and b > 0.99 and a > 0.99 then if r > 0.99 and gr > 0.99 and b > 0.99 then
local target = g.getCanvas() -- Two different full-frame whites, both replaced rather than drawn.
if target ~= nil --
and (target == battle.bgCanvas or target == battle.waveCanvas) then -- OPAQUE is the battle's background, and on the offscreen canvases it
g.clear(0, 0, 0, 0) -- 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 end
return return
end end
@@ -471,6 +481,18 @@ function OverworldBattle.sideTexture(battle, side)
return { canvas = canvas, ax = ax, ay = ay, trainer = trainer } return { canvas = canvas, ax = ax, ay = ay, trainer = trainer }
end 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. -- Both sides, or nil when neither has anything to show.
function OverworldBattle.textures(battle) function OverworldBattle.textures(battle)
if not battle then return nil end if not battle then return nil end
@@ -480,6 +502,7 @@ function OverworldBattle.textures(battle)
out.enemy = okE and enemy or nil out.enemy = okE and enemy or nil
out.player = okP and player or nil out.player = okP and player or nil
if not (out.enemy or out.player) then return nil end if not (out.enemy or out.player) then return nil end
out.flash = OverworldBattle.flashing(battle)
return out return out
end end
+18
View File
@@ -537,6 +537,24 @@ function Voxel3D.beginGhost()
end end
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() function Voxel3D.endGhost()
if not active then return end if not active then return end
pcall(love.graphics.setDepthMode, "lequal", true) pcall(love.graphics.setDepthMode, "lequal", true)
+18
View File
@@ -1374,6 +1374,24 @@ T.check(math.abs(sx - px) < 0.2,
-- arena floor out from under the two mons pinned to it -- arena floor out from under the two mons pinned to it
T.eq(rig.curve, 0, "the battle camera switches the world curve off") 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 -- ------- the wireframe is forced on in a battle
-- --
-- A fight is a staged shot rather than the world being walked through, so it -- A fight is a staged shot rather than the world being walked through, so it