Files
gen1recomp/src/core/game3/battle_transition.lua
T
Shane McGovern 795276935c Performance pass: cut per-frame allocations and draw calls, fix frame-rate and batch bugs
- Shared render: cache Font encode/width, skip unchanged palette/ShaderFX
  uniform sends, reuse quads and scratch tables, cheaper sprite palettes
- Mods: fast path for loaded requires, memoized sandbox verdicts, fewer
  Hooks:call allocations, drop emptied hook chains
- Audio: bulk PCM writes, cry/SFX prewarm on the worker, sliced sync
  fallback, cached Gen 3 SE sources
- Gen 1: per-frame SGB battle palettes, hoisted overworld FX, cached menus
- Gen 2: numeric attr grid, indexed anim cells, entity culling, memoized
  cache keys, palette shader set once per tile run
- Gen 3: reuse native tile batches, per-cell metatile rebuilds, partial
  FRLG tile anim uploads, near-view ghost ticking, packed FRLG font atlas
- Fixes: emergency quit timer, Gen 3 batch regression and stale sprite
  index, trainer card texture leak, missing pic re-reads, anim tint cache,
  Crystal ghost grass overdraw, refresh-rate dependent counters, pad swap
2026-09-30 14:53:18 +00:00

2070 lines
56 KiB
Lua

-- Gen 3 (FireRed) battle transition orchestrator & visual effects.
-- Replicates pret battle_transition.c and battle_setup.c.
local Audio = require("src.core.game3.audio")
local Trig = require("src.core.game3.trig")
local Pal = require("src.core.game3.pal_fade")
local bit = require("bit")
local band, bor, bxor = bit.band, bit.bor, bit.bxor
local lshift, rshift, arshift = bit.lshift, bit.rshift, bit.arshift
local floor = math.floor
local SINE = Trig.SINE
local BattleTransition = {}
local IdsFrlg = require("src.core.game3.battle_transition_ids_frlg")
local ID = IdsFrlg.ID
local MUGSHOT_BY_ID = IdsFrlg.MUGSHOT_BY_ID
local IDS_MODULES = {
frlg = "src.core.game3.battle_transition_ids_frlg",
rse = "src.core.game3.battle_transition_ids_rse",
}
function BattleTransition.family()
return require("src.core.game3.profile").family()
end
function BattleTransition.ids(family)
family = family or BattleTransition.family()
local module = IDS_MODULES[family]
if not module then error("battle transition: no id table for family '" .. tostring(family) .. "'") end
return require(module)
end
setmetatable(BattleTransition, {
__index = function(_, k)
if k == "ID" or k == "TERRAIN" then return BattleTransition.ids()[k] end
return nil
end,
})
BattleTransition._active = false
BattleTransition._phase = "idle"
BattleTransition._transitionId = ID.SLICE
BattleTransition._opts = {}
BattleTransition._doneCb = nil
BattleTransition._frame = 0
BattleTransition._fx = nil
BattleTransition._pal = nil
BattleTransition._intro = nil
BattleTransition._worldDrawn = false
local function getChrome()
local ok, Chrome = pcall(require, "src.ui.game3.battle_transition_chrome")
if ok and Chrome then return Chrome end
return nil
end
local function getTrainerPic()
local ok, TP = pcall(require, "src.core.game3.trainer_pic")
if ok and TP then return TP end
return nil
end
--------------------------------------------------------------------------------
-- Transition Selection (battle_setup.c parity)
--------------------------------------------------------------------------------
function BattleTransition.getTerrainByMap(opts)
return BattleTransition.ids().getTerrainByMap(opts)
end
function BattleTransition.pickWild(opts)
return BattleTransition.ids().pickWild(opts)
end
function BattleTransition.pickTrainer(opts)
return BattleTransition.ids().pickTrainer(opts)
end
function BattleTransition.pick(opts)
opts = opts or {}
if opts.transitionId then return opts.transitionId end
if opts.wild then
return BattleTransition.pickWild(opts)
else
return BattleTransition.pickTrainer(opts)
end
end
local DW, DH = 240, 160
local GRAY = { 11, 11, 11 }
local function s16(v)
v = band(v, 0xFFFF)
if v >= 0x8000 then v = v - 0x10000 end
return v
end
local function u16(v) return band(v, 0xFFFF) end
local function u8(v) return band(v, 0xFF) end
-- src/trig.c:514
local function Sin(i, a) return s16(arshift(a * SINE[band(i, 0xFF) + 1], 8)) end
local function Cos(i, a) return s16(arshift(a * SINE[band(i + 64, 0xFF) + 1], 8)) end
local function WIN_RANGE(a, b) return u16(bor(lshift(a, 8), b)) end
local function winH(v)
v = u16(v or 0)
local l, r = rshift(v, 8), band(v, 0xFF)
if r > DW or l > r then r = DW end
if l >= r then return nil end
return l, r
end
local function winV(v)
v = u16(v or 0)
local t, b = rshift(v, 8), band(v, 0xFF)
if b > DH or t > b then b = DH end
return t, b
end
-- src/battle_transition.c:2952
local function initBlackWipe(d, sx, sy, ex, ey, stx, sty)
d.startX, d.startY = sx, sy
d.currX, d.currY = sx, sy
d.endX, d.endY = ex, ey
d.xMove, d.yMove = stx, sty
d.xDist = ex - sx
if d.xDist < 0 then
d.xDist = -d.xDist
d.xMove = -stx
end
d.yDist = ey - sy
if d.yDist < 0 then
d.yDist = -d.yDist
d.yMove = -sty
end
d.temp = 0
end
-- src/battle_transition.c:2979
local function updateBlackWipe(d, xExact, yExact)
if d.xDist > d.yDist then
d.currX = d.currX + d.xMove
d.temp = d.temp + d.yDist
if d.temp > d.xDist then
d.currY = d.currY + d.yMove
d.temp = d.temp - d.xDist
end
else
d.currY = d.currY + d.yMove
d.temp = d.temp + d.xDist
if d.temp > d.yDist then
d.currX = d.currX + d.xMove
d.temp = d.temp - d.yDist
end
end
local n = 0
if (d.xMove > 0 and d.currX >= d.endX) or (d.xMove < 0 and d.currX <= d.endX) then
n = n + 1
if xExact then d.currX = d.endX end
end
if (d.yMove > 0 and d.currY >= d.endY) or (d.yMove < 0 and d.currY <= d.endY) then
n = n + 1
if yExact then d.currY = d.endY end
end
return n == 2
end
-- src/battle_transition.c:2903
local function setCircularMask(buf, x, y, radius)
for i = 0, DH - 1 do buf[i] = 0x0A0A end
for i = 0, 63 do
local sinR = Sin(i, radius)
local cosR = Cos(i, radius)
local leftX = x - sinR
local winVal = x + sinR
local topY = y - cosR
local bottomY = y + cosR
if leftX < 0 then leftX = 0 end
if winVal > DW then winVal = DW end
if topY < 0 then topY = 0 end
if bottomY > DH - 1 then bottomY = DH - 1 end
winVal = bor(winVal, lshift(leftX, 8))
buf[topY] = winVal
buf[bottomY] = winVal
cosR = Cos(i + 1, radius)
local nextTop = y - cosR
local nextBottom = y + cosR
if nextTop < 0 then nextTop = 0 end
if nextBottom > DH - 1 then nextBottom = DH - 1 end
while topY > nextTop do topY = topY - 1; buf[topY] = winVal end
while topY < nextTop do topY = topY + 1; buf[topY] = winVal end
while bottomY > nextBottom do bottomY = bottomY - 1; buf[bottomY] = winVal end
while bottomY < nextBottom do bottomY = bottomY + 1; buf[bottomY] = winVal end
end
end
local function fadeScreenBlack(fx)
fx.pal:blend(Pal.ALL, 16, Pal.BLACK)
end
local function copyBuf(dst, src, n, off)
off = off or 0
for i = off, off + n - 1 do dst[i] = src[i] end
end
local DEF = {}
-- src/battle_transition.c:723
DEF[ID.BLUR] = {
funcs = {
function(fx)
fx.mosaic = 0
fx.state = 1
return true
end,
function(fx)
local t = fx.t
if t.delay ~= 0 then
t.delay = t.delay - 1
else
t.delay = fx.K.blurDelay
t.counter = t.counter + 1
if t.counter == 10 then
fx.pal:beginFade(Pal.ALL, -1, 0, 16, Pal.BLACK)
end
fx.mosaic = band(t.counter, 0xF)
if t.counter > 14 then fx.state = 2 end
end
return false
end,
function(fx)
if not fx.pal:fadeActive() then fx.done = true end
return false
end,
},
init = function(fx) fx.t.delay, fx.t.counter = 0, 0 end,
redraw = true,
}
-- src/battle_transition.c:773
DEF[ID.SWIRL] = {
funcs = {
function(fx)
fx.pal:beginFade(Pal.ALL, 4, 0, 16, Pal.BLACK)
fx.wave1 = { idx = 0, amp = 0 }
fx.state = 1
return false
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
t.sinIndex = s16(t.sinIndex + 4)
t.amp = s16(t.amp + 8)
fx.wave0 = { idx = t.sinIndex, amp = t.amp }
if not fx.pal:fadeActive() then fx.done = true end
fx.T.vblankDma = true
return false
end,
},
init = function(fx) fx.t.sinIndex, fx.t.amp = 0, 0 end,
vblank = function(fx)
if fx.T.vblankDma and fx.wave0 then fx.wave1 = fx.wave0 end
end,
shift = function(fx, y)
local w = fx.wave1
if not w then return 0, 0 end
return Sin(w.idx + 2 * y, w.amp), 0
end,
redraw = true,
}
-- src/battle_transition.c:829
DEF[ID.SHUFFLE] = {
funcs = {
function(fx)
fx.pal:beginFade(Pal.ALL, 4, 0, 16, Pal.BLACK)
fx.sh1 = { sinVal = 0, amp = 0 }
fx.state = 1
return false
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
local sinVal = u16(t.sinVal)
local amp = u16(arshift(t.amp, 8))
t.sinVal = s16(t.sinVal + 4224)
t.amp = s16(t.amp + 384)
fx.sh0 = { sinVal = sinVal, amp = s16(amp) }
if not fx.pal:fadeActive() then fx.done = true end
fx.T.vblankDma = true
return false
end,
},
init = function(fx) fx.t.sinVal, fx.t.amp = 0, 0 end,
vblank = function(fx)
if fx.T.vblankDma and fx.sh0 then fx.sh1 = fx.sh0 end
end,
shift = function(fx, y)
local s = fx.sh1
if not s then return 0, 0 end
return 0, Sin(rshift(u16(s.sinVal + 4224 * y), 8), s.amp)
end,
redraw = true,
}
-- src/battle_transition.c:906
local function bigBlend(t, dec)
local fire
if t.blendDelay == 0 then
fire = true
else
t.blendDelay = t.blendDelay - 1
fire = t.blendDelay == 0
end
if fire then
if dec then
t.evb = t.evb - 1
t.blendDelay = 2
else
t.eva = t.eva + 1
t.blendDelay = 1
end
end
end
local function bigAmp(t)
if t.amp > 0 then
t.sinIndex = s16(t.sinIndex + 12)
t.amp = s16(t.amp - 384)
else
t.amp = 0
end
end
DEF[ID.BIG_POKEBALL] = {
funcs = {
function(fx)
local t = fx.t
t.evb, t.eva, t.sinIndex, t.amp, t.blendDelay = 16, 0, 0, 0x4000, 0
fx.T.bldAlpha = { 0, 16 }
fx.hofs1 = { const = 240 }
fx.state = 1
return false
end,
function(fx)
fx.pattern = true
fx.state = 2
return true
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
bigBlend(t, false)
fx.T.bldAlpha = { t.eva, t.evb }
if t.eva > 15 then fx.state = 3 end
t.sinIndex = s16(t.sinIndex + 12)
t.amp = s16(t.amp - 384)
fx.hofs0 = { idx = t.sinIndex, amp = arshift(t.amp, 8) }
fx.T.vblankDma = true
return false
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
bigBlend(t, true)
fx.T.bldAlpha = { t.eva, t.evb }
if t.evb == 0 then fx.state = 4 end
bigAmp(t)
fx.hofs0 = { idx = t.sinIndex, amp = arshift(t.amp, 8) }
fx.T.vblankDma = true
return false
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
bigAmp(t)
fx.hofs0 = { idx = t.sinIndex, amp = arshift(t.amp, 8) }
if t.amp <= 0 then
fx.state = 5
t.radius = DH
t.radiusDelta = 256
t.vblankSet = false
end
fx.T.vblankDma = true
return false
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
if t.radiusDelta < 2048 then t.radiusDelta = t.radiusDelta + 256 end
if t.radius ~= 0 then
t.radius = t.radius - arshift(t.radiusDelta, 8)
if t.radius < 0 then t.radius = 0 end
end
setCircularMask(fx.buf0, DW / 2, DH / 2, t.radius)
if t.radius == 0 then
fadeScreenBlack(fx)
fx.done = true
end
if not t.vblankSet then
t.vblankSet = true
fx.maskMode = true
end
fx.T.vblankDma = true
return false
end,
},
vblank = function(fx)
local T = fx.T
fx.bldAlpha1 = T.bldAlpha
if fx.maskMode then
if not fx.mask1 then
fx.mask1 = {}
fx.hofs1 = { const = 0 }
end
if T.vblankDma then copyBuf(fx.mask1, fx.buf0, DH) end
elseif T.vblankDma and fx.hofs0 then
fx.hofs1 = fx.hofs0
end
end,
winSpans = function(fx, y, out)
if not fx.mask1 then return 0 end
local l, r = winH(fx.mask1[y])
if not l then
out[1], out[2] = 0, DW
return 1
end
local n = 0
if l > 0 then n = n + 1; out[2 * n - 1], out[2 * n] = 0, l end
if r < DW then n = n + 1; out[2 * n - 1], out[2 * n] = r, DW end
return n
end,
}
-- src/battle_transition.c:1099
local TRAIL_START_X = { [0] = -16, [1] = DW + 16 }
local TRAIL_DELAYS = { 0, 16, 32, 8, 24 }
local TRAIL_SPEEDS = { [0] = 8, [1] = -8 }
local TRAIL_LANE_MIN, TRAIL_LANE_MAX = -16, 20
local function trailRandom()
local ok, Rng = pcall(require, "src.core.game3.rng")
if ok and Rng and Rng.Random then return Rng.Random() end
return math.random(0, 0xFFFF)
end
DEF[ID.POKEBALLS_TRAIL] = {
funcs = {
function(fx)
fx.trail = {}
fx.state = 1
return false
end,
function(fx)
local side0 = band(trailRandom(), 1)
for lane = TRAIL_LANE_MIN, TRAIL_LANE_MAX do
local side = bxor(side0, band(lane, 1))
fx.sprites[#fx.sprites + 1] = {
x = TRAIL_START_X[side],
y = lane * 32 + 16,
lane = lane,
side = side,
delay = TRAIL_DELAYS[(lane % 5) + 1],
prevX = -1,
rot = 0,
real = lane >= 0 and lane < 5,
}
end
fx.state = 2
return false
end,
function(fx)
for _, s in ipairs(fx.sprites) do
if s.real and not s.dead then return false end
end
fadeScreenBlack(fx)
fx.done = true
return false
end,
},
sprite = function(fx, s)
s.rot = s.rot + (s.side == 0 and -4 or 4)
if s.delay ~= 0 then
s.delay = s.delay - 1
return
end
if s.x >= 0 and s.x <= DW then
local posX = arshift(s.x, 3)
if posX ~= s.prevX then
s.prevX = posX
local cols = fx.trail[s.lane]
if not cols then
cols = {}
fx.trail[s.lane] = cols
end
cols[posX] = true
end
end
s.x = s.x + TRAIL_SPEEDS[s.side]
if s.x < -15 or s.x > DW + 15 then s.dead = true end
end,
}
-- src/battle_transition.c:1207
DEF[ID.CLOCKWISE_WIPE] = {
funcs = {
function(fx)
for i = 0, DH - 1 do fx.buf1[i] = WIN_RANGE(DW + 3, DW + 4) end
fx.T.endX = DW / 2
fx.state = 1
return true
end,
function(fx)
local T, b = fx.T, fx.buf0
T.vblankDma = false
initBlackWipe(T, DW / 2, DH / 2, T.endX, -1, 1, 1)
repeat
b[T.currY] = WIN_RANGE(DW / 2, T.currX + 1)
until updateBlackWipe(T, true, true)
T.endX = T.endX + fx.K.wipeStepX
if T.endX >= DW then
T.endY = 0
fx.state = 2
end
T.vblankDma = true
return false
end,
function(fx)
local T, b = fx.T, fx.buf0
local start, stop
local finished = false
T.vblankDma = false
initBlackWipe(T, DW / 2, DH / 2, DW, T.endY, 1, 1)
while true do
start = DW / 2
stop = T.currX + 1
if T.endY >= DH / 2 then
start = T.currX
stop = DW
end
b[T.currY] = WIN_RANGE(start, stop)
if finished then break end
finished = updateBlackWipe(T, true, true)
end
T.endY = T.endY + fx.K.wipeStepY
if T.endY >= DH then
T.endX = DW
fx.state = 3
else
while T.currY < T.endY do
T.currY = T.currY + 1
b[T.currY] = WIN_RANGE(start, stop)
end
end
T.vblankDma = true
return false
end,
function(fx)
local T, b = fx.T, fx.buf0
T.vblankDma = false
initBlackWipe(T, DW / 2, DH / 2, T.endX, DH, 1, 1)
repeat
b[T.currY] = u16(bor(lshift(T.currX, 8), DW))
until updateBlackWipe(T, true, true)
T.endX = T.endX - fx.K.wipeStepX
if T.endX <= 0 then
T.endY = DH
fx.state = 4
end
T.vblankDma = true
return false
end,
function(fx)
local T, b = fx.T, fx.buf0
local start, stop
local finished = false
T.vblankDma = false
initBlackWipe(T, DW / 2, DH / 2, 0, T.endY, 1, 1)
while true do
stop = band(b[T.currY] or 0, 0xFF)
start = T.currX
if T.endY <= DH / 2 then
start = DW / 2
stop = T.currX
end
b[T.currY] = WIN_RANGE(start, stop)
if finished then break end
finished = updateBlackWipe(T, true, true)
end
T.endY = T.endY - fx.K.wipeStepY
if T.endY <= 0 then
T.endX = 0
fx.state = 5
else
while T.currY > T.endY do
T.currY = T.currY - 1
b[T.currY] = WIN_RANGE(start, stop)
end
end
T.vblankDma = true
return false
end,
function(fx)
local T, b = fx.T, fx.buf0
T.vblankDma = false
initBlackWipe(T, 120, 80, T.endX, 0, 1, 1)
repeat
local start, stop = DW / 2, T.currX
if T.currX >= 120 then
start, stop = 0, DW
end
b[T.currY] = WIN_RANGE(start, stop)
until updateBlackWipe(T, true, true)
T.endX = T.endX + fx.K.wipeStepX
if T.currX > DW / 2 then fx.state = 6 end
T.vblankDma = true
return false
end,
function(fx)
fadeScreenBlack(fx)
fx.done = true
return false
end,
},
vblank = function(fx)
if fx.T.vblankDma then copyBuf(fx.buf1, fx.buf0, DH) end
end,
winSpans = function(fx, y, out)
local l, r = winH(fx.buf1[y])
if not l then return 0 end
out[1], out[2] = l, r
return 1
end,
}
-- src/battle_transition.c:1412
DEF[ID.RIPPLE] = {
funcs = {
function(fx)
fx.rp1 = { sinVal = 0, amp = 0 }
fx.state = 1
return true
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
local amp = arshift(t.amp, 8)
local sinVal = u16(t.sinVal)
t.sinVal = s16(t.sinVal + 0x400)
if t.amp <= 0x1FFF then t.amp = t.amp + 384 end
fx.rp0 = { sinVal = sinVal, amp = amp }
t.timer = t.timer + 1
if t.timer == fx.K.rippleFadeAt then
t.fadeStarted = true
fx.pal:beginFade(Pal.ALL, fx.K.rippleFadeDelay, 0, 16, Pal.BLACK)
end
if t.fadeStarted and not fx.pal:fadeActive() then fx.done = true end
fx.T.vblankDma = true
return false
end,
},
init = function(fx) fx.t.sinVal, fx.t.amp, fx.t.timer = 0, 0, 0 end,
vblank = function(fx)
if fx.T.vblankDma and fx.rp0 then fx.rp1 = fx.rp0 end
end,
shift = function(fx, y)
local s = fx.rp1
if not s then return 0, 0 end
return 0, Sin(rshift(u16(s.sinVal + 384 * y), 8), s.amp)
end,
redraw = true,
}
-- src/battle_transition.c:1489
local function waveX(w, y)
local x = w.x + Sin(u8(w.idx + 4 * y), 40)
if x < 0 then x = 0 end
if x > DW then x = DW end
return x
end
DEF[ID.WAVE] = {
funcs = {
function(fx)
fx.state = 1
return true
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
local sinIndex = u8(t.sinIndex)
t.sinIndex = s16(t.sinIndex + 16)
t.x = t.x + 8
local w = { x = t.x, idx = sinIndex }
local finished = true
for i = 0, DH - 1 do
if waveX(w, i) < DW then
finished = false
break
end
end
fx.wv0 = w
if finished then fx.state = 2 end
fx.T.vblankDma = true
return false
end,
function(fx)
fadeScreenBlack(fx)
fx.done = true
return false
end,
},
init = function(fx) fx.t.sinIndex, fx.t.x = 0, 0 end,
vblank = function(fx)
if fx.T.vblankDma and fx.wv0 then fx.wv1 = fx.wv0 end
end,
rowSpans = function(fx, y, R, out)
local w = fx.wv1
if not w then return 0 end
local x = waveX(w, y)
if x <= 0 then return 0 end
out[1], out[2] = R.X0, R.hmapR(x)
return 1
end,
}
-- src/battle_transition.c:2293
DEF[ID.SLICE] = {
funcs = {
function(fx)
fx.t.speed = 256
fx.t.accel = 1
fx.sl1 = 0
fx.state = 1
return true
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
t.effectX = t.effectX + arshift(t.speed, 8)
if t.effectX > DW then t.effectX = DW end
if t.speed <= 0xFFF then t.speed = t.speed + t.accel end
if t.accel < 128 then t.accel = t.accel * 2 end
fx.sl0 = t.effectX
if t.effectX >= DW then fx.state = 2 end
fx.T.vblankDma = true
return false
end,
function(fx)
fadeScreenBlack(fx)
fx.done = true
return false
end,
},
init = function(fx) fx.t.effectX = 0 end,
vblank = function(fx)
if fx.T.vblankDma and fx.sl0 then fx.sl1 = fx.sl0 end
end,
rowSpans = function(fx, y, R, out)
local e = R.hdist(fx.sl1 or 0)
if e <= 0 then return 0 end
if band(y, 1) == 1 then
out[1], out[2] = R.X1 - e, R.X1
else
out[1], out[2] = R.X0, R.X0 + e
end
return 1
end,
shift = function(fx, y, R)
local e = R.hdist(fx.sl1 or 0)
if band(y, 1) == 1 then return e, 0 end
return -e, 0
end,
redraw = true,
}
-- src/battle_transition.c:2402
local NUM_WHITE_BARS = 6
local WHITE_BAR_HEIGHT = 1 + floor(DH / NUM_WHITE_BARS)
local WHITE_BAR_DELAYS = { 0, 9, 15, 6, 12, 3 }
local FADE_TARGET = 16 * 256
DEF[ID.WHITE_BARS_FADE] = {
funcs = {
function(fx)
for i = 0, DH - 1 do
fx.buf1[i] = 0
fx.buf0[i] = 0
end
fx.barLevel = {}
fx.barLevel1 = {}
for i = 0, NUM_WHITE_BARS - 1 do
fx.barLevel[i] = 0
fx.barLevel1[i] = 0
end
fx.T.counter = 0
fx.state = 1
return false
end,
function(fx)
local posY = 0
local last
for i = 0, NUM_WHITE_BARS - 1 do
last = {
x = DW, y = posY, bar = i, fade = 0, finished = false,
delay = WHITE_BAR_DELAYS[i + 1], main = false,
}
fx.sprites[#fx.sprites + 1] = last
posY = posY + WHITE_BAR_HEIGHT
end
last.main = true
fx.state = 2
return false
end,
function(fx)
fx.T.vblankDma = false
if fx.T.counter >= NUM_WHITE_BARS then
fx.pal:blend(Pal.ALL, 16, Pal.WHITE)
fx.state = 3
end
return false
end,
function(fx)
fx.T.vblankDma = false
fx.darkenPhase = true
fx.T.bldY = 0
fx.T.counter = 0
fx.state = 4
return false
end,
function(fx)
local T = fx.T
T.counter = T.counter + 480
T.bldY = arshift(T.counter, 8)
if T.bldY > 16 then
fadeScreenBlack(fx)
fx.done = true
end
return false
end,
},
sprite = function(fx, s)
local T = fx.T
if s.delay ~= 0 then
s.delay = s.delay - 1
if s.main then T.vblankDma = true end
return
end
local h = s.main and (WHITE_BAR_HEIGHT - 2) or WHITE_BAR_HEIGHT
local lvl = rshift(s.fade, 8)
for i = 0, h - 1 do fx.buf0[s.y + i] = lvl end
fx.barLevel[s.bar] = lvl
if s.x == 0 and s.fade == FADE_TARGET then s.finished = true end
s.x = s.x - 24
s.fade = s.fade + 192
if s.x < 0 then s.x = 0 end
if s.fade > FADE_TARGET then s.fade = FADE_TARGET end
if s.main then T.vblankDma = true end
if s.finished and (not s.main or T.counter > 4) then
T.counter = T.counter + 1
s.dead = true
end
end,
vblank = function(fx)
if fx.T.vblankDma and not fx.darkenPhase then
copyBuf(fx.buf1, fx.buf0, DH)
for i = 0, NUM_WHITE_BARS - 1 do fx.barLevel1[i] = fx.barLevel[i] end
end
end,
post = function(fx, R, G)
if fx.darkenPhase then
local y = fx.T.bldY
if y > 16 then y = 16 end
if y > 0 then
G.setColor(0, 0, 0, y / 16)
G.rectangle("fill", R.X0, R.Y0, R.X1 - R.X0, R.Y1 - R.Y0)
end
return
end
if not fx.barLevel1 then return end
R.rowBands(function(y)
local v
if y >= 0 and y < DH then
v = fx.buf1[y] or 0
else
v = fx.barLevel1[floor(y / WHITE_BAR_HEIGHT) % NUM_WHITE_BARS] or 0
end
if v > 16 then v = 16 end
return v
end, function(v, y0, y1)
if v > 0 then
G.setColor(1, 1, 1, v / 16)
G.rectangle("fill", R.X0, y0, R.X1 - R.X0, y1 - y0)
end
end)
end,
}
-- src/battle_transition.c:2583
DEF[ID.GRID_SQUARES] = {
funcs = {
function(fx)
fx.gridStage = 0
fx.state = 1
return false
end,
function(fx)
local t = fx.t
if t.delay == 0 then
t.delay = 3
t.stage = t.stage + 1
fx.gridStage = t.stage
if t.stage > 13 then
fx.state = 2
t.delay = 16
end
end
t.delay = t.delay - 1
return false
end,
function(fx)
local t = fx.t
t.delay = t.delay - 1
if t.delay == 0 then
fadeScreenBlack(fx)
fx.done = true
end
return false
end,
},
init = function(fx) fx.t.delay, fx.t.stage = 0, 0 end,
}
-- src/battle_transition.c:2641
local ANGLED_MOVE = {
{ 56, 0, 0, DH, 0 },
{ 104, DH, DW, 88, 1 },
{ DW, 72, 56, 0, 1 },
{ 0, 32, 144, DH, 0 },
{ 144, DH, 184, 0, 1 },
{ 56, 0, 168, DH, 0 },
{ 168, DH, 48, 0, 1 },
}
local ANGLED_END_DELAYS = { 1, 1, 1, 1, 1, 1, 0 }
DEF[ID.ANGLED_WIPES] = {
funcs = {
function(fx)
for i = 0, DH - 1 do
fx.buf0[i] = WIN_RANGE(0, DW)
fx.buf1[i] = fx.buf0[i]
end
fx.state = 1
return true
end,
function(fx)
local md = ANGLED_MOVE[fx.t.wipeId + 1]
initBlackWipe(fx.T, md[1], md[2], md[3], md[4], 1, 1)
fx.t.dir = md[5]
fx.state = 2
return true
end,
function(fx)
local T, b = fx.T, fx.buf0
T.vblankDma = false
local finished = false
for _ = 0, 15 do
local v = b[T.currY] or 0
local left, right = rshift(v, 8), band(v, 0xFF)
if fx.t.dir == 0 then
if left < T.currX then left = T.currX end
if left > right then left = right end
else
if right > T.currX then right = T.currX end
if right <= left then right = left end
end
b[T.currY] = WIN_RANGE(left, right)
if finished then
fx.state = 3
break
end
finished = updateBlackWipe(T, true, true)
end
T.vblankDma = true
return false
end,
function(fx)
local t = fx.t
t.wipeId = t.wipeId + 1
if t.wipeId < #ANGLED_MOVE then
fx.state = 4
t.delay = ANGLED_END_DELAYS[t.wipeId]
return true
end
fadeScreenBlack(fx)
fx.done = true
return false
end,
function(fx)
local t = fx.t
t.delay = t.delay - 1
if t.delay == 0 then
fx.state = 1
return true
end
return false
end,
},
init = function(fx) fx.t.wipeId = 0 end,
vblank = function(fx)
if fx.T.vblankDma then copyBuf(fx.buf1, fx.buf0, DH) end
end,
winSpans = function(fx, y, out)
local l, r = winH(fx.buf1[y])
if not l then
out[1], out[2] = 0, DW
return 1
end
local n = 0
if l > 0 then n = n + 1; out[2 * n - 1], out[2 * n] = 0, l end
if r < DW then n = n + 1; out[2 * n - 1], out[2 * n] = r, DW end
return n
end,
}
-- src/battle_transition.c:1566
local SPIRAL_ANGLE = {
0x0, 0x26E, 0x100, 0x69, 0x0, -0x69, -0x100, -0x266E,
0x0, 0x26E, 0x100, 0x69, 0x0, -0x69, -0x100, -0x266E,
}
local function spiralUpdate(b, initRadius, dmax, off)
local sinIndex = 0
for i = DH * 2, DH * 6 - 1 do b[i] = DW / 2 end
local x1, y1, x2, y2
for _ = 0, dmax * 16 - 1 do
local a1 = initRadius + rshift(sinIndex, 3)
local a2 = a1
if rshift(sinIndex, 3) ~= rshift(sinIndex + 1, 3) then a2 = a1 + 1 end
y1 = DH / 2 - Sin(sinIndex, a1)
x1 = Cos(sinIndex, a1) + DW / 2
y2 = DH / 2 - Sin(sinIndex + 1, a2)
x2 = Cos(sinIndex + 1, a2) + DW / 2
if not ((y1 < 0 and y2 < 0) or (y1 > DH - 1 and y2 > DH - 1)) then
if y1 < 0 then y1 = 0 end
if y1 > DH - 1 then y1 = DH - 1 end
if x1 < 0 then x1 = 0 end
if x1 > 255 then x1 = 255 end
if y2 < 0 then y2 = 0 end
if y2 > DH - 1 then y2 = DH - 1 end
if x2 < 0 then x2 = 0 end
if x2 > 255 then x2 = 255 end
y2 = y2 - y1
local base = (sinIndex >= 64 and sinIndex < 192) and DH * 2 or DH * 3
b[y1 + base] = x1
if y2 ~= 0 then
x2 = x2 - x1
if x2 < -1 and x1 > 1 then
x1 = x1 - 1
elseif x2 > 1 and x1 < 255 then
x1 = x1 + 1
end
if y2 < 0 then
while y2 < 0 do b[y1 + y2 + base] = x1; y2 = y2 + 1 end
else
while y2 > 0 do b[y1 + y2 + base] = x1; y2 = y2 - 1 end
end
end
end
sinIndex = u8(sinIndex + 1)
end
if off ~= 0 and dmax % 4 ~= 0 then
y1 = Sin(dmax * 16, initRadius + lshift(dmax, 1))
local q = floor(dmax / 4)
local ang = SPIRAL_ANGLE[dmax + 1]
if q == 0 or q == 1 then
if y1 > DH / 2 then y1 = DH / 2 end
for i = y1, 1, -1 do
x1 = arshift(i * ang, 8) + DW / 2
if x1 >= 0 and x1 <= 255 then
if q == 0 and b[560 - i] < x1 then
b[560 - i] = DW / 2
elseif b[400 - i] < x1 then
b[400 - i] = x1
end
end
end
else
if y1 < -(DH / 2 - 1) then y1 = -(DH / 2 - 1) end
for i = y1, 0 do
x1 = arshift(i * ang, 8) + DW / 2
if x1 >= 0 and x1 <= 255 then
if q == 2 and b[400 - i] >= x1 then
b[400 - i] = DW / 2
elseif b[560 - i] > x1 then
b[560 - i] = x1
end
end
end
end
end
for i = 0, DH - 1 do
b[i * 2 + off] = bor(lshift(b[i + DH * 2], 8), b[i + DH * 3])
end
end
local function spiralWinV(d2)
local top = 48 - d2
if top < 0 then top = 0 end
local bottom = d2 + 112
if bottom > 255 then bottom = 255 end
return u16(bor(top, bottom))
end
DEF[ID.SPIRAL] = {
funcs = {
function(fx)
local T = fx.T
T.win0V = WIN_RANGE(48, DH - 48)
T.win1V = WIN_RANGE(16, DH - 16)
T.counter = 0
spiralUpdate(fx.buf1, 0, 0, 0)
spiralUpdate(fx.buf1, 0, 0, 1)
copyBuf(fx.buf0, fx.buf1, DH * 2)
fx.t.d1, fx.t.d2 = 0, 0
fx.state = 1
return false
end,
function(fx)
local T, t = fx.T, fx.t
spiralUpdate(fx.buf1, t.d2, t.d1, 1)
T.vblankDma = true
t.d1 = t.d1 + 1
if t.d1 == #SPIRAL_ANGLE + 1 then
spiralUpdate(fx.buf1, t.d2, 16, 0)
T.win0V = spiralWinV(t.d2)
t.d2 = t.d2 + 32
t.d1 = 0
spiralUpdate(fx.buf1, t.d2, 0, 1)
T.win1V = spiralWinV(t.d2)
T.vblankDma = true
if t.d2 >= DH then
T.counter = 1
fadeScreenBlack(fx)
end
end
return false
end,
},
vblank = function(fx)
local T = fx.T
if T.counter ~= 0 then
fx.done = true
return
end
if T.vblankDma then
copyBuf(fx.buf0, fx.buf1, DH * 2)
T.vblankDma = false
end
fx.win0V1, fx.win1V1 = T.win0V, T.win1V
end,
winSpans = function(fx, y, out)
local n = 0
local t0, b0 = winV(fx.win0V1 or 0)
if y >= t0 and y < b0 then
local l, r = winH(fx.buf0[y * 2])
if l then n = n + 1; out[1], out[2] = l, r end
end
local t1, b1 = winV(fx.win1V1 or 0)
if y >= t1 and y < b1 then
local l, r = winH(fx.buf0[y * 2 + 1])
if l then n = n + 1; out[2 * n - 1], out[2 * n] = l, r end
end
return n
end,
}
local PIC_SLIDE_SPEEDS = { [0] = 12, [1] = -12 }
local PIC_SLIDE_ACCELS = { [0] = -1, [1] = 1 }
local PIC_FUNCS = {
[0] = function() return false end,
[1] = function(s)
s.state = 2
s.speed = PIC_SLIDE_SPEEDS[s.dir]
s.accel = PIC_SLIDE_ACCELS[s.dir]
return true
end,
[2] = function(s)
s.x = s.x + s.speed
if s.dir == 1 and s.x < DW - 107 then
s.state = 3
elseif s.dir == 0 and s.x > 103 then
s.state = 3
end
return false
end,
[3] = function(s)
s.speed = s.speed + s.accel
s.x = s.x + s.speed
if s.speed == 0 then
s.state = 4
s.accel = -s.accel
s.done = true
end
return false
end,
[4] = function() return false end,
[5] = function(s)
s.speed = s.speed + s.accel
s.x = s.x + s.speed
if s.x < -31 or s.x > DW + 31 then s.state = 6 end
return false
end,
[6] = function() return false end,
}
local function mugHofs(T)
T.hofsOpp = s16(T.hofsOpp - 8)
T.hofsPl = s16(T.hofsPl + 8)
end
local MUGSHOT_DEF = {
funcs = {
function(fx)
local t, T = fx.t, fx.T
local mug = fx.mug
local c = mug.coords or { 0, 0 }
fx.opp = { x = c[1] - 32, y = c[2] + 42, state = 0, dir = 0, speed = 0, accel = 0,
pic = mug.pic, scale = mug.scale }
fx.player = { x = DW + 32, y = 106, state = 0, dir = 0, speed = 0, accel = 0,
pic = mug.playerPic, flip = true, scale = 2 }
fx.sprites[1] = fx.opp
fx.sprites[2] = fx.player
t.sinIndex = 0
t.top = 1
t.bottom = DW - 1
T.hofsOpp, T.hofsPl = 0, 0
for i = 0, DH - 1 do fx.buf1[i] = WIN_RANGE(DW, DW + 1) end
fx.state = 1
return false
end,
function(fx)
fx.banner = true
fx.state = 2
return false
end,
function(fx)
local t, T, b = fx.t, fx.T, fx.buf0
T.vblankDma = false
local si = u8(t.sinIndex)
t.sinIndex = s16(t.sinIndex + 16)
for i = 0, DH / 2 - 1 do
local x = t.top + Sin(si, 16)
if x < 0 then x = 1 end
if x > DW then x = DW end
b[i] = x
si = u8(si + 16)
end
for i = DH / 2, DH - 1 do
local x = t.bottom - Sin(si, 16)
if x < 0 then x = 0 end
if x > DW - 1 then x = DW - 1 end
b[i] = u16(bor(lshift(x, 8), DW))
si = u8(si + 16)
end
t.top = t.top + 8
t.bottom = t.bottom - 8
if t.top > DW then t.top = DW end
if t.bottom < 0 then t.bottom = 0 end
if t.top == DW and t.bottom == 0 then fx.state = 3 end
mugHofs(T)
T.vblankDma = true
return false
end,
function(fx)
local t, T = fx.t, fx.T
T.vblankDma = false
for i = 0, DH - 1 do fx.buf0[i] = DW end
fx.state = 4
t.sinIndex, t.top, t.bottom = 0, 0, 0
mugHofs(T)
fx.opp.dir = 0
fx.player.dir = 1
fx.opp.state = fx.opp.state + 1
pcall(function() Audio.playSe("SE_MUGSHOT") end)
T.vblankDma = true
return false
end,
function(fx)
mugHofs(fx.T)
if fx.opp.done then
fx.state = 5
fx.player.state = fx.player.state + 1
end
return false
end,
function(fx)
local T = fx.T
mugHofs(T)
if fx.player.done then
T.vblankDma = false
for i = 0, DH - 1 do
fx.buf0[i] = 0
fx.buf1[i] = 0
end
fx.fadeMode = true
fx.t.timer = 0
fx.t.spread = 0
fx.state = 6
end
return false
end,
function(fx)
local t, T, b = fx.t, fx.T, fx.buf0
T.vblankDma = false
local active = true
mugHofs(T)
if t.spread < DH / 2 then t.spread = t.spread + 2 end
if t.spread > DH / 2 then t.spread = DH / 2 end
t.timer = t.timer + 1
if band(t.timer, 1) == 1 then
active = false
for i = 0, t.spread do
local y1, y2 = DH / 2 - i, DH / 2 + i
if (b[y1] or 0) <= 15 then
active = true
b[y1] = (b[y1] or 0) + 1
end
if (b[y2] or 0) <= 15 then
active = true
b[y2] = (b[y2] or 0) + 1
end
end
end
if t.spread == DH / 2 and not active then fx.state = 7 end
T.vblankDma = true
return false
end,
function(fx)
fx.T.vblankDma = false
fx.pal:blend(Pal.ALL, 16, Pal.WHITE)
fx.darken = true
fx.t.timer = 0
fx.state = 8
return true
end,
function(fx)
local t = fx.t
fx.T.vblankDma = false
t.timer = t.timer + 1
for i = 0, DH - 1 do fx.buf0[i] = u16(t.timer * 257) end
if t.timer > 15 then fx.state = 9 end
fx.T.vblankDma = true
return false
end,
function(fx)
fadeScreenBlack(fx)
fx.done = true
return false
end,
},
sprite = function(_, s)
while PIC_FUNCS[s.state](s) do end
end,
vblank = function(fx)
local T = fx.T
if T.vblankDma then copyBuf(fx.buf1, fx.buf0, DH) end
fx.hofsOpp1, fx.hofsPl1 = T.hofsOpp, T.hofsPl
fx.darken1 = fx.darken
end,
}
for mid in pairs(MUGSHOT_BY_ID) do DEF[mid] = MUGSHOT_DEF end
local DEFS = { frlg = DEF }
BattleTransition.RSE_MODULES = {
"src.core.game3.battle_transition_rse_a",
"src.core.game3.battle_transition_rse_b",
"src.core.game3.battle_transition_rse_frontier",
}
BattleTransition.H = {
DW = DW, DH = DH, Pal = Pal, Audio = Audio,
s16 = s16, u16 = u16, u8 = u8, Sin = Sin, Cos = Cos,
WIN_RANGE = WIN_RANGE, winH = winH, winV = winV,
initBlackWipe = initBlackWipe, updateBlackWipe = updateBlackWipe,
setCircularMask = setCircularMask, fadeScreenBlack = fadeScreenBlack, copyBuf = copyBuf,
getChrome = getChrome, getTrainerPic = getTrainerPic,
}
function BattleTransition.defs(family)
family = family or BattleTransition.family()
local hit = DEFS[family]
if hit then return hit end
local Ids = BattleTransition.ids(family)
local t = {}
for name, id in pairs(Ids.ID) do
local frId = ID[name]
if frId ~= nil and not MUGSHOT_BY_ID[frId] and DEF[frId] then t[id] = DEF[frId] end
end
for id in pairs(Ids.MUGSHOT_BY_ID) do t[id] = MUGSHOT_DEF end
for _, module in ipairs(BattleTransition.RSE_MODULES) do
require(module).register(t, Ids.ID, BattleTransition.H)
end
DEFS[family] = t
return t
end
local function mugshotFor(Ids, key, female)
if Ids.family == "frlg" then
-- pokefirered/src/battle_transition.c:360
local pics = { lorelei = 112, bruno = 113, agatha = 114, lance = 115, blue = 125 }
local coords = {
lorelei = { -8, 0 }, bruno = { -10, 0 }, agatha = { 0, 0 }, lance = { -32, 0 }, blue = { 0, 0 },
}
return { pic = pics[key] or 125, coords = coords[key] or { 0, 0 }, scale = 2,
playerPic = female and 136 or 135 }
end
local Chrome = getChrome()
local manifest = Chrome and Chrome.manifest and Chrome.manifest() or {}
local pics, coords, scales = Ids.mugshotTables(manifest)
local sc = scales[key]
return {
pic = pics[key],
coords = coords[key] or { 0, 0 },
scale = sc and sc[1] / 256 or 2,
playerPic = Ids.MUGSHOT_PLAYER_PIC[female and "female" or "male"],
}
end
--------------------------------------------------------------------------------
-- Orchestrator Lifecycle
--------------------------------------------------------------------------------
local function newFx(tid, opts)
local Ids = BattleTransition.ids()
local defs = BattleTransition.defs(Ids.family)
local def = defs[tid]
if not def then
if Ids.family ~= "frlg" then
print("[game3/battle_transition] transition " .. tostring(tid) .. " has no port; drawing SLICE")
end
def = defs[Ids.ID.SLICE]
end
local fx = {
id = tid, def = def, state = 0, t = {}, T = { vblankDma = false },
buf0 = {}, buf1 = {}, sprites = {}, done = false,
pal = BattleTransition._pal, opts = opts or {}, K = Ids.TUNE,
}
fx.mugKey = Ids.MUGSHOT_BY_ID[tid]
local g = opts and opts.playerGender
fx.female = (g == 1 or g == "female")
if fx.mugKey then fx.mug = mugshotFor(Ids, fx.mugKey, fx.female) end
if def.init then def.init(fx) end
return fx
end
local function stepFx(fx)
local def = fx.def
local funcs = def.funcs
while not fx.done do
local f = funcs[fx.state + 1]
if not f or not f(fx) then break end
end
if def.sprite then
local alive = {}
for _, s in ipairs(fx.sprites) do
if not s.dead then def.sprite(fx, s) end
if not s.dead or s.real then alive[#alive + 1] = s end
end
fx.sprites = alive
end
fx.pal:updateFade()
if def.vblank then def.vblank(fx) end
end
-- src/battle_transition.c:2798
local function introStep(intro, pal)
if intro.state == 0 then
intro.blend = intro.blend + 2
if intro.blend > 16 then intro.blend = 16 end
pal:blend(Pal.ALL, intro.blend, GRAY)
if intro.blend >= 16 then intro.state = 1 end
else
intro.blend = intro.blend - 2
if intro.blend < 0 then intro.blend = 0 end
pal:blend(Pal.ALL, intro.blend, GRAY)
if intro.blend == 0 then
intro.fades = intro.fades - 1
if intro.fades == 0 then return true end
intro.state = 0
end
end
return false
end
function BattleTransition.start(transitionId, opts, doneCb)
opts = opts or {}
BattleTransition._active = true
BattleTransition._transitionId = transitionId or ID.SLICE
BattleTransition._opts = opts
BattleTransition._doneCb = doneCb
BattleTransition._frame = 0
BattleTransition._pal = Pal.new()
BattleTransition._fx = nil
BattleTransition._intro = { state = 0, blend = 0, fades = BattleTransition.ids().TUNE.introFades, wait = 1,
done = false }
BattleTransition._worldDrawn = false
if opts.skipIntro then
BattleTransition._phase = "main"
else
BattleTransition._phase = "intro"
end
if opts.headless then
BattleTransition.finish()
return true
end
return true
end
function BattleTransition.isActive()
return BattleTransition._active
end
function BattleTransition.finish()
BattleTransition._active = false
BattleTransition._phase = "done"
BattleTransition._fx = nil
BattleTransition._mosaicCanvas = nil
BattleTransition._mosaicKey = nil
local cb = BattleTransition._doneCb
BattleTransition._doneCb = nil
if cb then cb() end
end
function BattleTransition.abort()
BattleTransition._active = false
BattleTransition._phase = "idle"
BattleTransition._fx = nil
BattleTransition._doneCb = nil
BattleTransition._mosaicCanvas = nil
BattleTransition._mosaicKey = nil
end
function BattleTransition.tick()
if not BattleTransition._active then return false end
BattleTransition._frame = BattleTransition._frame + 1
local phase = BattleTransition._phase
if phase == "intro" then
local intro = BattleTransition._intro
if intro.wait > 0 then
intro.wait = intro.wait - 1
elseif not intro.done then
if introStep(intro, BattleTransition._pal) then intro.done = true end
else
BattleTransition._phase = "main"
end
BattleTransition._pal:updateFade()
return true
end
if phase == "main" then
local fx = BattleTransition._fx
if not fx then
fx = newFx(BattleTransition._transitionId, BattleTransition._opts)
BattleTransition._fx = fx
end
stepFx(fx)
if fx.done then BattleTransition._phase = "ending" end
return true
end
if phase == "ending" then
BattleTransition.finish()
return true
end
return false
end
local function makeView(X0, X1, Y0, Y1)
local R = { X0 = X0, X1 = X1, Y0 = Y0, Y1 = Y1 }
local span = X1 - X0
R.hdist = function(v) return floor(v * span / DW + 0.5) end
R.hmap = function(x) return X0 + floor(x * span / DW + 0.5) end
R.hmapL = function(x) if x <= 0 then return X0 end return R.hmap(x) end
R.hmapR = function(x) if x >= DW then return X1 end return R.hmap(x) end
return R
end
local SCRATCH = {}
function BattleTransition.blackSpans(y, X0, X1)
local fx = BattleTransition._fx
if not fx then return {} end
local out = {}
local pal = fx.pal.slots[0]
if pal.y >= 16 and pal.color == Pal.BLACK then
return { X0 or 0, X1 or DW }
end
local def = fx.def
local R = makeView(X0 or 0, X1 or DW, 0, DH)
local n = 0
if def.rowSpans then
n = def.rowSpans(fx, y, R, SCRATCH)
elseif def.winSpans then
n = def.winSpans(fx, y, SCRATCH)
end
for i = 1, 2 * n do out[i] = SCRATCH[i] end
return out
end
function BattleTransition.screenCoverage()
local covered = 0
for y = 0, DH - 1 do
local s = BattleTransition.blackSpans(y)
local row = {}
for i = 1, #s, 2 do
for x = math.max(0, s[i]), math.min(DW, s[i + 1]) - 1 do row[x] = true end
end
for _ in pairs(row) do covered = covered + 1 end
end
return covered / (DW * DH)
end
local function mergeRuns(R, valueFn, drawFn)
local cur, start
for y = R.Y0, R.Y1 - 1 do
local v = valueFn(y)
if v ~= cur then
if start and cur then drawFn(cur, start, y) end
cur, start = v, y
end
end
if start and cur then drawFn(cur, start, R.Y1) end
end
local function drawFieldRows(R, G, fx)
local tex = R.field
if not tex then return end
local def = fx and fx.def
if fx and def.shift then
local q = R.fieldQuad
local runStart, rdx, rdy
local function flush(yEnd)
if not runStart then return end
q:setViewport(rdx, runStart + rdy + R.gy, R.X1 - R.X0, yEnd - runStart)
G.draw(tex, q, R.X0, runStart)
end
for y = R.Y0, R.Y1 - 1 do
local dx, dy = def.shift(fx, y, R)
if not (runStart and dx == rdx and dy == rdy) then
flush(y)
runStart, rdx, rdy = y, dx, dy
end
end
flush(R.Y1)
return
end
if fx and fx.mosaic and fx.mosaic > 0 and G.newCanvas then
local m = fx.mosaic + 1
local bx0, by0 = floor(R.X0 / m), floor(R.Y0 / m)
local cw = floor((R.X1 - 1) / m) - bx0 + 1
local ch = floor((R.Y1 - 1) / m) - by0 + 1
-- One canvas per transition, grown only when a level needs more room;
-- each level draws just its cw x ch corner through a quad.
local small = BattleTransition._mosaicCanvas
local key = BattleTransition._mosaicKey
if not small or type(key) ~= "table" or key.w < cw or key.h < ch then
if small and small.release then pcall(small.release, small) end
local nw = math.max(cw, type(key) == "table" and small and key.w or 0)
local nh = math.max(ch, type(key) == "table" and small and key.h or 0)
small = G.newCanvas(nw, nh, { dpiscale = 1 })
small:setFilter("nearest", "nearest")
key = { w = nw, h = nh, quad = G.newQuad(0, 0, cw, ch, nw, nh) }
BattleTransition._mosaicCanvas = small
BattleTransition._mosaicKey = key
end
key.quad:setViewport(0, 0, cw, ch, key.w, key.h)
G.push("all")
G.origin()
G.setCanvas(small)
G.clear(0, 0, 0, 1)
G.setColor(1, 1, 1, 1)
G.draw(tex, -bx0 + 0.5 - (R.gx + 0.5) / m, -by0 + 0.5 - (R.gy + 0.5) / m, 0, 1 / m, 1 / m)
G.pop()
G.setColor(1, 1, 1, 1)
G.draw(small, key.quad, bx0 * m, by0 * m, 0, m, m)
return
end
G.draw(tex, R.X0, R.Y0)
end
local function drawSpanRows(R, G, spanFn)
local rows = {}
local tmp = {}
for y = R.Y0, R.Y1 - 1 do
local k = spanFn(y, tmp)
local key = ""
for i = 1, 2 * k do key = key .. tmp[i] .. "," end
rows[#rows + 1] = key
rows[key] = rows[key] or { unpack(tmp, 1, 2 * k) }
end
G.setColor(0, 0, 0, 1)
local cur, start = nil, R.Y0
local function flush(yEnd)
if cur == nil or cur == "" then return end
local s = rows[cur]
for i = 1, #s, 2 do
if s[i + 1] > s[i] then G.rectangle("fill", s[i], start, s[i + 1] - s[i], yEnd - start) end
end
end
for i = 1, #rows do
local y = R.Y0 + i - 1
if rows[i] ~= cur then
flush(y)
cur, start = rows[i], y
end
end
flush(R.Y1)
end
local function drawWindowBlack(R, G, fx)
local def = fx.def
local screen = {}
local tmp = {}
for y = 0, DH - 1 do
local n = def.winSpans(fx, y, tmp)
local s = {}
for i = 1, 2 * n do s[i] = tmp[i] end
screen[y] = s
end
local sub = { X0 = math.max(R.X0, 0), X1 = math.min(R.X1, DW), Y0 = math.max(R.Y0, 0), Y1 = math.min(R.Y1, DH) }
drawSpanRows(sub, G, function(y, out)
local s = screen[y]
local k = 0
for i = 1, #s, 2 do
local a, b = math.max(s[i], sub.X0), math.min(s[i + 1], sub.X1)
if b > a then
k = k + 1
out[2 * k - 1], out[2 * k] = a, b
end
end
return k
end)
if R.X0 >= 0 and R.Y0 >= 0 and R.X1 <= DW and R.Y1 <= DH then return end
local function blackAt(x, y)
local s = screen[y]
for i = 1, #s, 2 do
if x >= s[i] and x < s[i + 1] then return true end
end
return false
end
local cx, cy, K = DW / 2, DH / 2, 64
local function wedge(ax, ay, bx, by)
G.polygon("fill", ax, ay, bx, by,
cx + (bx - cx) * K, cy + (by - cy) * K,
cx + (ax - cx) * K, cy + (ay - cy) * K)
end
G.setColor(0, 0, 0, 1)
local function edge(count, test, emit)
local runStart
for i = 0, count do
local on = i < count and test(i)
if on and not runStart then
runStart = i
elseif not on and runStart then
emit(runStart, i)
runStart = nil
end
end
end
edge(DW, function(x) return blackAt(x, 0) end, function(a, b) wedge(a, 0, b, 0) end)
edge(DW, function(x) return blackAt(x, DH - 1) end, function(a, b) wedge(a, DH, b, DH) end)
edge(DH, function(y) return blackAt(0, y) end, function(a, b) wedge(0, a, 0, b) end)
edge(DH, function(y) return blackAt(DW - 1, y) end, function(a, b) wedge(DW, a, DW, b) end)
end
local function drawBigPokeball(R, G, fx)
if not fx.pattern then return end
local Chrome = getChrome()
local img = Chrome and Chrome.bigPokeball and Chrome.bigPokeball()
local ba = fx.bldAlpha1 or { 0, 16 }
local eva, evb = math.min(16, ba[1]), math.min(16, ba[2])
if not img then return end
local q = BattleTransition._bigQuad
if not q then
q = G.newQuad(0, 0, 1, 1, DW, DH)
BattleTransition._bigQuad = q
end
local h = fx.hofs1 or { const = 240 }
local function rowHofs(y)
if h.const then return h.const end
return Sin(h.idx + 132 * y, h.amp)
end
local function pass()
for y = 0, DH - 1 do
local s = band(rowHofs(y), 0xFF)
if s < DW then
q:setViewport(s, y, DW - s, 1)
G.draw(img, q, 0, y)
end
if s > 16 then
q:setViewport(0, y, s - 16, 1)
G.draw(img, q, 256 - s, y)
end
end
end
if evb < 16 then
G.setColor(0, 0, 0, 1 - evb / 16)
pass()
end
if eva > 0 then
G.setBlendMode("add")
G.setColor(eva / 16, eva / 16, eva / 16, 1)
pass()
G.setBlendMode("alpha")
end
G.setColor(1, 1, 1, 1)
end
local function drawTrail(R, G, fx)
if not fx.trail then return end
G.setColor(0, 0, 0, 1)
for lane, cols in pairs(fx.trail) do
local y0 = lane * 32
if y0 + 32 > R.Y0 and y0 < R.Y1 then
local runStart
for c = 0, 32 do
local on = cols[c]
if on and not runStart then
runStart = c
elseif not on and runStart then
local a, b = R.hmapL(runStart * 8), R.hmapR(c * 8)
if b > a then G.rectangle("fill", a, y0, b - a, 32) end
runStart = nil
end
end
end
end
local Chrome = getChrome()
local ball = Chrome and Chrome.slidingPokeball and Chrome.slidingPokeball()
if not ball then return end
G.setColor(1, 1, 1, 1)
for _, s in ipairs(fx.sprites) do
if not s.dead and s.y + 16 > R.Y0 and s.y - 16 < R.Y1 then
G.draw(ball, R.hmap(s.x), s.y, -s.rot * math.pi * 2 / 256, 1, 1, 16, 16)
end
end
end
local function drawGrid(R, G, fx)
local stage = fx.gridStage or 0
if stage <= 0 then return end
local Chrome = getChrome()
local img = Chrome and Chrome.gridFrame and Chrome.gridFrame(stage)
if not img then
if stage >= 14 then
G.setColor(0, 0, 0, 1)
G.rectangle("fill", R.X0, R.Y0, R.X1 - R.X0, R.Y1 - R.Y0)
end
return
end
local q = BattleTransition._gridQuad
if not q then
q = G.newQuad(0, 0, 1, 1, 8, 8)
BattleTransition._gridQuad = q
end
q:setViewport(R.X0, R.Y0, R.X1 - R.X0, R.Y1 - R.Y0, 8, 8)
G.setColor(1, 1, 1, 1)
G.draw(img, q, R.X0, R.Y0)
end
local function drawMugshot(R, G, fx)
local Chrome = getChrome()
local vsbar = fx.banner and Chrome and Chrome.vsbar and Chrome.vsbar(fx.mugKey, fx.female and "female" or "male")
if vsbar then
local q = BattleTransition._vsQuad
if not q then
q = G.newQuad(0, 0, 1, 1, 256, DH)
BattleTransition._vsQuad = q
end
G.setColor(1, 1, 1, 1)
local y0, y1 = math.max(R.Y0, 0), math.min(R.Y1, DH)
for y = y0, y1 - 1 do
local a, b
if fx.fadeMode then
a, b = R.X0, R.X1
else
local l, r = winH(fx.buf1[y])
if l then a, b = R.hmapL(l), R.hmapR(r) end
end
if a and b > a then
local hofs = (y < DH / 2) and (fx.hofsOpp1 or 0) or (fx.hofsPl1 or 0)
q:setViewport(a + hofs, y, b - a, 1, 256, DH)
G.draw(vsbar, q, a, y)
end
end
end
local TP = getTrainerPic()
if not TP then return end
local pq = BattleTransition._picQuad
if not pq then
pq = G.newQuad(0, 0, 64, 32, 64, 64)
BattleTransition._picQuad = pq
end
G.setColor(1, 1, 1, 1)
for _, s in ipairs({ fx.opp, fx.player }) do
local entry = s and TP.front and TP.front(s.pic)
local img = entry and (entry.image or entry)
if img and type(img) ~= "table" then
local k = s.scale or 2
local sx = s.flip and -k or k
G.draw(img, pq, R.hmap(s.x), s.y, 0, sx, k, 32, 16)
end
end
end
local function drawMugshotPost(R, G, fx)
if not fx.fadeMode then return end
if fx.darken1 then
local v = band(fx.buf1[0] or 0, 0x1F)
if v > 16 then v = 16 end
if v > 0 then
G.setColor(0, 0, 0, v / 16)
G.rectangle("fill", R.X0, R.Y0, R.X1 - R.X0, R.Y1 - R.Y0)
end
return
end
mergeRuns(R, function(y)
local yy = y
if yy < 0 then yy = 0 end
if yy > DH - 1 then yy = DH - 1 end
local v = band(fx.buf1[yy] or 0, 0x1F)
if v > 16 then v = 16 end
return v
end, function(v, y0, y1)
if v > 0 then
G.setColor(1, 1, 1, v / 16)
G.rectangle("fill", R.X0, y0, R.X1 - R.X0, y1 - y0)
end
end)
end
local function render(R, G)
local fx = BattleTransition._fx
R.rowBands = function(valueFn, drawFn) mergeRuns(R, valueFn, drawFn) end
G.setColor(1, 1, 1, 1)
G.setBlendMode("alpha")
if fx and fx.def.redraw then drawFieldRows(R, G, fx) end
if fx then
local def = fx.def
if def == DEF[ID.BIG_POKEBALL] then drawBigPokeball(R, G, fx) end
if def == DEF[ID.GRID_SQUARES] then drawGrid(R, G, fx) end
if def == DEF[ID.POKEBALLS_TRAIL] then drawTrail(R, G, fx) end
if fx.mugKey then drawMugshot(R, G, fx) end
if def.draw then def.draw(fx, R, G) end
if def.rowSpans then
drawSpanRows(R, G, function(y, out) return def.rowSpans(fx, y, R, out) end)
elseif def.winSpans then
drawWindowBlack(R, G, fx)
end
end
local pal = BattleTransition._pal and BattleTransition._pal.slots[0]
if pal and pal.y > 0 then
local c = pal.color or Pal.BLACK
G.setColor(c[1] / 31, c[2] / 31, c[3] / 31, math.min(16, pal.y) / 16)
G.rectangle("fill", R.X0, R.Y0, R.X1 - R.X0, R.Y1 - R.Y0)
end
if fx then
if fx.def.post then fx.def.post(fx, R, G) end
if fx.mugKey then drawMugshotPost(R, G, fx) end
end
G.setColor(1, 1, 1, 1)
end
local function viewFor(gx, gy, w, h)
local R = makeView(-gx, w - gx, -gy, h - gy)
R.gx, R.gy = gx, gy
return R
end
function BattleTransition.drawWorld(canvas, vw, vh)
if not BattleTransition._active then return false end
if BattleTransition._opts and BattleTransition._opts.overUi then return false end
if not (love and love.graphics and canvas) then return false end
local G = love.graphics
local gx, gy = floor((vw - DW) / 2), floor((vh - DH) / 2)
local R = viewFor(gx, gy, vw, vh)
local fx = BattleTransition._fx
G.push("all")
G.origin()
if fx and fx.def.redraw then
local scratch = BattleTransition._scratch
if not scratch or scratch:getWidth() ~= vw or scratch:getHeight() ~= vh then
if scratch and scratch.release then pcall(scratch.release, scratch) end
scratch = G.newCanvas(vw, vh, { dpiscale = 1 })
scratch:setFilter("nearest", "nearest")
pcall(scratch.setWrap, scratch, "repeat", "repeat")
BattleTransition._scratch = scratch
BattleTransition._fieldQuad = G.newQuad(0, 0, 1, 1, vw, vh)
end
G.setCanvas(scratch)
G.clear(0, 0, 0, 1)
G.setColor(1, 1, 1, 1)
G.draw(canvas, 0, 0)
G.setCanvas(canvas)
G.clear(0, 0, 0, 1)
R.field = scratch
R.fieldQuad = BattleTransition._fieldQuad
end
G.translate(gx, gy)
local ok, err = pcall(render, R, G)
G.pop()
if not ok then print("[game3/battle_transition] " .. tostring(err)) end
BattleTransition._worldDrawn = true
return true
end
function BattleTransition.draw()
if not BattleTransition._active then return end
if not (love and love.graphics) then return end
if BattleTransition._worldDrawn then
BattleTransition._worldDrawn = false
return
end
local G = love.graphics
G.push("all")
local R = viewFor(0, 0, DW, DH)
local fx = BattleTransition._fx
local target = G.getCanvas()
if BattleTransition._opts and BattleTransition._opts.overUi and fx and fx.def.redraw and target then
local w, h = target:getWidth(), target:getHeight()
local scratch = BattleTransition._uiScratch
if not scratch or scratch:getWidth() ~= w or scratch:getHeight() ~= h then
if scratch and scratch.release then pcall(scratch.release, scratch) end
scratch = G.newCanvas(w, h, { dpiscale = 1 })
scratch:setFilter("nearest", "nearest")
pcall(scratch.setWrap, scratch, "repeat", "repeat")
BattleTransition._uiScratch = scratch
BattleTransition._uiQuad = G.newQuad(0, 0, 1, 1, w, h)
end
G.origin()
G.setCanvas(scratch)
G.clear(0, 0, 0, 1)
G.setColor(1, 1, 1, 1)
G.draw(target, 0, 0)
G.setCanvas(target)
G.clear(0, 0, 0, 1)
R.field = scratch
R.fieldQuad = BattleTransition._uiQuad
end
local ok, err = pcall(render, R, G)
G.pop()
if not ok then print("[game3/battle_transition] " .. tostring(err)) end
end
return BattleTransition