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