mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 11:44:42 +02:00
2000 lines
57 KiB
Lua
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
|