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
+62 -2
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,12 +714,40 @@ 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.
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
-- enemy's frontpic, facing-UP for the player's backpic.
@@ -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")