CLOSES #1231, CLOSES #1269, CLOSES #1301

This commit is contained in:
bryanthaboi
2026-08-16 10:04:54 -04:00
parent 1151c188a7
commit a910b65434
9 changed files with 390 additions and 40 deletions
+12 -10
View File
@@ -72,11 +72,11 @@ function Pool:reset()
for row = 0, SCREEN_ROWS do self.lyBackup[row] = 0 end
-- wBGP / wOBP0 / wOBP1, as DMG palette bytes.
self.bgp, self.obp0, self.obp1 = NORMAL_PAL, NORMAL_PAL, NORMAL_PAL
-- Per-battler state the CGB paths write instead of touching wBGP: a DMG
-- shade byte the view remaps that battler's pic through, whether the pic is
-- hidden outright, and which of the six BG squares it is drawn at.
-- Per-battler state the CGB paths write instead of touching wBGP: shade
-- byte, hidden flag, lifted tile rows, and which BG square it is drawn at.
self.monShade = { player = NORMAL_PAL, enemy = NORMAL_PAL }
self.hidden = { player = false, enemy = false }
self.liftedRows = { player = nil, enemy = nil }
self.picSize = { player = nil, enemy = nil }
self.slide = { player = 0, enemy = 0 }
-- wSurfWaveBGEffect: the $40-byte rolling wave Surf keeps beside the
@@ -488,6 +488,7 @@ local function runPicResize(self, st, script)
self.picSize[side] = step
self.hidden[side] = false
end
self.liftedRows[side] = nil
incJt(st)
elseif jt >= 1 and jt <= 2 then
incJt(st)
@@ -545,7 +546,7 @@ end
-- The two battler-pic objects: the animation borrows the mon's own tiles as
-- an OBJ so it can be moved without touching the tilemap.
local function battlerObj(self, st, objectPlayer, objectEnemy, clearRows)
local function battlerObj(self, st, objectPlayer, objectEnemy, rows)
local jt = st.jt
if jt == 0 then
if self:flyDig(st) then
@@ -562,25 +563,26 @@ local function battlerObj(self, st, objectPlayer, objectEnemy, clearRows)
}
elseif jt == 1 then
incJt(st)
-- The rows the OBJ now covers are cleared out of the tilemap so the mon
-- is not drawn twice.
self.hidden[self:sideKey(st)] = clearRows
-- engine/battle_anims/bg_effects.asm:448-465: the rows the OBJ now covers
-- come out of the tilemap, and .five never puts them back.
self.liftedRows[self:sideKey(st)] = rows[self:sideKey(st)]
elseif jt >= 2 and jt <= 4 then
incJt(st)
elseif jt == 5 then
self.hidden[self:sideKey(st)] = false
endEffect(st)
end
end
E.BATTLE_BG_EFFECT_BATTLEROBJ_1ROW = function(self, st)
battlerObj(self, st, "BATTLE_ANIM_OBJ_PLAYERHEAD_1ROW",
"BATTLE_ANIM_OBJ_ENEMYFEET_1ROW", true)
"BATTLE_ANIM_OBJ_ENEMYFEET_1ROW",
{ player = { 0, 1 }, enemy = { 6, 1 } })
end
E.BATTLE_BG_EFFECT_BATTLEROBJ_2ROW = function(self, st)
battlerObj(self, st, "BATTLE_ANIM_OBJ_PLAYERHEAD_2ROW",
"BATTLE_ANIM_OBJ_ENEMYFEET_2ROW", true)
"BATTLE_ANIM_OBJ_ENEMYFEET_2ROW",
{ player = { 0, 2 }, enemy = { 5, 2 } })
end
-- BGEffect_RapidCyclePals. On a CGB the palette is applied to ONE battler
+54 -16
View File
@@ -150,6 +150,10 @@ end
-- animation (most of them) skips the canvas entirely.
local function needsCanvas(runner)
local bg = runner.bg
-- engine/battle_anims/bg_effects.asm:448-465: a lifted battler row stays
-- out of the BG until the next pic redraw, so those frames stay baked too.
local lifted = bg.liftedRows
if lifted and (lifted.player or lifted.enemy) then return true end
if bg.scx ~= 0 or bg.scy ~= 0 then return true end
if not bg.lcdc then return false end
if bg.lyEnd <= bg.lyStart then return false end
@@ -229,13 +233,12 @@ end
-- times, and the grouping is by VALUE so a table that happens to repeat costs
-- nothing extra.
local function bgpBands(bg)
local base = bg.bgp or GbcPalette.BGP_IDENTITY
local order, bands = {}, {}
for row = 0, SCREEN_H - 1 do
local inWindow = row >= bg.lyStart and row < bg.lyEnd
-- Outside the window the register still reads whatever wBGP holds, which
-- for every effect that aims hLCDCPointer at rBGP is the identity.
local byte = inWindow and (bg.lyBackup[row] or GbcPalette.BGP_IDENTITY)
or GbcPalette.BGP_IDENTITY
-- Outside the window the register still reads whatever wBGP holds.
local byte = inWindow and (bg.lyBackup[row] or base) or base
local band = bands[byte]
if not band then
band = { byte = byte, rows = {} }
@@ -244,24 +247,56 @@ local function bgpBands(bg)
end
band.rows[#band.rows + 1] = row
end
-- Identity first so the fillBackground below it happens before any blit and
-- the common band is the one drawn from the first bake.
-- The base band first so the fillBackground below it happens before any blit
-- and the common band is the one drawn from the first bake.
table.sort(order, function(a, b)
if a.byte == b.byte then return false end
if a.byte == GbcPalette.BGP_IDENTITY then return true end
if b.byte == GbcPalette.BGP_IDENTITY then return false end
if a.byte == base then return true end
if b.byte == base then return false end
return a.rows[1] < b.rows[1]
end)
return order
end
-- Runs `drawBg` (the battle panel) and then puts it on screen through the
-- animation's BG registers. Returns without a canvas when nothing is
-- displacing anything, which is the common case and costs nothing.
function BattleAnimView:present(runner, drawBg)
-- engine/battle_anims/anim_commands.asm:1293 BattleAnim_SetBGPals
function BattleAnimView:panelPalettes(battle)
local list = {}
local shades = {}
for index = 1, 4 do shades[index] = GbcPalette.color(nil, index) end
list[#list + 1] = shades
local function bracket(pair)
if not (pair and pair[1] and pair[2]) then return end
list[#list + 1] = {
{ 255, 255, 255 },
{ pair[1][1], pair[1][2], pair[1][3] },
{ pair[2][1], pair[2][2], pair[2][3] },
{ 0, 0, 0 },
}
end
for _, side in ipairs({ "player", "enemy" }) do
local mon = battle and battle[side]
local colors = mon
and Palettes.monColors(self.palettes, mon.species, mon.shiny)
if colors then list[#list + 1] = colors end
end
local hpBar = self.palettes and self.palettes.hpBar
if hpBar then
bracket(hpBar.green)
bracket(hpBar.yellow)
bracket(hpBar.red)
end
bracket(self.palettes and self.palettes.expBar)
return list
end
-- Runs `drawBg` (the battle panel) and puts it on screen through the
-- animation's BG registers; skips the canvas when nothing needs one.
function BattleAnimView:present(runner, drawBg, battle)
if not (love and love.graphics) then return end
local bg = runner.bg
if not needsCanvas(runner) then
local invert = bg.bgp and bg.bgp ~= GbcPalette.BGP_IDENTITY
and bg.lcdc ~= "BGP" and GbcPalette.remapShader() ~= nil
if not invert and not needsCanvas(runner) then
drawBg()
return
end
@@ -292,9 +327,10 @@ function BattleAnimView:present(runner, drawBg)
self:bake(drawBg, nil)
-- A shifted scanline exposes whatever the BG map holds beside the pic, which
-- outside the two pic boxes is the blank tile. Without this the exposed
-- strip is the canvas's own transparency and every shake shows a seam.
local remapped = invert
and GbcPalette.useRemap(self:panelPalettes(battle), bg.bgp)
-- A shifted scanline exposes the blank tile beside the pic boxes; without
-- this the exposed strip is the canvas's own transparency.
self:fillBackground()
G.setColor(1, 1, 1, 1)
-- hSCX / hSCY move the whole background; the per-scanline overrides only
@@ -314,6 +350,7 @@ function BattleAnimView:present(runner, drawBg)
self:blitRow(row, dx, dy)
end
end
if remapped then GbcPalette.clear() end
-- Shaderless boot: the panel is raw grayscale, so there are no palettes to
-- permute and the entry's BRIGHTNESS is the only thing left to reproduce.
if bg.lcdc == "BGP" then
@@ -417,5 +454,6 @@ end
BattleAnimView.SCREEN_W = SCREEN_W
BattleAnimView.SCREEN_H = SCREEN_H
BattleAnimView.needsCanvas = needsCanvas
return BattleAnimView
+66 -6
View File
@@ -661,7 +661,7 @@ function BattleState:drawPic(mon, back)
-- the mon drawn at this frame.
local scale = self:picScale(path, mon, back)
if anim then
px = px + (anim.slide or 0)
if not self.liftedPass then px = px + (anim.slide or 0) end
local resized = anim.size and PIC_RESIZE_TILES[anim.size]
if resized then scale = scale * (resized / boxTiles) end
end
@@ -714,11 +714,39 @@ function BattleState:drawPic(mon, back)
-- A mod-supplied pic that says it is already coloured is drawn as it is:
-- pokemon.sprite's ctx.trueColor, the same flag Gen 1's Sprites.path hands
-- back to its own draw site.
if colors and not trueColor and GbcPalette.available() then
GbcPalette.with(colors, body)
else
body()
local function paint()
if colors and not trueColor and GbcPalette.available() then
GbcPalette.with(colors, body)
else
body()
end
end
local lifted = anim and anim.lifted
if not lifted then
paint()
return
end
-- engine/battle_anims/bg_effects.asm:448-465: the ClearBoxed band is off the BG.
local bandY = (back and BattleState.PLAYER_PIC_TILE_Y
or BattleState.ENEMY_PIC_TILE_Y) * 8 + lifted[1] * 8
local bandH = lifted[2] * 8
local psx, psy, psw, psh
if G.getScissor then psx, psy, psw, psh = G.getScissor() end
if self.liftedPass then
G.setScissor(0, bandY, 160, bandH)
paint()
else
if bandY > 0 then
G.setScissor(0, 0, 160, bandY)
paint()
end
local below = 144 - bandY - bandH
if below > 0 then
G.setScissor(0, bandY + bandH, 160, below)
paint()
end
end
if psx then G.setScissor(psx, psy, psw, psh) else G.setScissor() end
end
-- MonsterSpriteGFX (gfx/sprites.asm:82): the facing-DOWN 16x16 frame for the
@@ -1137,6 +1165,7 @@ function BattleState:animPicState(side)
local bg = self.anim.bg
return {
hidden = bg.hidden[side],
lifted = bg.liftedRows and bg.liftedRows[side] or nil,
size = bg.picSize[side],
slide = bg.slide[side] or 0,
shade = bg.monShade[side],
@@ -3436,6 +3465,36 @@ function BattleState:drawScene()
end
end
-- data/battle_anims/objects.asm:390-397: the lifted band rides at ABSOLUTE_X,
-- outside the scanline blit, so the attacker's SCX never moves it.
function BattleState:drawLiftedRows()
local battle = self.battle
if not battle then return end
local enemy = self:animPicState("enemy")
local player = self:animPicState("player")
local enemyLift = enemy and enemy.lifted
local playerLift = player and player.lifted
if not (enemyLift or playerLift) then return end
local G = love.graphics
if not self.liftCanvas then
self.liftCanvas = G.newCanvas(160, 144)
self.liftCanvas:setFilter("nearest", "nearest")
end
local previous = G.getCanvas()
G.setCanvas(self.liftCanvas)
G.clear(0, 0, 0, 0)
G.push()
G.origin()
self.liftedPass = true
if enemyLift then self:drawPic(battle.enemy, false) end
if playerLift then self:drawPic(battle.player, true) end
self.liftedPass = nil
G.pop()
G.setCanvas(previous)
G.setColor(1, 1, 1, 1)
G.draw(self.liftCanvas, 0, 0)
end
function BattleState:drawSceneBody()
local panel = function() self:drawPanel() end
if self.animView and self.slideFrame < BattleAnimView.SLIDE_FRAMES then
@@ -3456,7 +3515,8 @@ function BattleState:drawSceneBody()
return
end
if self.anim and self.animView then
self.animView:present(self.anim, panel)
self.animView:present(self.anim, panel, self.battle)
self:drawLiftedRows()
self.animView:drawObjects(self.anim, self.battle)
return
end
+2 -2
View File
@@ -6,6 +6,7 @@ local Assets = require("src.render.Assets")
local BorderFill = require("src.world.gen2.BorderFill")
local GbcPalette = require("src.render.GbcPalette")
local Palettes = require("src.world.gen2.Palettes")
local PixelCanvas = require("src.render.PixelCanvas")
local MapPreview = {}
@@ -96,9 +97,8 @@ function MapPreview.bake(baker, map, daytime)
local blocks = tileset.blocks
local tilesPerRow = tileset.tilesPerRow or 16
local pw, ph = map.width * 32, map.height * 32
local okCanvas, canvas = pcall(love.graphics.newCanvas, pw, ph)
local okCanvas, canvas = pcall(PixelCanvas.new, pw, ph, "nearest")
if not okCanvas or not canvas then return nil end
if canvas.setFilter then canvas:setFilter("nearest", "nearest") end
local quads = {}
local function quadFor(tile)
local q = quads[tile]
+6 -6
View File
@@ -47,6 +47,7 @@ local NPC = require("src.world.gen2.Npc")
local Party = require("src.pokemon.Party")
local Permissions = require("src.world.gen2.Permissions")
local Pipelines = require("src.render.Pipelines")
local PixelCanvas = require("src.render.PixelCanvas")
local Player = require("src.world.gen2.Player")
local Pokerus = require("src.core.gen2.Pokerus")
local Roamers = require("src.core.gen2.Roamers")
@@ -7612,8 +7613,9 @@ function World:bakeMapImage(map, daytime, flicker)
local blocks = tileset.blocks
local tilesPerRow = tileset.tilesPerRow or 16
local pw, ph = map.width * 32, map.height * 32
local canvas = love.graphics.newCanvas(pw, ph)
canvas:setFilter("nearest", "nearest")
-- Map pixels, not the screen's: a DPI-scaled canvas bakes them non-square
-- (#208, see src/render/PixelCanvas.lua).
local canvas = PixelCanvas.new(pw, ph, "nearest")
local quads = {}
local function quadFor(tile)
local q = quads[tile]
@@ -8140,12 +8142,11 @@ function World:scrollStrip(mapDef, tileset, tile, scroll)
local cached = self.scrollStrips[key]
if cached ~= nil then return cached or nil end
local atlas = self:atlasFor(mapDef)
local ok, canvas = pcall(love.graphics.newCanvas, 8, 8 * 8)
local ok, canvas = pcall(PixelCanvas.new, 8, 8 * 8, "nearest")
if not (atlas and ok and canvas) then
self.scrollStrips[key] = false
return nil
end
canvas:setFilter("nearest", "nearest")
local perRow = tileset.tilesPerRow or 16
local sx, sy = (tile % perRow) * 8, math.floor(tile / perRow) * 8
local aw, ah = atlas:getDimensions()
@@ -9945,8 +9946,7 @@ function World:drawTilted(w, h, s, gw, gh)
if self.tiltCanvas and self.tiltCanvas.release then
self.tiltCanvas:release()
end
self.tiltCanvas = G.newCanvas(gw, gh)
self.tiltCanvas:setFilter("linear", "linear")
self.tiltCanvas = PixelCanvas.new(gw, gh, "linear")
end
local previous = G.getCanvas()
@@ -0,0 +1,68 @@
-- engine/battle_anims/bg_effects.asm:406-471 BattleBGEffect_BattlerObj_1Row
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local T = require("tests.harness")
local BgEffects = require("src.battle.gen2.BgEffects")
local BattleAnimView = require("src.ui.gen2.BattleAnimView")
do
local bg = BgEffects.new(nil, { battleTurn = 0 })
bg:queue("BATTLE_BG_EFFECT_BATTLEROBJ_1ROW", 0, 0, 0)
bg:playFrame()
local spawns = bg:takeSpawns()
T.eq(spawns[1] and spawns[1].object, "BATTLE_ANIM_OBJ_ENEMYFEET_1ROW",
"player attacking: the enemy's feet row becomes an OBJ")
T.eq(spawns[1] and spawns[1].x, 16 * 8 + 4, "at the asm's fixed x")
T.eq(bg.liftedRows.enemy, nil, "the tilemap row is intact on frame one")
T.eq(BattleAnimView.needsCanvas({ bg = bg }), false,
"an intact tilemap with no scroll skips the bake canvas")
bg:playFrame()
local lifted = bg.liftedRows.enemy
T.check(lifted and lifted[1] == 6 and lifted[2] == 1,
"frame two ClearBoxes row 6 of the enemy box (hlcoord 12, 6)")
T.eq(bg.hidden.enemy, false, "the rest of the pic stays on the BG")
T.eq(BattleAnimView.needsCanvas({ bg = bg }), true,
"a lifted row keeps the panel on the bake canvas even with scx 0 and no"
.. " lcdc pointer, so drawPic's 160x144 scissor stays in canvas space")
for _ = 1, 4 do bg:playFrame() end
T.eq(bg:activeCount(), 0, ".five ends the effect")
lifted = bg.liftedRows.enemy
T.check(lifted and lifted[1] == 6 and lifted[2] == 1,
".five never restores the row")
T.eq(BattleAnimView.needsCanvas({ bg = bg }), true,
"and the wait frames after .five stay baked as well")
bg:queue("BATTLE_BG_EFFECT_SHOW_MON", 0, 0, 0)
bg:playFrame()
T.eq(bg.liftedRows.enemy, nil, "SHOW_MON's box redraw puts the row back")
T.eq(BattleAnimView.needsCanvas({ bg = bg }), false,
"after which the plain no-canvas path returns")
end
do
local bg = BgEffects.new(nil, { battleTurn = 1 })
bg:queue("BATTLE_BG_EFFECT_BATTLEROBJ_2ROW", 0, 0, 0)
bg:playFrame()
local spawns = bg:takeSpawns()
T.eq(spawns[1] and spawns[1].object, "BATTLE_ANIM_OBJ_PLAYERHEAD_2ROW",
"enemy attacking: the player's head rows become an OBJ")
T.eq(spawns[1] and spawns[1].x, 6 * 8, "at the asm's fixed x")
bg:playFrame()
local lifted = bg.liftedRows.player
T.check(lifted and lifted[1] == 0 and lifted[2] == 2,
"rows 0-1 of the player box (hlcoord 2, 6, two rows)")
T.eq(bg.liftedRows.enemy, nil, "the attacker keeps its own rows")
end
do
local bg = BgEffects.new(nil, { battleTurn = 0, flying = { enemy = true } })
bg:queue("BATTLE_BG_EFFECT_BATTLEROBJ_1ROW", 0, 0, 0)
bg:playFrame()
T.eq(#bg:takeSpawns(), 0, "a flying target spawns nothing")
T.eq(bg:activeCount(), 0, "and the effect ends at once")
T.eq(bg.liftedRows.enemy, nil, "with no row lifted")
end
T.finish("gen2 battler row lift bug 1231")
+54
View File
@@ -0,0 +1,54 @@
-- Gen 2 map bakes take dpiscale 1 so map pixels stay square LCD pixels
-- (#208 #1301, constants/hardware.inc:932; see src/render/PixelCanvas.lua).
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local g = love.graphics
local seen = {}
local realNewCanvas = g.newCanvas
g.newCanvas = function(w, h, settings)
seen[#seen + 1] = { w = w, h = h, dpiscale = settings and settings.dpiscale }
local c = realNewCanvas(w, h)
c.renderTo = function(_, fn) fn() end
c.setFilter = function() end
return c
end
local World = require("src.world.gen2.World")
local MapPreview = require("src.world.gen2.MapPreview")
local atlas = {
getDimensions = function() return 128, 128 end,
setFilter = function() end,
}
local tileset = { blocks = { [1] = {} }, tilesPerRow = 16 }
local map = { def = { tileset = "TILESET_JOHTO" }, width = 20, height = 18,
blocks = {}, borderBlock = 0 }
local world = setmetatable({
atlasCache = {},
atlasFor = function() return atlas, tileset end,
}, { __index = World })
World.bakeMapImage(world, map, nil, nil)
local bakeCount = #seen
T.check(bakeCount >= 1, "the map bake allocated a canvas")
World.scrollStrip(world, map.def, tileset, 3, { h = 1, v = 0 })
T.check(#seen > bakeCount, "the scroll strip allocated a canvas")
local stripCount = #seen
local origAtlasFor = MapPreview.atlasFor
MapPreview.atlasFor = function() return atlas, tileset end
MapPreview.bake({ tilesets = {}, atlasCache = {}, mapImages = {} }, map, "DAY")
MapPreview.atlasFor = origAtlasFor
T.check(#seen > stripCount, "the save-editor bake allocated a canvas")
for i, c in ipairs(seen) do
T.eq(c.dpiscale, 1,
("canvas %d (%dx%d) is allocated at dpiscale 1"):format(i, c.w, c.h))
end
g.newCanvas = realNewCanvas
T.finish("gen2 map bake dpi")
@@ -44,4 +44,46 @@ do
"the identity byte returns the palette untouched")
end
-- engine/battle_anims/anim_commands.asm:1293 BattleAnim_SetBGPals
do
local BattleAnimView = require("src.ui.gen2.BattleAnimView")
local palettes = {
pokemon = {
[6] = { normal = { { 200, 100, 50 }, { 80, 40, 20 } } },
[25] = { normal = { { 230, 200, 40 }, { 150, 90, 20 } } },
},
hpBar = {
green = { { 100, 220, 100 }, { 30, 160, 30 } },
yellow = { { 230, 220, 90 }, { 180, 150, 20 } },
red = { { 230, 100, 90 }, { 180, 30, 20 } },
},
expBar = { { 120, 140, 230 }, { 40, 60, 160 } },
}
local view = BattleAnimView.new({}, palettes)
local battle = { player = { species = 6 }, enemy = { species = 25 } }
local list = view:panelPalettes(battle)
T.eq(#list, 7, "shades + two mons + three hp bars + exp bar")
local src, dst, count, ambiguous = GbcPalette.remapTable(list, 0x1b)
T.check(count > 0 and count <= GbcPalette.REMAP_MAX,
"the table fits the shader array")
T.eq(ambiguous, 0, "no colour maps two ways")
local function mapped(from)
for i = 1, count do
if src[i][1] == from[1] and src[i][2] == from[2]
and src[i][3] == from[3] then
return dst[i]
end
end
end
local black = mapped({ 255, 255, 255 })
T.check(black and black[1] == 0 and black[2] == 0 and black[3] == 0,
"white inverts to black")
local white = mapped({ 0, 0, 0 })
T.check(white and white[1] == 255 and white[2] == 255 and white[3] == 255,
"black inverts to white")
local mid = mapped({ 200, 100, 50 })
T.check(mid and mid[1] == 80 and mid[2] == 40 and mid[3] == 20,
"the mon's colour 1 shows its colour 2")
end
T.finish("gen2 shadow ball bgp bug 1269")
@@ -0,0 +1,86 @@
-- engine/battle_anims/anim_commands.asm:1293 BattleAnim_SetBGPals
--
-- gen2_shadow_ball_bgp_bug1269.lua proves the runner lands bg.bgp and that
-- panelPalettes/remapTable would invert correctly; it never calls
-- BattleAnimView:present, which is where #1269 actually lived (the byte
-- was landed but nothing read it). This suite drives present() itself and
-- watches the shader binding around the backdrop draw.
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
-- A remap shader that never touches the GPU: present() only needs
-- love.graphics.newShader to succeed so GbcPalette.remapShader() is
-- non-nil, which is the gate `present` checks before it will bake+remap.
local sentShader = { calls = {} }
function sentShader:send(name, ...) self.calls[#self.calls + 1] = name end
love.graphics.newShader = function() return sentShader end
-- The stub's stand-in Quad has no setViewport/getViewport, which blitRow
-- (the scanline blit present() drives 144 times a frame) needs; the real
-- love.graphics.Quad has both.
love.graphics.newQuad = function(x, y, w, h)
local q = { x = x, y = y, w = w, h = h }
function q:setViewport(x2, y2, w2, h2) self.x, self.y, self.w, self.h = x2, y2, w2, h2 end
function q:getViewport() return self.x, self.y, self.w, self.h end
return q
end
local T = require("tests.harness")
local GbcPalette = require("src.render.GbcPalette")
local BattleAnimView = require("src.ui.gen2.BattleAnimView")
local shaderDuringFill = "unset"
local realRectangle = love.graphics.rectangle
love.graphics.rectangle = function(...)
if shaderDuringFill == "unset" then
shaderDuringFill = love.graphics.getShader()
end
return realRectangle(...)
end
local view = BattleAnimView.new({}, nil)
-- data/moves/animations.asm:4509 BattleAnim_ShadowBall's anim_bgp $1b, with
-- no scroll and no rBGP window queued, is exactly the frame that used to
-- fall through the old `needsCanvas`-only early-out untouched.
local runner = {
bg = {
bgp = 0x1b,
lcdc = nil,
scx = 0,
scy = 0,
lyStart = 0,
lyEnd = 0,
lyBackup = {},
},
}
T.check(love.graphics.getShader() == nil, "no shader bound before present")
view:present(runner, function() end, nil)
T.check(shaderDuringFill == sentShader,
"the panel backdrop is drawn through the remap shader, not plainly")
T.check(love.graphics.getShader() == nil,
"the shader is unbound again once present returns, so drawObjects is unaffected")
local sawRemapSend = false
for _, name in ipairs(sentShader.calls) do
if name == "remapSrc" then sawRemapSend = true end
end
T.check(sawRemapSend, "GbcPalette.useRemap actually sent a remap table, not just bound the shader")
-- The identity byte must take the plain path: no bake, no shader, ever.
shaderDuringFill = "unset"
local identityRunner = {
bg = { bgp = GbcPalette.BGP_IDENTITY, lcdc = nil, scx = 0, scy = 0,
lyStart = 0, lyEnd = 0, lyBackup = {} },
}
local plainDrawCalled = false
view:present(identityRunner, function() plainDrawCalled = true end, nil)
T.check(plainDrawCalled, "identity rBGP takes the plain drawBg() path")
T.check(shaderDuringFill == "unset",
"identity rBGP never touches the remap shader")
T.finish("gen2 shadow ball bgp view bug 1269")