Files
gen1recomp/src/battle/gen2/AnimObjects.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

2000 lines
57 KiB
Lua

-- The Gen 2 battle-animation OBJECT runtime.
--
-- pokegold engine/battle_anims/core.asm (the struct pool and the OAM writer),
-- engine/battle_anims/helpers.asm (GetBattleAnimFrame) and
-- engine/battle_anims/functions.asm (the 80 per-frame functions). The DATA
-- these run on -- objects, framesets, OAM sets, GFX sheets -- is extracted
-- into `data/generated/battle_anims.lua`; nothing here is transcribed by hand.
--
-- Ten structs, and one of them is picked by QueueBattleAnimation the same way
-- _InitSpriteAnimStruct picks a sprite-anim slot: the first with INDEX == 0.
-- Every field is a byte and wraps, and several functions rely on that: an
-- object walks its Y offset down past 0 into $ff.. and reads the result back
-- as a negative pixel offset.
--
-- Coordinates are hardware OAM coordinates: an OBJ at struct (x, y) draws at
-- (x - 8, y - 16) on the 160x144 screen. `xOffset`/`yOffset` are signed bytes
-- added on top, which is what every sine-driven function writes.
--
-- Two traps this file exists to get right:
--
-- * ASM fallthrough is not the same as a jumptable branch. A dozen of these
-- functions end their `.zero` case by dropping straight into `.one` on the
-- SAME frame, so an object that only ran its init would be a frame late for
-- the rest of its life. Every one of those is written out here.
-- * `ld a, [hl]` followed by `inc [hl]` leaves `a` holding the value from
-- BEFORE the increment. Dizzy's frameset flip and Perish Song's descent
-- both read the pre-increment value, and using the new one desynchronises
-- the animation from its own frameset.
--
-- Love-free on purpose: src/ui/gen2/BattleAnimView.lua turns the OAM list into
-- draw calls, and the tests step whole animations with no window at all.
local bit = require("bit")
-- The sine table is engine/math/sine.asm's `sine_table 32`, shared with the
-- overworld/intro sprite anims: BattleAnim_Sine is `calc_sine_wave
-- BattleAnimSineWave`, the same macro over the same 32-entry quarter wave.
local SpriteAnims = require("src.ui.gen2.SpriteAnims")
local AnimObjects = {}
local NUM_STRUCTS = 10 -- NUM_BATTLE_ANIM_STRUCTS
-- wShadowOAM is 40 objects; BattleAnimOAMUpdate returns carry once it is full
-- and BattleAnim_UpdateOAM_All stops walking the structs.
local OAM_LIMIT = 40
local OAM_PRIO, OAM_YFLIP, OAM_XFLIP = 0x80, 0x40, 0x20
local OAM_FLAG_MASK = 0xe0
local OAM_PAL1 = 0x10
-- BATTLEANIMSTRUCT_OAMFLAGS_FIX_COORDS_F: bit 0 of the object's flags byte
-- means "mirror this object onto the other battler's side when the enemy is
-- the one attacking".
local FIX_COORDS = 0x01
--------------------------------------------------------------------------
-- Byte arithmetic
--------------------------------------------------------------------------
local function u8(value) return value % 256 end
-- The byte read as a signed value, which is what every `bit 7, a` test and
-- every coordinate add is really doing.
local function s8(value)
value = value % 256
return value < 0x80 and value or value - 256
end
-- `sra a`: arithmetic shift right, sign preserved.
local function sra(value) return u8(math.floor(s8(value) / 2)) end
-- `swap a`
local function swap(value)
value = u8(value)
return bit.bor(bit.rshift(value, 4), bit.band(bit.lshift(value, 4), 0xf0))
end
-- `rlca`
local function rlca(value)
value = u8(value)
return u8(bit.lshift(value, 1) + bit.rshift(value, 7))
end
local sine = SpriteAnims.sine
local cosine = SpriteAnims.cosine
--------------------------------------------------------------------------
-- The struct pool
--------------------------------------------------------------------------
local Pool = {}
Pool.__index = Pool
local function newStruct()
return {
index = 0, oamFlags = 0, fixY = 0, framesetId = 0, func = 0, palette = 0,
tileId = 0, x = 0, y = 0, xOffset = 0, yOffset = 0, param = 0,
duration = 0, frame = 0xff, jt = 0, var1 = 0, var2 = 0,
}
end
-- `data` is the cache's battle_anims.lua table; `constants` the cache's
-- constants.lua (for the ordered name lists an id indexes into).
--
-- `env` is what the battle screen owns and this pool only reads:
-- env.battleTurn hBattleTurn: 0 while the player is attacking
-- env.animId wFXAnimID, the move whose script is running (KINESIS,
-- SOFTBOILED and MILK_DRINK get their own Y nudge)
-- env.ballPalette the PAL_BATTLE_OB_* name GetBallAnimPal resolves for
-- wCurItem, or nil outside a ball throw
-- env.sgb hSGB, which only Sky Attack's palette cycle reads
function AnimObjects.new(data, constants, env)
local self = setmetatable({}, Pool)
self.data = data or {}
self.env = env or {}
self.structs = {}
for slot = 1, NUM_STRUCTS do self.structs[slot] = newStruct() end
self.lastIndex = 0 -- wLastAnimObjectIndex
self.oam = {}
constants = constants or {}
self.objectOrder = constants.battleAnimObjectOrder or {}
self.framesetOrder = constants.battleAnimFramesetOrder or {}
-- Name -> numeric id, because several functions do frameset ARITHMETIC
-- (`ld a, BATTLE_ANIM_FRAMESET_SOUND_1; add [hl]`) while the extractor
-- writes names. Keeping the struct's FRAMESET_ID numeric is what makes
-- those adds mean the same thing they do on the cart.
self.framesetIds = {}
for index, name in ipairs(self.framesetOrder) do
self.framesetIds[name] = index - 1
end
-- Written by the two functions that reach past their own struct: Sky
-- Attack's OBP0 cycle and Surf's per-scanline window. The view reads them.
self.obp0 = nil
self.lyOverride = nil
return self
end
function Pool:clear()
for slot = 1, NUM_STRUCTS do self.structs[slot] = newStruct() end
self.lastIndex = 0
self.oam = {}
self.obp0 = nil
self.lyOverride = nil
end
-- BattleAnimCmd_ClearObjs. The cart's loop clears $a0 bytes from
-- wActiveAnimObjects and BATTLEANIMSTRUCT_LENGTH is $18, so it reaches six
-- whole structs plus the first sixteen bytes of the seventh -- enough to zero
-- that one's INDEX, and no further. Structs 8-10 keep running: that is the
-- documented bug (docs/bugs_and_glitches.md), and an animation that spawns
-- more than seven objects visibly depends on it.
function Pool:clearObjs()
for slot = 1, 7 do self.structs[slot] = newStruct() end
end
function Pool:framesetId(name)
local id = self.framesetIds[name]
if not id then error("unknown battle anim frameset: " .. tostring(name)) end
return id
end
-- ReinitBattleAnimFrameset: swap framesets and restart the frame walk.
local function reinit(st, framesetId)
st.framesetId = u8(framesetId)
st.duration = 0
st.frame = 0xff -- `ld [hl], -1`
end
local function deinit(st) st.index = 0 end
-- InitBattleAnimation. The object row's six bytes land in the struct in
-- order; the seventh field, TILEID, comes from the tile dict instead
-- (GetBattleAnimTileOffset), because where a sheet ended up in VRAM is a
-- property of the running script and not of the object.
function Pool:queue(objectId, x, y, param, tileOffsetFor)
local name = objectId
if type(objectId) == "number" then
name = self.objectOrder[objectId + 1] or objectId
end
local object = (self.data.objects or {})[name]
if not object then return nil end
for slot = 1, NUM_STRUCTS do
local st = self.structs[slot]
if st.index == 0 then
self.lastIndex = u8(self.lastIndex + 1)
st.index = self.lastIndex
st.oamFlags = object.flags or 0
st.fixY = object.fixY or 0
st.framesetId = self.framesetIds[object.frameset] or 0
st.func = object.func or 0
st.palette = object.palette or 0
st.tileId = tileOffsetFor and tileOffsetFor(object.gfx) or 0
st.x, st.y = u8(x), u8(y)
st.xOffset, st.yOffset = 0, 0
st.param = u8(param or 0)
st.duration = 0
st.frame = 0xff
st.jt, st.var1, st.var2 = 0, 0, 0
st.objectId = name
return st
end
end
-- QueueBattleAnimation returns carry when all ten are busy; the script does
-- not look, and neither does anything here.
return nil
end
function Pool:findByIndex(value)
for slot = 1, NUM_STRUCTS do
local st = self.structs[slot]
if st.index == value then return st end
end
return nil
end
function Pool:activeCount()
local count = 0
for slot = 1, NUM_STRUCTS do
if self.structs[slot].index ~= 0 then count = count + 1 end
end
return count
end
--------------------------------------------------------------------------
-- GetBattleAnimFrame (engine/battle_anims/helpers.asm)
--------------------------------------------------------------------------
-- What a frameset row yields: the OAM set name, or the "wait" / "delete"
-- pseudo-commands BattleAnimOAMUpdate acts on, plus the frame's flip flags.
local function frameYield(row)
local kind = row[1]
if kind == "wait" then return "wait", 0 end
if kind == "delete" then return "delete", 0 end
return row[2], row[4] or 0
end
-- `oamwait n` is not skipped here: GetBattleAnimFrame stores n as the struct's
-- duration and hands the command itself back, so the struct genuinely spends
-- n frames drawing nothing. Only BattleAnimOAMUpdate knows what to do with it.
function Pool:getFrame(st)
local frames = (self.data.framesets or {})[self.framesetOrder[st.framesetId + 1]]
if not frames then return nil, 0 end
for _ = 1, 64 do
if st.duration ~= 0 then
st.duration = st.duration - 1
local row = frames[st.frame + 1]
if not row then return nil, 0 end
return frameYield(row)
end
st.frame = u8(st.frame + 1)
local row = frames[st.frame + 1]
if not row then return nil, 0 end
local kind = row[1]
if kind == "restart" then
st.duration = 0
st.frame = 0xff
elseif kind == "end" then
-- Step back two so the next pass lands on the frame before this one and
-- then holds it forever.
st.duration = 0
st.frame = u8(st.frame - 2)
elseif kind == "delete" then
-- `oamdelete` carries no argument; the cart reads the next byte as a
-- duration anyway and then throws the whole struct away, so it does not
-- matter what lands here.
st.duration = 0
return "delete", 0
elseif kind == "wait" then
st.duration = u8(row[2])
return "wait", 0
else
st.duration = u8(row[3])
return frameYield(row)
end
end
error("battle anim frameset never yields a frame: "
.. tostring(self.framesetOrder[st.framesetId + 1]))
end
--------------------------------------------------------------------------
-- BattleAnimOAMUpdate (engine/battle_anims/core.asm)
--------------------------------------------------------------------------
-- AddOrSubtractY / AddOrSubtractX: a flipped entry mirrors around its own
-- 8-pixel cell, which is `-(offset + 8)`.
local function mirror(value, flip)
if not flip then return value end
return u8(-(u8(value) + 8))
end
-- InitBattleAnimBuffer. On the enemy's turn the whole object is reflected
-- onto the other side of the field -- but only if its OAMFLAGS ask for it.
function Pool:initBuffer(st)
local buf = {
oamFlags = bit.band(st.oamFlags, OAM_PRIO),
palette = st.palette,
tileId = st.tileId,
x = st.x, y = st.y,
xOffset = st.xOffset, yOffset = st.yOffset,
}
if (self.env.battleTurn or 0) == 0 then return buf end
buf.oamFlags = st.oamFlags
if bit.band(st.oamFlags, FIX_COORDS) == 0 then return buf end
-- x' = (-10 tiles + 4) - x: reflected about the middle of the field.
buf.x = u8((-10 * 8 + 4) - st.x)
if st.fixY == 0xff then
buf.y = u8(5 * 8 + st.y)
else
local y = u8(st.fixY - st.y)
local animId = self.env.animId
-- The three self-targeting animations whose object sits one tile higher
-- on the enemy's side.
if animId == "KINESIS" or animId == "SOFTBOILED" or animId == "MILK_DRINK" then
y = u8(y - 8)
end
buf.y = y
end
buf.xOffset = u8(-st.xOffset)
return buf
end
-- One struct's OAM entries appended to self.oam. Returns true once the
-- 40-object shadow OAM is full, which is the carry the caller stops on.
function Pool:updateOam(st)
local buf = self:initBuffer(st)
local oamsetName, frameFlags = self:getFrame(st)
if oamsetName == "wait" or oamsetName == nil then return false end
if oamsetName == "delete" then
deinit(st)
return false
end
buf.oamFlags = bit.band(bit.bxor(frameFlags, buf.oamFlags), OAM_FLAG_MASK)
local set = (self.data.oamsets or {})[oamsetName]
if not set then return false end
local tileId = u8(buf.tileId + (set.vtile or 0))
local yFlip = bit.band(buf.oamFlags, OAM_YFLIP) ~= 0
local xFlip = bit.band(buf.oamFlags, OAM_XFLIP) ~= 0
for _, entry in ipairs(set.sprites or {}) do
if #self.oam >= OAM_LIMIT then return true end
-- GetSpriteOAMAttr: the frame's flip/priority flags toggle the entry's;
-- OAM_PAL1 passes through from the entry, and the palette slot comes from
-- the struct.
local attr = bit.band(bit.bxor(entry.attr or 0, buf.oamFlags), OAM_FLAG_MASK)
attr = attr + bit.band(entry.attr or 0, OAM_PAL1)
self.oam[#self.oam + 1] = {
y = u8(buf.y + buf.yOffset + mirror(entry.y or 0, yFlip)),
x = u8(buf.x + buf.xOffset + mirror(entry.x or 0, xFlip)),
-- BATTLEANIM_BASE_TILE is added here on the cart and subtracted again by
-- every sheet lookup, so the port keeps tiles in sheet-relative space.
tile = u8(tileId + (entry.tile or 0)),
attr = attr,
palette = buf.palette,
}
end
return false
end
-- BattleAnim_UpdateOAM_All: run every live struct's function, then let it
-- write its OAM entries. A struct that deinitialises itself inside its
-- function still draws this frame, because the ASM calls BattleAnimOAMUpdate
-- unconditionally.
function Pool:playFrame()
self.oam = {}
for slot = 1, NUM_STRUCTS do
local st = self.structs[slot]
if st.index ~= 0 then
local fn = AnimObjects.FUNCTIONS[st.func]
if fn then fn(self, st) end
if self:updateOam(st) then break end
end
end
return self.oam
end
--------------------------------------------------------------------------
-- engine/battle_anims/functions.asm
--------------------------------------------------------------------------
local function incJt(st) st.jt = u8(st.jt + 1) end
-- BattleAnim_StepToTarget: inches the object toward the opponent's side, half
-- as far vertically as horizontally. The `dec [hl]` loop runs BEFORE `dec e`
-- is tested, so a vertical step of 0 walks the Y coordinate 256 times -- right
-- back where it started, which is the point.
local function stepToTarget(st, speed)
local e = bit.band(speed, 0xf)
st.x = u8(st.x + e)
local steps = bit.rshift(e, 1)
st.y = u8(st.y - (steps == 0 and 256 or steps))
end
-- BattleAnim_StepCircle: circular movement whose height is a quarter of its
-- width.
local function stepCircle(st, angle, radius)
st.yOffset = sra(sra(sine(angle, radius)))
st.xOffset = cosine(angle, radius)
end
-- A 16-bit accumulator spread over two byte fields, which is how every
-- sub-pixel movement here is done: the HIGH byte is the pixel coordinate and
-- the LOW byte the fraction.
local function add16(high, low, delta)
local value = (u8(high) * 256 + u8(low) + delta) % 0x10000
return math.floor(value / 256), value % 256
end
local F = {}
F.BATTLE_ANIM_FUNC_NULL = function(_, st)
-- anim_incobj is what walks this one to `.one`, which deletes the object.
if st.jt ~= 0 then deinit(st) end
end
-- BattleAnimFunction_ThrowFromUserToTarget: right 2 and up 1 a frame, with the
-- object's PARAM as the amplitude of a sine on the Y offset. Returns true for
-- "still going", which is the carry the AndDisappear wrapper reads.
local function throwToTarget(st)
if st.x >= 0x88 then return false end
st.x = u8(st.x + 2)
st.y = u8(st.y - 1)
local angle = st.var1
st.var1 = u8(st.var1 - 1)
st.yOffset = sine(angle, st.param)
return true
end
F.BATTLE_ANIM_FUNC_THROW_TO_TARGET = function(_, st) throwToTarget(st) end
F.BATTLE_ANIM_FUNC_THROW_TO_TARGET_DISAPPEAR = function(_, st)
if not throwToTarget(st) then deinit(st) end
end
F.BATTLE_ANIM_FUNC_WAVE_TO_TARGET = function(_, st)
if st.x >= 0x88 then
deinit(st)
return
end
st.x = u8(st.x + 2)
st.y = u8(st.y - 1)
local angle = st.var1
st.var1 = u8(st.var1 + 4)
st.yOffset = sine(angle, 0x10)
-- The X offset is the cosine divided by sixteen, so the wave is much
-- flatter across than it is up.
st.xOffset = sra(sra(sra(sra(cosine(angle, 0x10)))))
end
F.BATTLE_ANIM_FUNC_MOVE_IN_CIRCLE = function(_, st)
if st.jt == 0 then
incJt(st)
-- Bit 7 of PARAM starts the object half a turn round; the rest is the
-- radius, so the flag has to come off before it is used as one.
st.var1 = bit.band(st.param, 0x80) ~= 0 and 0x20 or 0
st.param = bit.band(st.param, 0x7f)
end
local angle = st.var1
st.yOffset = sine(angle, st.param)
st.xOffset = cosine(angle, st.param)
st.var1 = u8(st.var1 + 1)
end
F.BATTLE_ANIM_FUNC_USER_TO_TARGET = function(_, st)
if st.jt ~= 0 then
deinit(st)
return
end
if st.x >= 0x84 then return end
stepToTarget(st, st.param)
end
F.BATTLE_ANIM_FUNC_USER_TO_TARGET_DISAPPEAR = function(_, st)
if st.x >= 0x84 then
deinit(st)
return
end
stepToTarget(st, st.param)
end
-- GetBallAnimPal: the thrown ball wears the colour of the ball being thrown
-- (data/battle_anims/ball_colors.asm). The battle screen resolves that for
-- wCurItem and hands it over as env.ballPalette.
local function ballPal(self, st)
if self.env.ballPalette then st.palette = self.env.ballPalette end
end
-- .four: the ball bounces on a shrinking sine while VAR2 steps down by four;
-- when it reaches zero the ball opens.
local function pokeballBounce(self, st)
st.yOffset = sine(st.var1, st.var2)
st.var1 = u8(st.var1 - 1)
if bit.band(st.var1, 0x1f) ~= 0 then return end
-- `ld [hl], a` after the mask: VAR1 is zeroed, not just left on a boundary,
-- so every bounce starts from the same phase.
st.var1 = 0
st.var2 = u8(st.var2 - 4)
if st.var2 ~= 0 then return end
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_POKE_BALL_4"))
incJt(st)
end
-- .eight/.ten: the same sine, but every $10 steps the jumptable advances, so
-- the caught / broke-free branch is only ever reached on a shake boundary.
local function pokeballWobble(_, st)
st.yOffset = sine(st.var1, st.var2)
st.var1 = u8(st.var1 - 1)
if bit.band(st.var1, 0x1f) == 0 then
deinit(st)
return
end
if bit.band(st.var1, 0xf) ~= 0 then return end
incJt(st)
end
F.BATTLE_ANIM_FUNC_POKEBALL = function(self, st)
local jt = st.jt
if jt == 0 then
ballPal(self, st)
incJt(st)
elseif jt == 1 then
if throwToTarget(st) then return end
-- The arc's Y offset is folded into the coordinate before the ball
-- switches to its opening frameset, so it lands where it fell.
st.y = u8(st.y + st.yOffset)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_POKE_BALL_3"))
incJt(st)
elseif jt == 3 then
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_POKE_BALL_1"))
st.var1, st.var2 = 0, 0x10
pokeballBounce(self, st) -- .three falls into .four
elseif jt == 4 then
pokeballBounce(self, st)
elseif jt == 6 then
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_POKE_BALL_5"))
st.jt = u8(st.jt - 1)
elseif jt == 7 then
ballPal(self, st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_POKE_BALL_2"))
incJt(st)
st.var2 = 0x20
pokeballWobble(self, st) -- .seven falls into .eight
elseif jt == 8 or jt == 10 then
pokeballWobble(self, st)
elseif jt == 11 then
deinit(st)
end
end
local function pokeballBlockedFall(_, st)
if st.y >= 0x80 then
deinit(st)
return
end
st.y = u8(st.y + 4)
st.x = u8(st.x - 2)
end
F.BATTLE_ANIM_FUNC_POKEBALL_BLOCKED = function(self, st)
local jt = st.jt
if jt == 0 then
ballPal(self, st)
incJt(st)
elseif jt == 1 then
if st.x < 0x70 then
throwToTarget(st)
return
end
incJt(st)
pokeballBlockedFall(self, st) -- .next falls into .two
elseif jt == 2 then
pokeballBlockedFall(self, st)
end
end
F.BATTLE_ANIM_FUNC_EMBER = function(self, st)
local jt = st.jt
if jt == 0 then
-- The upper nybble of PARAM picks which branch this object runs.
st.jt = bit.band(swap(st.param), 0xf)
elseif jt == 1 then
if st.x >= 0x88 then return end
stepToTarget(st, st.param)
elseif jt == 2 then
deinit(st)
elseif jt == 3 then
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_FLAMETHROWER"))
end
end
F.BATTLE_ANIM_FUNC_DROP = function(_, st)
if st.jt == 0 then
incJt(st)
st.var1, st.var2 = 0x30, 0x48
end
st.yOffset = sine(st.var1, st.var2)
st.var1 = u8(st.var1 + 1)
if bit.band(st.var1, 0x3f) ~= 0 then return end
st.var1 = 0x20
-- Each bounce loses PARAM off the amplitude; once that would go to zero or
-- below, the object is done.
local left = st.var2 - st.param
if left <= 0 then
deinit(st)
return
end
st.var2 = left
end
F.BATTLE_ANIM_FUNC_USER_TO_TARGET_SPIN = function(_, st)
-- .SetCoords: the lower nybble of PARAM is the horizontal step, half of it
-- the vertical one.
local function setCoords()
local e = bit.band(st.param, 0xf)
st.x = u8(st.x + e)
local steps = bit.rshift(e, 1)
st.y = u8(st.y - (steps == 0 and 256 or steps))
end
-- .two: a circle whose top is flattened -- the cosine is pulled down by its
-- own radius and halved.
local function orbit()
if st.var1 < 0x40 then
st.yOffset = sra(u8(cosine(st.var1, 0x18) - 0x18))
st.xOffset = sine(st.var1, 0x18)
st.var1 = u8(st.var1 + bit.band(st.param, 0xf))
return
end
-- .loop_back: the upper nybble is a lap counter.
local laps = bit.band(st.param, 0xf0)
if laps == 0 then
incJt(st) -- .finish falls into .three
if st.x >= 0xb0 then
deinit(st)
else
setCoords()
end
return
end
st.param = bit.band(st.param, 0xf) + (laps - 0x10)
st.jt = u8(st.jt - 1)
end
local jt = st.jt
if jt == 0 then
if st.x < 0x80 then
setCoords()
return
end
-- .next -> .one -> .two, all on this frame.
incJt(st)
incJt(st)
st.var1 = 0
orbit()
elseif jt == 1 then
incJt(st)
st.var1 = 0
orbit()
elseif jt == 2 then
orbit()
elseif jt == 3 then
if st.x >= 0xb0 then
deinit(st)
return
end
setCoords()
end
end
F.BATTLE_ANIM_FUNC_SHAKE = function(_, st)
-- .done_one: hold for the upper nybble of PARAM, then jump to the other side.
local function flip()
st.var1 = bit.band(swap(st.param), 0xf)
st.xOffset = u8(-st.xOffset)
end
local jt = st.jt
if jt == 0 then
incJt(st)
st.var1 = 0
st.xOffset = bit.band(st.param, 0xf)
flip() -- .zero falls into .one, and VAR1 is 0, so .done_one runs at once
elseif jt == 1 then
if st.var1 ~= 0 then
st.var1 = st.var1 - 1
return
end
flip()
elseif jt == 2 then
deinit(st)
end
end
F.BATTLE_ANIM_FUNC_FIRE_BLAST = function(self, st)
-- .eight: the travelling flame spirals once it arrives.
local function spin()
local angle = st.var1
st.yOffset = sine(angle, 0x10)
st.xOffset = cosine(angle, 0x10)
st.var1 = u8(st.var1 + 1)
end
-- .seven: straight across, then hand over to the spiral.
local function travel()
if st.x < 0x88 then
st.x = u8(st.x + 2)
st.y = u8(st.y - 1)
return
end
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_EMBER"))
spin() -- .set_up_eight falls into .eight
end
local jt = st.jt
if jt == 0 then
-- PARAM picks the branch outright: 7 is the flame that travels, and the
-- rest are the five arms of the blast, which only drift.
st.jt = st.param
if st.param == 7 then
travel()
else
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_BURNED"))
end
elseif jt == 1 then
st.yOffset = u8(st.yOffset - 1)
elseif jt == 2 then
st.xOffset = u8(st.xOffset - 1)
elseif jt == 3 then
st.xOffset = u8(st.xOffset + 1)
elseif jt == 4 then
st.yOffset = u8(st.yOffset + 1)
st.xOffset = u8(st.xOffset - 1)
elseif jt == 5 then
st.yOffset = u8(st.yOffset + 1)
st.xOffset = u8(st.xOffset + 1)
elseif jt == 7 then
travel()
elseif jt == 8 then
spin()
elseif jt == 9 then
deinit(st)
end
end
-- BattleAnim_ScatterHorizontal: a 16-bit per-frame X step picked from the
-- object's PARAM, so a screenful of leaves fans out instead of moving as one.
local function scatterHorizontal(st)
local param = st.param
if bit.band(param, 0x80) == 0 then
if param >= 0x20 then return 0x100 end
if param >= 0x18 then return 0x180 end
return 0x200
end
local masked = bit.band(param, 0x3f)
if masked >= 0x20 then return -0x100 end
if masked >= 0x18 then return -0x180 end
return -0x200
end
F.BATTLE_ANIM_FUNC_RAZOR_LEAF = function(self, st)
local function arcStep()
local radius = bit.band(st.param, 0x3f)
local angle = st.var1
st.var1 = u8(st.var1 - 1)
st.yOffset = sine(angle, radius)
st.x, st.var2 = add16(st.x, st.var2, scatterHorizontal(st))
end
local function arcOrLand()
if st.var1 >= 0x30 then
arcStep()
return
end
incJt(st)
st.var1, st.var2 = 0, 0
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_RAZOR_LEAF_2"))
-- Bit 6 starts the second frameset six frames in, which is the leaf
-- already half-turned.
if bit.band(st.param, 0x40) ~= 0 then st.frame = 5 end
end
local jt = st.jt
if jt == 0 then
incJt(st)
st.var1 = 0x40
arcOrLand() -- .zero falls into .one
elseif jt == 1 then
arcOrLand()
elseif jt == 2 then
if st.yOffset == 0x20 then
deinit(st)
return
end
st.xOffset = sine(st.var1, 0x10)
if bit.band(st.param, 0x40) ~= 0 then
st.var1 = u8(st.var1 - 1)
else
st.var1 = u8(st.var1 + 1)
end
st.yOffset, st.var2 = add16(st.yOffset, st.var2, 0x80)
elseif jt == 3 then
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_RAZOR_LEAF_1"))
st.oamFlags = bit.band(st.oamFlags, 0xff - OAM_XFLIP)
incJt(st)
elseif jt >= 4 and jt <= 7 then
incJt(st)
elseif jt == 8 then
if st.x >= 0xc0 then return end
stepToTarget(st, 8)
end
end
F.BATTLE_ANIM_FUNC_ROCK_SMASH = function(self, st)
if st.jt == 0 then
-- Bit 6 picks between the two rock framesets.
st.framesetId = u8(rlca(rlca(bit.band(st.param, 0x40)))
+ self:framesetId("BATTLE_ANIM_FRAMESET_BIG_ROCK"))
incJt(st)
st.var1 = 0x40
end
if st.var1 < 0x30 then
deinit(st)
return
end
local radius = bit.band(st.param, 0x3f)
local angle = st.var1
st.var1 = u8(st.var1 - 1)
st.yOffset = sine(angle, radius)
st.x, st.var2 = add16(st.x, st.var2, scatterHorizontal(st))
end
F.BATTLE_ANIM_FUNC_BUBBLE = function(self, st)
local jt = st.jt
if jt == 0 then
incJt(st)
st.var1 = 0xc
jt = 1
end
if jt == 1 then
if st.var1 ~= 0 then
st.var1 = st.var1 - 1
stepToTarget(st, st.param)
return
end
incJt(st)
st.var1 = 0
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_PULSING_BUBBLE"))
return
end
if jt ~= 2 then return end
if st.x < 0x98 then
st.x, st.var1 = add16(st.x, st.var1, 0x60)
end
if st.y < 0x20 then return end
-- The upper nybble of PARAM is a per-frame rise; `ld d, $ff` is what makes
-- it a NEGATIVE 16-bit step.
st.y, st.var2 = add16(st.y, st.var2, bit.band(st.param, 0xf0) - 0x100)
end
-- BattleAnimFunction_Surf: the wave is a per-scanline SCY override, so the
-- port hands the view the same window the cart writes to hLYOverrideStart /
-- hLYOverrideEnd rather than moving anything itself.
F.BATTLE_ANIM_FUNC_SURF = function(self, st)
local jt = st.jt
if jt == 0 then
incJt(st)
self.lyOverride = { register = "SCY", first = 0x58, last = 0x5e }
return
end
if jt == 1 then
if st.y < st.param then
incJt(st)
if self.lyOverride then self.lyOverride.first = 0 end
return
end
st.y = u8(st.y - 1)
st.yOffset = sine(st.var1, 0x10)
local top = st.yOffset + st.y - 0x10
-- `ret c`: the wave stops climbing entirely on the frames the subtraction
-- underflows, offsets and all.
if top < 0 then return end
if self.lyOverride then self.lyOverride.first = u8(top) end
st.xOffset = bit.band(st.xOffset + 1, 7)
st.var1 = u8(st.var1 + 2)
return
end
if jt == 3 then
if st.y >= 0x70 then
self.lyOverride = nil
deinit(st)
return
end
st.y = u8(st.y + 2)
local top = st.y - 0x10
if top < 0 then return end
if self.lyOverride then self.lyOverride.first = u8(top) end
return
end
if jt == 4 then deinit(st) end
end
F.BATTLE_ANIM_FUNC_SING = function(self, st)
if st.jt == 0 then
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_MUSIC_NOTE_1") + st.param)
end
if st.x >= 0xb8 then
deinit(st)
return
end
stepToTarget(st, 2)
local angle = st.var1
st.var1 = u8(st.var1 - 1)
st.yOffset = sine(angle, 8)
end
F.BATTLE_ANIM_FUNC_WATER_GUN = function(self, st)
local jt = st.jt
if jt == 0 then
incJt(st)
jt = 1
end
if jt == 1 then
if st.y >= 0x30 then
stepToTarget(st, 2)
local angle = st.var1
st.var1 = u8(st.var1 - 1)
st.yOffset = sine(angle, 8)
return
end
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_WATER_GUN_2"))
st.yOffset = 0
st.y = 0x30
-- Everything but FIX_COORDS is dropped, so the splash never flips.
st.oamFlags = bit.band(st.oamFlags, FIX_COORDS)
jt = 2
end
if jt ~= 2 then return end
if st.yOffset >= 0x18 then
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_WATER_GUN_3"))
return
end
st.yOffset = u8(st.yOffset + 1)
end
F.BATTLE_ANIM_FUNC_POWDER = function(_, st)
if st.yOffset >= 0x38 then
deinit(st)
return
end
st.yOffset, st.var1 = add16(st.yOffset, st.var1, 0x80)
-- Shakes sixteen pixels either side by toggling one bit.
st.xOffset = bit.bxor(st.xOffset, 0x10)
end
F.BATTLE_ANIM_FUNC_RECOVER = function(_, st)
if st.jt == 0 then
incJt(st)
st.var2 = bit.band(st.param, 0xf0) -- radius
st.var1 = u8(bit.band(st.param, 0xf) * 8) -- starting angle
st.param = 1 -- reused as an every-other-frame toggle
end
if st.var2 == 0 then
deinit(st)
return
end
local angle = st.var1
st.var1 = u8(st.var1 + 1)
st.yOffset = sine(angle, st.var2)
st.xOffset = cosine(angle, st.var2)
st.param = bit.bxor(st.param, 1)
if st.param == 0 then return end
st.var2 = u8(st.var2 - 1)
end
F.BATTLE_ANIM_FUNC_THUNDER_WAVE = function(self, st)
if st.jt == 1 then
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_THUNDER_WAVE_EXTRA"))
elseif st.jt == 3 then
deinit(st)
end
end
-- Clamp/Encore: two halves clap together, twice. The frameset the object
-- switches to is the base or the base + 1 (CLAMP_FLIPPED / ENCORE_HAND_
-- FLIPPED), picked by the SIGN of the sine, so both halves close together.
F.BATTLE_ANIM_FUNC_CLAMP_ENCORE = function(self, st)
local function step()
local value = sine(st.var1, st.param)
st.xOffset = value
reinit(st, bit.band(value, 0x80) ~= 0 and st.var2 or u8(st.var2 + 1))
st.var1 = u8(st.var1 + 1)
if bit.band(st.var1, 0x1f) ~= 0 then return end
incJt(st) -- falls into .two, which is a bare IncAnonJumptableIndex
end
local jt = st.jt
if jt == 0 then
incJt(st)
st.var2 = st.framesetId
st.var1 = bit.band(st.param, 0x80) ~= 0 and 0x30 or 0x10
st.param = bit.band(st.param, 0x7f)
step()
elseif jt == 1 then
step()
elseif jt >= 2 and jt <= 5 then
incJt(st)
elseif jt == 6 then
st.jt = 1
end
end
F.BATTLE_ANIM_FUNC_BITE = function(self, st)
local function step()
local value = sine(st.var1, st.param)
st.yOffset = value
reinit(st, bit.band(value, 0x80) ~= 0
and self:framesetId("BATTLE_ANIM_FRAMESET_BITE_1")
or self:framesetId("BATTLE_ANIM_FRAMESET_BITE_2"))
st.var1 = u8(st.var1 + 2)
if bit.band(st.var1, 0x1f) ~= 0 then return end
incJt(st)
end
local jt = st.jt
if jt == 0 then
incJt(st)
st.var1 = bit.band(st.param, 0x80) ~= 0 and 0x30 or 0x10
st.param = bit.band(st.param, 0x7f)
step()
elseif jt == 1 then
step()
elseif jt >= 2 and jt <= 5 then
incJt(st)
elseif jt == 6 then
st.jt = 1
end
end
F.BATTLE_ANIM_FUNC_SOLAR_BEAM = function(_, st)
if st.jt == 0 then
incJt(st)
st.var1, st.var2 = 0x28, 0
end
local angle = st.param
st.yOffset = sine(angle, st.var1)
st.xOffset = cosine(angle, st.var1)
if st.var1 == 0 then
deinit(st)
return
end
-- The radius is a 16-bit value shrinking half a pixel a frame.
st.var1, st.var2 = add16(st.var1, st.var2, -0x80)
end
-- The gust's radius comes from a nine-entry table, so the whirl pulses rather
-- than turning at a constant width.
local GUST_OFFSETS = { [0] = 8, [1] = 6, [2] = 5, [3] = 4, [4] = 5, [5] = 6,
[6] = 8, [7] = 12, [8] = 16 }
F.BATTLE_ANIM_FUNC_GUST = function(_, st)
local function wobble()
local radius = GUST_OFFSETS[st.var2] or 8
local angle = st.var1
-- Height is a sixteenth of the width, plus PARAM's own drift.
st.yOffset = u8(sra(sra(sra(sra(sine(angle, radius))))) + st.param)
st.xOffset = cosine(angle, radius)
st.var1 = u8(st.var1 - 8)
-- PARAM counts DOWN from 0 through $ff; once it drops below $c2 the whirl
-- settles back to the middle.
if st.param ~= 0 and st.param < 0xc2 then
st.var2, st.param, st.xOffset, st.yOffset = 0, 0, 0, 0
return
end
st.param = u8(st.param - 1)
if bit.band(st.param, 7) ~= 0 then return end
st.var2 = u8(st.var2 + 1)
end
local function move()
wobble()
st.x = u8(st.x + 1)
if bit.band(st.x, 1) ~= 0 then return end
st.y = u8(st.y - 1)
end
local jt = st.jt
if jt == 0 then
incJt(st)
st.param = 0
wobble() -- .zero falls into .one
elseif jt == 1 or jt == 3 then
wobble()
elseif jt == 2 then
if st.x < 0x88 then
move()
else
incJt(st)
end
elseif jt == 4 then
if st.x < 0xb8 then
move()
else
deinit(st)
end
end
end
F.BATTLE_ANIM_FUNC_ABSORB = function(_, st)
if st.x < 0x30 then
deinit(st)
return
end
local e = bit.band(st.param, 0xf)
st.x = u8(st.x - e)
local steps = bit.rshift(e, 1)
st.y = u8(st.y + (steps == 0 and 256 or steps))
end
F.BATTLE_ANIM_FUNC_WRAP = function(_, st)
-- anim_incobj walks the frameset one step along the BIND_1..4 run.
if st.jt ~= 1 then return end
reinit(st, u8(st.framesetId + 1))
incJt(st)
st.var1 = 8
end
-- BattleAnim_StepThrownToTarget: a parabola whose horizontal step is PARAM's
-- two nybbles read as a 16-bit fixed-point number -- the LOW nybble is the
-- fraction and the HIGH nybble the whole pixels, which is the reverse of how
-- the macro's argument reads.
local function stepThrownToTarget(st)
st.var2 = u8(st.var2 - 1)
st.yOffset = sine(st.var2, 0x20)
st.fixY = u8(st.fixY + 2)
local step = bit.rshift(bit.band(st.param, 0xf0), 4) * 256
+ swap(bit.band(st.param, 0xf))
st.x, st.var1 = add16(st.x, st.var1, step)
if bit.band(st.var2, 1) ~= 0 then return end
st.y = u8(st.y - 1)
end
F.BATTLE_ANIM_FUNC_LEECH_SEED = function(self, st)
local jt = st.jt
if jt == 0 then
incJt(st)
st.var2 = 0x40
elseif jt == 1 then
if st.var2 >= 0x20 then
stepThrownToTarget(st)
return
end
st.var2 = 0x40
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_LEECH_SEED_2"))
incJt(st)
elseif jt == 2 then
if st.var2 ~= 0 then
st.var2 = st.var2 - 1
return
end
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_LEECH_SEED_3"))
end
end
F.BATTLE_ANIM_FUNC_SPIKES = function(_, st)
local jt = st.jt
if jt == 0 then
incJt(st)
st.var2 = 0x40
elseif jt == 1 then
if st.var2 >= 0x20 then
stepThrownToTarget(st)
return
end
incJt(st)
end
end
F.BATTLE_ANIM_FUNC_RAZOR_WIND = function(self, st)
F.BATTLE_ANIM_FUNC_MOVE_IN_CIRCLE(self, st)
-- Fifteen extra steps a frame, so the object races round the circle.
st.var1 = u8(st.var1 + 0xf)
end
local function kickRoll(_, st)
if st.x >= 0x98 then return end
st.x = u8(st.x + 2)
local angle = st.var1
st.var1 = u8(st.var1 + 1)
st.yOffset = sine(angle, 8)
end
F.BATTLE_ANIM_FUNC_KICK = function(_, st)
local jt = st.jt
if jt == 1 then
if st.y < 0x30 then
st.y = u8(st.y + 4)
return
end
st.jt = 0
elseif jt == 2 then
if st.x >= 0x98 then return end
st.x = u8(st.x + 2)
-- The kick pins itself to the target's side and holds one frame.
st.oamFlags = bit.bor(st.oamFlags, FIX_COORDS)
st.fixY = 0x90
st.frame = 0
st.duration = 2
st.y = u8(st.y - 1)
elseif jt == 3 then
incJt(st)
st.var1 = 0x2c
st.frame = 0
st.duration = 0x80
kickRoll(_, st) -- .three falls into .four
elseif jt == 4 then
kickRoll(_, st)
end
end
F.BATTLE_ANIM_FUNC_EGG = function(self, st)
-- .EggVerticalWaveMotion, shared by both openings.
local function wave()
st.yOffset = sine(st.var1, st.var2)
st.var1 = u8(st.var1 + 1)
if bit.band(st.var1, 0x3f) ~= 0 then return end
st.var1 = 0x20
st.var2 = u8(st.var2 - 8)
if st.var2 ~= 0 then return end
st.var1, st.var2 = 0, 0
incJt(st)
end
-- .egg_bomb_step: the egg drifts up half a pixel a frame while it travels.
local function step()
st.x = u8(st.x + 1)
st.y, st.var1 = add16(st.y, st.var1, -0x80)
end
local jt = st.jt
if jt == 0 then
-- The object starts here and then jumps to whichever branch PARAM names,
-- which is how one object serves both Egg Bomb and Softboiled.
st.var1, st.var2 = 0x28, 0x10
st.jt = st.param
elseif jt == 1 then
if st.x < 0x40 then st.x = u8(st.x + 1) end
wave()
elseif jt == 2 then
if st.x >= 0x88 then
incJt(st)
incJt(st) -- .egg_bomb_done skips straight to .four
return
end
if bit.band(st.x, 0xf) ~= 0 then
step()
return
end
st.var2 = 0x10
incJt(st)
elseif jt == 3 then
if st.var2 ~= 0 then
st.var2 = st.var2 - 1
return
end
st.jt = u8(st.jt - 1)
step()
elseif jt == 5 then
deinit(st)
elseif jt == 6 then
if st.x < 0x4b then st.x = u8(st.x + 1) end
wave()
elseif jt == 7 then
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_EGG_WOBBLE"))
incJt(st)
elseif jt == 8 then
local angle = st.var1
st.var1 = u8(st.var1 + 2)
st.xOffset = sine(angle, 2)
elseif jt == 9 then
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_EGG_CRACKED_BOTTOM"))
st.yOffset = 4
incJt(st)
elseif jt == 11 then
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_EGG_CRACKED_TOP"))
incJt(st)
st.var1 = 0x40
elseif jt == 12 then
st.yOffset = sine(st.var1, 0x20)
if st.var1 < 0x30 then
incJt(st)
return
end
st.var1 = u8(st.var1 - 1)
end
end
F.BATTLE_ANIM_FUNC_MOVE_UP = function(_, st)
-- Runs while the offset is 0 or already past $d8 going negative; anything
-- in between is "far enough up" and ends the object.
if st.yOffset ~= 0 and st.yOffset < 0xd8 then
deinit(st)
return
end
st.yOffset = u8(st.yOffset - st.param)
end
F.BATTLE_ANIM_FUNC_SOUND = function(self, st)
local function motion()
local angle = st.var2
st.var2 = u8(st.var2 + 2)
local value = sine(angle, 0x10)
st.xOffset = value
if st.param == 0 then
st.yOffset = u8(-value)
elseif st.param ~= 1 then
st.yOffset = value
end
-- PARAM 1 leaves the Y offset alone: that is the flat sideways wave.
end
if st.jt == 0 then
if (self.env.battleTurn or 0) ~= 0 then
-- `xor $ff; add $3` is 2 - param: the enemy's three angles are the
-- player's three mirrored, 0 <-> 2.
st.param = u8(2 - st.param)
end
incJt(st)
st.var1 = 8
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_SOUND_1") + st.param)
return
end
if st.var1 == 0 then
deinit(st)
return
end
st.var1 = st.var1 - 1
motion()
end
F.BATTLE_ANIM_FUNC_CONFUSE_RAY = function(self, st)
if st.jt == 0 then
incJt(st)
st.var2 = bit.band(st.param, 0x3f)
-- Bit 7 becomes both the frameset offset and, once swapped, the radius.
st.param = rlca(bit.band(st.param, 0x80))
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_CONFUSE_RAY_1") + st.param)
return
end
local radius = swap(st.param)
local angle = st.var2
st.var2 = u8(st.var2 + 1)
st.yOffset = sine(angle, radius)
st.xOffset = cosine(angle, radius)
if st.x >= 0x80 then return end
-- Both tests read the NEW VAR2, and the second `and $1` is applied to what
-- the first `and $3` left, not to the register again.
local phase = bit.band(st.var2, 3)
if phase == 0 then st.y = u8(st.y - 1) end
if bit.band(phase, 1) ~= 0 then return end
st.x = u8(st.x + 1)
end
F.BATTLE_ANIM_FUNC_DIZZY = function(_, st)
if st.jt == 0 then
incJt(st)
st.var1 = st.framesetId
reinit(st, u8(st.var1 + rlca(bit.band(st.param, 0x80))))
st.param = bit.band(st.param, 0x7f)
end
local angle = st.param
st.yOffset = sra(sra(sine(angle, 0x10)))
st.xOffset = cosine(angle, 0x10)
-- `ld a, [hl]` then `inc [hl]`: the frameset flip tests the PRE-increment
-- angle, so the two chick frames swap on the same beat as the circle.
st.param = u8(st.param + 1)
local phase = bit.band(angle, 0x3f)
if phase == 0 then
reinit(st, st.var1)
elseif bit.band(phase, 0x1f) == 0 then
reinit(st, u8(st.var1 + 1))
end
end
-- Hardcoded Y offsets, one per PARAM.
local AMNESIA_OFFSETS = { [0] = 0xec, [1] = 0xf8, [2] = 0x00 }
F.BATTLE_ANIM_FUNC_AMNESIA = function(self, st)
if st.jt == 0 then
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_AMNESIA_1") + st.param)
st.yOffset = AMNESIA_OFFSETS[st.param] or 0
elseif st.jt == 2 then
-- anim_incobj forces the object to deinit; Present is what uses it.
deinit(st)
end
end
F.BATTLE_ANIM_FUNC_FLOAT_UP = function(_, st)
local angle = st.var1
st.var1 = u8(st.var1 + 2)
st.xOffset = sine(angle, 4)
-- `lb hl, -1, $a0` is the 16-bit constant $ffa0: up 3/8 of a pixel a frame.
st.yOffset, st.var2 = add16(st.yOffset, st.var2, -0x60)
end
F.BATTLE_ANIM_FUNC_DIG = function(_, st)
local angle = st.var1
st.var1 = u8(st.var1 - 2)
st.yOffset = sine(angle, 0x10)
st.x = u8(st.x + 1)
end
F.BATTLE_ANIM_FUNC_STRING = function(self, st)
if st.jt ~= 0 then return end
incJt(st)
-- PARAM 0 is the one that flips on the enemy's turn.
if st.param == 0 then st.oamFlags = bit.bor(st.oamFlags, OAM_YFLIP) end
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_STRING_SHOT_1") + st.param)
end
F.BATTLE_ANIM_FUNC_PARALYZED = function(self, st)
if st.jt == 0 then
incJt(st)
st.var1 = 0
local param = st.param
-- Bits 4-6 become the hold time; bit 7 flips the object, and the low
-- nybble is how far it jitters.
st.param = bit.band(swap(bit.band(param, 0x70)), 0xf)
if bit.band(param, 0x80) == 0 then
st.xOffset = bit.band(param, 0xf)
else
st.xOffset = u8(-bit.band(param, 0xf))
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_PARALYZED_FLIPPED"))
end
return
end
if st.var1 ~= 0 then
st.var1 = st.var1 - 1
return
end
st.var1 = st.param
st.xOffset = u8(-st.xOffset)
end
-- A shared descent: a circle whose height is an eighth of its width, sinking
-- one pixel every few frames until it is $28 down. Spiral Descent checks
-- every eight frames, Petal Dance every four.
local function spiralDescent(st, mask)
local angle = st.var1
st.yOffset = u8(sra(sra(sra(sine(angle, 0x18)))) + st.var2)
st.xOffset = cosine(angle, 0x18)
st.var1 = u8(st.var1 + 1)
if bit.band(st.var1, mask) ~= 0 then return end
if st.var2 >= 0x28 then
deinit(st)
return
end
st.var2 = u8(st.var2 + 1)
end
F.BATTLE_ANIM_FUNC_SPIRAL_DESCENT = function(_, st) spiralDescent(st, 7) end
F.BATTLE_ANIM_FUNC_PETAL_DANCE = function(_, st) spiralDescent(st, 3) end
F.BATTLE_ANIM_FUNC_POISON_GAS = function(_, st)
if st.jt ~= 0 then
spiralDescent(st, 7)
return
end
if st.x >= 0x84 then
incJt(st)
return
end
st.x = u8(st.x + 1)
local angle = st.var1
st.var1 = u8(st.var1 + 1)
st.xOffset = cosine(angle, 0x18)
if bit.band(st.x, 1) ~= 0 then return end
st.y = u8(st.y - 1)
end
F.BATTLE_ANIM_FUNC_SMOKE_FLAME_WHEEL = function(_, st)
local angle = st.param
st.yOffset = u8(sra(sra(sra(sine(angle, 0x18)))) + st.var2)
st.xOffset = cosine(angle, 0x18)
st.param = u8(st.param + 2)
if bit.band(st.param, 7) ~= 0 then return end
if st.var2 == 0xe8 then
deinit(st)
return
end
st.var2 = u8(st.var2 - 1)
end
F.BATTLE_ANIM_FUNC_SACRED_FIRE = function(_, st)
local angle = st.param
st.yOffset = u8(sra(sra(sra(sine(angle, 0x18)))) + st.var2)
st.xOffset = cosine(angle, 0x18)
st.param = u8(st.param + 2)
if bit.band(st.param, 3) ~= 0 then return end
if st.var2 == 0xd0 then
deinit(st)
return
end
st.var2 = u8(st.var2 - 2)
end
F.BATTLE_ANIM_FUNC_PRESENT_SMOKESCREEN = function(_, st)
if st.jt == 0 then
incJt(st)
st.var1, st.var2 = 0x34, 0x10
elseif st.jt == 2 then
deinit(st)
return
end
if st.jt ~= 1 then return end
if st.x >= 0x6c then return end
stepToTarget(st, 2)
local value = sine(st.var1, st.var2)
-- Only the upper half of the bounce shows: a positive sine is negated, so
-- the puff always sits above its line.
if bit.band(value, 0x80) == 0 then value = u8(-value) end
st.yOffset = value
st.var1 = u8(st.var1 - 4)
-- The halving below is unreachable on the cart: `and $1f` can never leave
-- $20, so Present's puff keeps its height the whole way across.
end
F.BATTLE_ANIM_FUNC_HORN = function(_, st)
local function spin()
local value = sine(st.var2, 8)
st.xOffset = value
st.y = u8(st.var1 - sra(value))
st.var2 = u8(st.var2 + 8)
end
local jt = st.jt
if jt == 0 then
st.jt = st.param
st.var1 = st.y
elseif jt == 1 then
if st.x >= 0x58 then return end
stepToTarget(st, 2)
elseif jt == 2 then
if st.var2 >= 0x20 then
deinit(st)
return
end
spin()
elseif jt == 3 then
spin()
end
end
F.BATTLE_ANIM_FUNC_NEEDLE = function(_, st)
local function line()
if st.x >= 0x84 then
deinit(st)
return
end
stepToTarget(st, st.param)
end
local jt = st.jt
if jt == 0 then
-- The upper nybble of PARAM picks straight line or arc.
st.jt = bit.band(swap(st.param), 0xf)
elseif jt == 1 then
line()
elseif jt == 2 then
local value = sine(st.var1, 0x10)
-- Only the negative half is written, so the needle arcs upward only.
if bit.band(value, 0x80) ~= 0 then st.yOffset = value end
st.var1 = u8(st.var1 - 4)
line() -- .two falls into .one
end
end
F.BATTLE_ANIM_FUNC_THIEF_PAYDAY = function(_, st)
if st.jt == 0 then
incJt(st)
st.var1 = 0x28
st.var2 = u8(st.y - 0x28)
end
st.yOffset = sine(st.var1, st.var2)
-- PARAM is a MASK, so the coin only drifts left on the frames that clear it.
if bit.band(st.var1, st.param) == 0 then st.x = u8(st.x - 1) end
st.var1 = u8(st.var1 + 1)
if bit.band(st.var1, 0x3f) ~= 0 then return end
st.var1 = 0x20
st.var2 = bit.rshift(st.var2, 1)
end
F.BATTLE_ANIM_FUNC_ABSORB_CIRCLE = function(_, st)
local angle = st.param
st.yOffset = sine(angle, st.var1)
st.xOffset = cosine(angle, st.var1)
st.param = u8(st.param + 1)
if bit.band(st.param, 1) == 0 then st.x = u8(st.x - 1) end
if bit.band(st.param, 3) == 0 then st.y = u8(st.y + 1) end
if st.x >= 0x5a then
st.var1 = u8(st.var1 + 1)
return
end
if st.var1 == 0 then
deinit(st)
return
end
st.var1 = u8(st.var1 - 1)
end
F.BATTLE_ANIM_FUNC_CONVERSION = function(_, st)
local angle = st.param
st.param = u8(st.param + 1)
st.yOffset = sine(angle, st.var1)
st.xOffset = cosine(angle, st.var1)
local age = st.var2
st.var2 = u8(st.var2 + 1)
if age < 0x40 then
st.var1 = u8(st.var1 + 1)
return
end
local radius = st.var1
st.var1 = u8(st.var1 - 1)
if radius ~= 0 then return end
deinit(st)
end
F.BATTLE_ANIM_FUNC_BONEMERANG = function(_, st)
if st.jt == 0 then
incJt(st)
st.var2 = st.y
end
st.y = u8(st.var2 + sine(st.param, 0x30))
-- Eight steps of phase between the two axes is what bends the throw into a
-- boomerang instead of a circle.
st.xOffset = cosine(st.param + 8, 0x30)
st.param = u8(st.param + 1)
end
F.BATTLE_ANIM_FUNC_SHINY = function(_, st)
if st.jt ~= 0 then return end
incJt(st)
st.yOffset = sine(st.param, 0x10)
st.xOffset = cosine(st.param, 0x10)
st.var2 = 0xf
end
-- Sky Attack pulses OBP0 rather than moving anything, so the palette write is
-- what the view has to see.
local SKY_ATTACK_GBC = { [0] = 0xff, [1] = 0xaa, [2] = 0x55, [3] = 0xaa }
local SKY_ATTACK_SGB = { [0] = 0xff, [1] = 0xff, [2] = 0x00, [3] = 0x00 }
F.BATTLE_ANIM_FUNC_SKY_ATTACK = function(self, st)
local function cyclePalette()
local phase = bit.band(st.var2, 7)
st.var2 = u8(st.var2 + 1)
local pals = self.env.sgb and SKY_ATTACK_SGB or SKY_ATTACK_GBC
self.obp0 = bit.band(pals[bit.rshift(phase, 1)] or 0xff, st.var1)
end
local jt = st.jt
if jt == 0 then
incJt(st)
st.var1 = (self.env.battleTurn or 0) == 0 and 0xf0 or 0xcc
elseif jt == 1 then
cyclePalette()
elseif jt == 2 then
cyclePalette()
if st.x >= 0x84 then return end
stepToTarget(st, 4)
elseif jt == 3 then
cyclePalette()
if st.x >= 0xd0 then
deinit(st)
return
end
stepToTarget(st, 4)
end
end
F.BATTLE_ANIM_FUNC_GROWTH_SWORDS_DANCE = function(_, st)
local angle = st.param
st.yOffset = u8(sra(sra(sra(sine(angle, 0x18)))) + st.var2)
st.param = u8(st.param + 1)
st.xOffset = cosine(angle, 0x18)
st.var2 = u8(st.var2 - 2)
end
F.BATTLE_ANIM_FUNC_STRENGTH_SEISMIC_TOSS = function(_, st)
local jt = st.jt
if jt == 0 then
if st.yOffset == 0xe0 then
incJt(st)
st.var1 = 2
return
end
st.yOffset, st.var1 = add16(st.yOffset, st.var1, -0x80)
elseif jt == 1 then
if st.var2 ~= 0 then
st.var2 = st.var2 - 1
return
end
-- Shakes by negating the accumulated step and folding it into the offset
-- every four frames.
st.var2 = 4
st.var1 = u8(-st.var1)
st.yOffset = u8(st.yOffset + st.var1)
elseif jt == 2 then
if st.x >= 0x84 then
deinit(st)
return
end
stepToTarget(st, 4)
end
end
F.BATTLE_ANIM_FUNC_SPEED_LINE = function(self, st)
if st.jt == 0 then
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_SPEED_LINE_1")
+ bit.band(st.param, 0x7f))
end
if bit.band(st.param, 0x80) ~= 0 then
st.xOffset = u8(st.xOffset - 1)
else
st.xOffset = u8(st.xOffset + 1)
end
end
F.BATTLE_ANIM_FUNC_SLUDGE = function(self, st)
local jt = st.jt
if jt == 0 then
incJt(st)
st.var1 = 0xc
elseif jt == 1 then
if st.var1 ~= 0 then
st.var1 = st.var1 - 1
return
end
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_SLUDGE_BUBBLE_BURST"))
st.yOffset = u8(st.yOffset - 1) -- .done falls into .two
elseif jt == 2 then
st.yOffset = u8(st.yOffset - 1)
end
end
F.BATTLE_ANIM_FUNC_METRONOME_HAND = function(_, st)
local angle = st.var1
st.var1 = u8(st.var1 + 2)
st.yOffset = sine(angle, 2)
st.xOffset = cosine(angle, 8)
end
F.BATTLE_ANIM_FUNC_METRONOME_SPARKLE_SKETCH = function(_, st)
if st.yOffset >= 0x20 then
deinit(st)
return
end
st.xOffset = cosine(st.param, 8)
st.param = u8(st.param + 2)
if bit.band(st.param, 7) ~= 0 then return end
st.yOffset = u8(st.yOffset + 1)
end
F.BATTLE_ANIM_FUNC_AGILITY = function(_, st)
-- anim_incobj is what makes it disappear.
if st.jt ~= 0 then
deinit(st)
return
end
st.x = u8(st.x + st.param)
end
F.BATTLE_ANIM_FUNC_SAFEGUARD_PROTECT = function(_, st)
local angle = st.param
st.yOffset = sine(angle, 0x18)
st.xOffset = sra(cosine(angle, 0x18))
st.param = u8(st.param + 1)
end
F.BATTLE_ANIM_FUNC_LOCK_ON_MIND_READER = function(_, st)
-- .two: hold for $10 frames, then go.
local function hold()
local left = st.var1
st.var1 = u8(st.var1 - 1)
if left ~= 0 then return end
deinit(st)
end
local jt = st.jt
if jt == 0 then
incJt(st)
st.var1 = 0x28
-- The low nybble walks along the four LOCK_ON / MIND_READER framesets,
-- and the object's own frameset id is the base of that run.
reinit(st, u8(st.framesetId + bit.band(st.param, 0xf)))
st.param = bit.bor(bit.band(st.param, 0xf0), 8)
jt = 1
end
if jt == 1 then
if st.var1 ~= 0 then
st.var1 = st.var1 - 1
-- The radius is the distance still to run, so the ring converges.
local radius = u8(st.var1 + 8)
st.yOffset = sine(st.param, radius)
st.xOffset = cosine(st.param, radius)
return
end
st.var1 = 0x10
incJt(st)
hold() -- .done falls into .two
return
end
if jt == 2 then hold() end
end
F.BATTLE_ANIM_FUNC_HEAL_BELL_NOTES = function(self, st)
if st.jt == 0 then
incJt(st)
reinit(st, self:framesetId("BATTLE_ANIM_FRAMESET_MUSIC_NOTE_1") + st.param)
end
if st.yOffset >= 0x38 then
deinit(st)
return
end
st.yOffset = u8(st.yOffset + 1)
local angle = st.var1
st.var1 = u8(st.var1 + 1)
st.xOffset = cosine(angle, 0x18)
-- Tests the Y COORDINATE, not the counter: the note drifts left on the
-- frames its row happens to be even.
if bit.band(st.y, 1) ~= 0 then return end
st.x = u8(st.x - 1)
end
F.BATTLE_ANIM_FUNC_BATON_PASS = function(_, st)
if st.param == 0 then return end
local angle = st.var1
st.var1 = u8(st.var1 + 1)
local value = sine(angle, st.param)
if bit.band(value, 0x80) == 0 then value = u8(-value) end
st.yOffset = value
if bit.band(st.var1, 0x1f) ~= 0 then return end
-- Each bounce is half the last.
st.param = bit.rshift(st.param, 1)
end
F.BATTLE_ANIM_FUNC_ENCORE_BELLY_DRUM = function(_, st)
if st.var1 >= 0x10 then
deinit(st)
return
end
local radius = st.var1
st.var1 = u8(st.var1 + 2)
st.yOffset = sine(st.param, radius)
st.xOffset = cosine(st.param, radius)
end
F.BATTLE_ANIM_FUNC_SWAGGER_MORNING_SUN = function(_, st)
-- The top two bits of PARAM are the speed and the low six the angle; the
-- amplitude is VAR1 as it was BEFORE this frame's speed was added.
local radius = st.var1
st.var1 = u8(st.var1 + rlca(rlca(bit.band(st.param, 0xc0))))
local angle = bit.band(st.param, 0x3f)
st.yOffset = sine(angle, radius)
st.xOffset = cosine(angle, radius)
end
F.BATTLE_ANIM_FUNC_HIDDEN_POWER = function(_, st)
-- .two: the ring expands eight pixels a frame and then vanishes.
local function expand()
if st.var1 >= 0x80 then
deinit(st)
return
end
local radius = st.var1
st.var1 = u8(st.var1 + 8)
stepCircle(st, st.param, radius)
end
local jt = st.jt
if jt == 0 then
local angle = st.param
st.param = u8(st.param + 1)
stepCircle(st, angle, 0x18)
elseif jt == 1 then
incJt(st)
st.var1 = 0x18
expand() -- .one falls into .two
elseif jt == 2 then
expand()
end
end
F.BATTLE_ANIM_FUNC_CURSE = function(_, st)
if st.jt ~= 1 then return end
if st.x < 0x30 then
deinit(st)
return
end
st.x = u8(st.x - 2)
st.y = u8(st.y + 2)
end
F.BATTLE_ANIM_FUNC_PERISH_SONG = function(_, st)
local angle = st.param
st.param = u8(st.param + 2)
-- VAR1 is both the sink and the counter: the sine is added to it and then
-- it is stepped, so the ring drifts down as it turns.
st.yOffset = u8(sra(sra(sine(angle, 0x50))) + st.var1)
st.var1 = u8(st.var1 + 1)
st.xOffset = cosine(angle, 0x50)
end
F.BATTLE_ANIM_FUNC_RAPID_SPIN = function(_, st)
if st.yOffset == 0xd0 then
deinit(st)
return
end
st.yOffset = u8(st.yOffset - 4)
end
F.BATTLE_ANIM_FUNC_BETA_PURSUIT = function(_, st)
local jt = st.jt
if jt == 0 then
if st.param ~= 0 then
incJt(st)
incJt(st)
return
end
incJt(st)
st.yOffset = 0xec
elseif jt == 1 then
if st.yOffset == 4 then
deinit(st)
return
end
st.yOffset = u8(st.yOffset + 4)
elseif jt == 2 then
if st.yOffset == 0xd8 then return end
st.yOffset = u8(st.yOffset - 4)
elseif jt == 3 then
deinit(st)
end
end
F.BATTLE_ANIM_FUNC_RAIN_SANDSTORM = function(_, st)
-- The Y offset wraps at $70, which is what makes a single object read as a
-- continuous fall of rain.
local function fall(step)
local y = st.yOffset + 4
st.yOffset = (y < 0x70) and y or 0
st.xOffset = u8(st.xOffset + step)
end
local jt = st.jt
if jt == 0 then
st.jt = u8(st.param + 1) -- .zero sets the index from PARAM and then incs
elseif jt == 1 then
fall(2)
elseif jt == 2 then
fall(8)
elseif jt == 3 then
fall(4)
end
end
F.BATTLE_ANIM_FUNC_BATTLE_ANIM_OBJ_B0 = function(_, st)
-- Unused on the cart (nothing names BATTLE_ANIM_OBJ_B0), transcribed
-- because the jumptable slot is real: PARAM's nybbles become a 16-bit step
-- over XCOORD:VAR1, the high nybble duplicated into both halves of the
-- whole-pixel byte.
local high = bit.band(st.param, 0xf0)
high = bit.bor(high, bit.rshift(high, 4))
st.x, st.var1 = add16(st.x, st.var1,
high * 256 + swap(bit.band(st.param, 0xf)))
end
F.BATTLE_ANIM_FUNC_PSYCH_UP = function(_, st)
local angle = st.param
st.param = u8(st.param + 1)
stepCircle(st, angle, 0x18)
end
F.BATTLE_ANIM_FUNC_COTTON = function(_, st)
local phase = st.var2
st.var2 = u8(st.var2 + 1)
stepCircle(st, u8(bit.rshift(phase, 1) + st.param), 0x18)
end
F.BATTLE_ANIM_FUNC_ANCIENT_POWER = function(_, st)
if st.var1 >= 0x20 then
deinit(st)
return
end
local angle = st.var1
st.var1 = u8(st.var1 + 1)
st.yOffset = u8(-sine(angle, st.param))
end
AnimObjects.FUNCTIONS = F
AnimObjects.NUM_STRUCTS = NUM_STRUCTS
AnimObjects.OAM_LIMIT = OAM_LIMIT
AnimObjects.OAM_XFLIP = OAM_XFLIP
AnimObjects.OAM_YFLIP = OAM_YFLIP
AnimObjects.OAM_PRIO = OAM_PRIO
AnimObjects.OAM_PAL1 = OAM_PAL1
AnimObjects.FIX_COORDS = FIX_COORDS
AnimObjects.sine, AnimObjects.cosine = sine, cosine
AnimObjects.u8, AnimObjects.s8, AnimObjects.sra = u8, s8, sra
AnimObjects.swap, AnimObjects.rlca = swap, rlca
return AnimObjects