-
Notifications
You must be signed in to change notification settings - Fork 1k
Separate Move Animations from Other Battle Animations
In this tutorial we will cover how to separate the move animations from other battle animations, like tossing a Pokeball or the poison/burn animation that plays when you take poison or burn damage. The reason for doing this is that these other battle animations take up precious slots for new moves that we'd like to add to the game. Ideally, you'd expect to have around 254 moves, but these other battle animations limit you to around 220 or so total moves that can be in the game. This is not a simple fix, and may need to be adapted to your own ROM hack, depending on the changes you've made thus far.
DISCLAIMER: I have not done a thorough debugging. But from the preliminary tests, the results look good. Let me know on Discord (@Xillicis) if you discover any bugs or have questions/suggestions.
- Split Move Constants
- Split Animation Pointers
- Define Separate Animation ID
- Split the Special Effects
- Modify Core Animation Code
- Edge Cases
- Fix Field Moves
- Closing Remarks
We'll start by separating the move/move animation constants from the other battle animation constants. Open the file constants/move_constants.asm and make the following modification.
...
const TRI_ATTACK ; a1
const SUPER_FANG ; a2
const SLASH ; a3
const SUBSTITUTE ; a4
const STRUGGLE ; a5
DEF NUM_ATTACKS EQU const_value - 1
- ; Moves do double duty as animation identifiers.
+ ; Separate other battle animations
+ const_def
+ const NO_ANIM
const SHOWPIC_ANIM
const STATUS_AFFECTED_ANIM
const ANIM_A8
...
const SHAKE_SCREEN_ANIM
const HIDEPIC_ANIM ; monster disappears
const ROCK_ANIM ; throw rock
const BAIT_ANIM ; throw bait
-DEF NUM_ATTACK_ANIMS EQU const_value - 1
+DEF NUM_ALTERNATIVE_ANIMS EQU const_value - 1
...
By adding const_def
to the list, we start the enumeration again starting from zero.
We also include NO_ANIM
so that SHOWPIC_ANIM
is not zero.
Zero is often used reserved for special code structure, so we don't want any actual animations to start from zero.
Now we need to also separate the animations defined by the moves. Head to data/moves/animations.asm and make the following change,
...
dw SubstituteAnim
dw StruggleAnim
assert_table_length NUM_ATTACKS
+
+AlternativeAnimationPointers:
+ table_width 2, AlternativeAnimationPointers
dw ShowPicAnim
dw EnemyFlashAnim
dw PlayerFlashAnim
...
dw ThrowRockAnim
dw ThrowBaitAnim
- assert_table_length NUM_ATTACK_ANIMS
+ assert_table_length NUM_ALTERNATIVE_ANIMS
dw ZigZagScreenAnim
...
We now need a way to know whether we are working with actual move animations or the alternative battle animations.
We do this by defining a new variable in WRAM, called wAltAnimationID
.
The basic idea is that, if the value stored in [wAltAnimationID]
is zero, then we are using a move animation.
If it is non-zero, then we are using an alternative animation.
Open up ram/wram.asm and define the new variable:
...
wAnimationID:: db
wNamingScreenType::
wPartyMenuTypeOrMessageID::
; temporary storage for the number of tiles in a tileset
wTempTilesetNumTiles:: db
; used by the pokemart code to save the existing value of wListScrollOffset
; so that it can be restored when the player is done with the pokemart NPC
wSavedListScrollOffset:: db
- ds 2
+wAltAnimationID:: db
+
+ ds 1
...
The rest of the tutorial is just modifying the battle code to account for the use of two different animation IDs; wAnimationID
and wAltAnimationID
.
Before we get to modifying the core code, we will split some special animations that share IDs across the two animation types. Open up data/battle_anims/special_effects.asm and make the change:
AnimationIdSpecialEffects:
; animation id, effect routine address
anim_special_effect MEGA_PUNCH, AnimationFlashScreen
anim_special_effect GUILLOTINE, AnimationFlashScreen
anim_special_effect MEGA_KICK, AnimationFlashScreen
anim_special_effect HEADBUTT, AnimationFlashScreen
anim_special_effect TAIL_WHIP, TailWhipAnimationUnused
anim_special_effect GROWL, DoGrowlSpecialEffects
anim_special_effect DISABLE, AnimationFlashScreen
anim_special_effect BLIZZARD, DoBlizzardSpecialEffects
anim_special_effect BUBBLEBEAM, AnimationFlashScreen
anim_special_effect HYPER_BEAM, FlashScreenEveryFourFrameBlocks
anim_special_effect THUNDERBOLT, FlashScreenEveryEightFrameBlocks
anim_special_effect REFLECT, AnimationFlashScreen
anim_special_effect SELFDESTRUCT, DoExplodeSpecialEffects
anim_special_effect SPORE, AnimationFlashScreen
anim_special_effect EXPLOSION, DoExplodeSpecialEffects
anim_special_effect ROCK_SLIDE, DoRockSlideSpecialEffects
+ db -1 ; end
+
+AltAnimationIdSpecialEffects:
anim_special_effect TRADE_BALL_DROP_ANIM, TradeHidePokemon
anim_special_effect TRADE_BALL_SHAKE_ANIM, TradeShakePokeball
anim_special_effect TRADE_BALL_TILT_ANIM, TradeJumpPokeball
anim_special_effect TOSS_ANIM, DoBallTossSpecialEffects
anim_special_effect SHAKE_ANIM, DoBallShakeSpecialEffects
anim_special_effect POOF_ANIM, DoPoofSpecialEffects
anim_special_effect GREATTOSS_ANIM, DoBallTossSpecialEffects
anim_special_effect ULTRATOSS_ANIM, DoBallTossSpecialEffects
db -1 ; end
This lists will be used in the main animation code
Next up, we modify the main subroutine that all of the battle animations get funneled into.
Open up engine/battle/animations.asm and head to the PlayAnimation:
subroutine.
We will make the following modifications:
PlayAnimation:
xor a
ldh [hROMBankTemp], a ; it looks like nothing reads this
ld [wSubAnimTransform], a
+; If [wAltAnimationID] = 0, then we play an attack animation
+ ld a, [wAltAnimationID]
+ and a
+ ld de, AlternativeAnimationPointers
+ jr nz, .gotAnimationType
ld a, [wAnimationID] ; get animation number
- dec a
+ ld de, AttackAnimationPointers ; animation command stream pointers
+.gotAnimationType
+ dec a
ld l, a
ld h, 0
add hl, hl
- ld de, AttackAnimationPointers ; animation command stream pointers
add hl, de
...
...
pop hl
vc_hook Stop_reducing_move_anim_flashing_Guillotine
jr .animationLoop
.AnimationOver
;;; make sure we zero out the alt animation ID after we're finished with the animation.
+ xor a
+ ld [wAltAnimationID], a
ret
The idea is simple, if [wAltAnimationID]
is zero, then we load the AttackAnimationPointers
, otherwise, we load AlternativeAnimationPointers
.
The only thing we need to do is make sure that when an attack animation should be played, we load [wAltAnimationID]
with zero.
If we play a non-attack animation, then we just load the corresponding battle animation ID into [wAltAnimationID]
.
We have a few more changes to make in this file, so head over to MoveAnimation:
and make the following changes:
MoveAnimation:
push hl
push de
push bc
push af
call WaitForSoundToFinish
call SetAnimationPalette
+; check alt animation first
+ ld a, [wAltAnimationID]
+ and a
+ jr nz, .checkTossAnimation
ld a, [wAnimationID]
and a
jr z, .animationFinished
+ jr .moveAnimation
; if throwing a Poké Ball, skip the regular animation code
+.checkTossAnimation
cp TOSS_ANIM
jr nz, .moveAnimation
ld de, .animationFinished
push de
jp TossBallAnimation
.moveAnimation
; check if battle animations are disabled in the options
ld a, [wOptions]
This is more of a safety measure here. We just check the value of [wAltAnimationID]
and if non-zero, then check the toss animation.
Now checkout TossBallAnimation:
and make the following change:
...
jr z, .done
ld b, ULTRATOSS_ANIM
.done
ld a, b
.PlayNextAnimation
- ld [wAnimationID], a
+ ld [wAltAnimationID], a
push bc
push hl
...
.BlockBall
ld a, TOSS_ANIM
- ld [wAnimationID], a
+ ld [wAltAnimationID], a
call PlayAnimation
ld a, SFX_FAINT_THUD
call PlaySound
ld a, BLOCKBALL_ANIM
- ld [wAnimationID], a
+ ld [wAltAnimationID], a
jp PlayAnimation
...
Since the ball toss/ball block animations are not attack animations, we want to load the alternative animation ID instead.
Lastly, we modify DoSpecialEffectByAnimationId
.
DoSpecialEffectByAnimationId:
push hl
push de
push bc
+ ld a, [wAltAnimationID]
+ and a
+ ld hl, AltAnimationIdSpecialEffects
+ jr nz, .usingAltAnimation
ld a, [wAnimationID]
ld hl, AnimationIdSpecialEffects
+.usingAltAnimation
ld de, 3
call IsInArray
jr nc, .done
inc hl
ld a, [hli]
ld h, [hl]
ld l, a
ld de, .done
push de
jp hl
.done
Again, we just need to make sure we load the correct list depending on which animation we are using.
A few small things to finish here. Moves like Growl and Roar which play the Pokemon's cry can mess up the the alternative animations from playing correctly. We modify DrawFrameBlock:
...
cp FRAMEBLOCKMODE_04
jr z, .done ; skip cleaning OAM buffer and don't advance the frame block destination address
;;; Need to "CleanOAM" if using an alternative animation
+ ld a, [wAltAnimationID]
+ and a
+ jr nz, .skipGrowlCheck
ld a, [wAnimationID]
cp GROWL
jr z, .resetFrameBlockDestAddr
+.skipGrowlCheck
call AnimationCleanOAM
.resetFrameBlockDestAddr
ld hl, wShadowOAM
...
and now modify IsCryMove:
IsCryMove:
; set carry if the move animation involves playing a monster cry
+ ld a, [wAltAnimationID]
+ and a
+ ret nz
ld a, [wAnimationID]
cp GROWL
jr z, .CryMove
...
There's a few things to consider with these last two modifications. If you have an alternative animation that makes use of the Pokemon's cry, then it will not play that cry. So just keep that in mind if you have other plans in the future.
The remainder of this tutorial is just making sure we correctly load/modify wAnimationID
and wAltAnimationId
.
Starting with engine/battle/core.asm
We will start by modifying PlayMoveAnimation:
and adding in a new subroutine for alternative animations.
Make the following changes
PlayMoveAnimation:
ld [wAnimationID], a
vc_hook_red Reduce_move_anim_flashing_Confusion
call Delay3
vc_hook_red Reduce_move_anim_flashing_Psychic
+; set alternative animation ID to be zero so that we use the move animations.
+ xor a
+ ld [wAltAnimationID], a
predef_jump MoveAnimation
; call this subroutine if we are playing an alternative animation.
+PlayAltAnimation:
+ ld [wAltAnimationID], a
+ predef_jump MoveAnimation
+
...
Now we will comb through the core.asm
and see what animations need to call PlayMoveAnimation
and which need to call PlayAltAnimation
.
This is most likely the point where your will need to adapt the tutorial to your own needs.
Since, if you added a lot of different moves, it's likely you've modified engine/battle/core.asm
, and need to adapt accordingly.
Here are the changes you need to make based on the original code.
Starting with the subroutine HandlePoisonBurnLeechSeed:
...
ld [wAnimationType], a
ld a, BURN_PSN_ANIM
- call PlayMoveAnimation ; play burn/poison animation
+ call PlayAltAnimation ; play burn/poison animation
pop hl
...
Subroutine SendOutMon:
...
ldh [hWhoseTurn], a
ld a, POOF_ANIM
- call PlayMoveAnimation
+ call PlayAltAnimation
hlcoord 4, 11
predef AnimateSendingOutMon
...
Subroutine playerCheckIfFlyOrChargeEffect:
...
.playAnim
xor a
ld [wAnimationType], a
ld a, STATUS_AFFECTED_ANIM
- call PlayMoveAnimation
+ call PlayAltAnimation
MirrorMoveCheck:
...
Subroutine CheckPlayerStatusConditions:
...
ld [wAnimationType], a
ld a, SLP_PLAYER_ANIM
- call PlayMoveAnimation
+ call PlayAltAnimation
ld hl, FastAsleepText
call PrintText
...
Subroutine CheckPlayerStatusConditions:
...
xor a
ld [wAnimationType], a
ld a, CONF_PLAYER_ANIM
- call PlayMoveAnimation
+ call PlayAltAnimation
call BattleRandom
cp 50 percent + 1 ; chance to hurt itself
jr c, .TriedToUseDisabledMoveCheck
...
...
.FlyOrChargeEffect
xor a
ld [wAnimationType], a
ld a, STATUS_AFFECTED_ANIM
- call PlayMoveAnimation
+ call PlayAltAnimation
.NotFlyOrChargeEffect
...
Subroutine EnemyCheckIfFlyOrChargeEffect:
...
.playAnim
xor a
ld [wAnimationType], a
ld a, STATUS_AFFECTED_ANIM
- call PlayMoveAnimation
+ call PlayAltAnimation
EnemyCheckIfMirrorMoveEffect:
...
Subroutine CheckEnemyStatusConditions:
...
ld [wAnimationType], a
ld a, SLP_ANIM
- call PlayMoveAnimation
+ call PlayAltAnimation
jr .sleepDone
.wokeUp
...
...
ld [wAnimationType], a
ld a, CONF_ANIM
- call PlayMoveAnimation
+ call PlayAltAnimation
call BattleRandom
cp $80
...
...
xor a
ld [wAnimationType], a
ld a, STATUS_AFFECTED_ANIM
- call PlayMoveAnimation
+ call PlayAltAnimation
.notFlyOrChargeEffect
...
This file is little bit more tricky to modify, let's start in the same way that we did with core.asm
.
We are going to break apart the subroutines that play the animations.
Starting with PlayBattleAnimation2:
PlayBattleAnimation2:
; play animation ID at a and animation type 6 or 3
ld [wAnimationID], a
+; zero out the alternative animation
+ xor a
+ ld [wAltAnimationID], a
+GotAnimationID:
ldh a, [hWhoseTurn]
and a
ld a, $6
jr z, .storeAnimationType
ld a, $3
.storeAnimationType
ld [wAnimationType], a
jp PlayBattleAnimationGotID
+PlayAlternativeAnimation2:
+ ld [wAltAnimationID], a
+ jr GotAnimationID
+
and PlayCurrentMoveAnimation:
PlayCurrentMoveAnimation:
; animation at MOVENUM will be played unless MOVENUM is 0
; resets wAnimationType
xor a
ld [wAnimationType], a
+;;; check for which type of animation to play
+ ld a, [wAltAnimationID]
+ and a
+ jr nz, PlayAlternativeAnimation
ldh a, [hWhoseTurn]
and a
ld a, [wPlayerMoveNum]
jr z, .notEnemyTurn
ld a, [wEnemyMoveNum]
.notEnemyTurn
and a
ret z
+;;; fallthrough
PlayBattleAnimation:
; play animation ID at a and predefined animation type
ld [wAnimationID], a
+;;; zero out the alternative animation
+ xor a
+ ld [wAltAnimationID], a
PlayBattleAnimationGotID:
; play animation at wAnimationID
push hl
push de
push bc
predef MoveAnimation
pop bc
pop de
pop hl
ret
+
+PlayAlternativeAnimation:
+ ld [wAltAnimationID], a
+ jr PlayBattleAnimationGotID
+
The idea is the same as we did in engine/battle/core.asm
.
We break apart the two different animations that can play and make sure to zero out [wAltAnimationID]
if we are playing a move animation.
Again, we need to go back and make changes for all the different animations that play in this file.
Subroutine PoisonEffect:
...
.continue
pop de
ld a, [de]
cp POISON_EFFECT
jr z, .regularPoisonEffect
ld a, b
- call PlayBattleAnimation2
+ call PlayAlternativeAnimation2
jp PrintText
.regularPoisonEffect
call PlayCurrentMoveAnimation2
jp PrintText
.noEffect
ld a, [de]
cp POISON_EFFECT
...
Subroutine FreezeBurnParalyzeEffect:
...
call QuarterSpeedDueToParalysis ; quarter speed of affected mon
ld a, ENEMY_HUD_SHAKE_ANIM
- call PlayBattleAnimation
+ call PlayAlternativeAnimation
jp PrintMayNotAttackText ; print paralysis text
.burn1
ld a, 1 << BRN
ld [wEnemyMonStatus], a
call HalveAttackDueToBurn ; halve attack of affected mon
ld a, ENEMY_HUD_SHAKE_ANIM
- call PlayBattleAnimation
+ call PlayAlternativeAnimation
ld hl, BurnedText
jp PrintText
.freeze1
call ClearHyperBeam ; resets hyper beam (recharge) condition from target
ld a, 1 << FRZ
ld [wEnemyMonStatus], a
ld a, ENEMY_HUD_SHAKE_ANIM
- call PlayBattleAnimation
+ call PlayAlternativeAnimation
ld hl, FrozenText
jp PrintText
.opponentAttacker
...
Subroutine ThrashPetalDanceEffect:
...
ld [de], a ; set thrash/petal dance counter to 2 or 3 at random
ldh a, [hWhoseTurn]
add SHRINKING_SQUARE_ANIM
- jp PlayBattleAnimation2
+ jp PlayAlternativeAnimation2
Subroutine ChargeEffect:
. For this subroutine, the structure is a little bit more complicated.
Essentially it could play play either a move animation or an alternative animation.
I've made a quick hack to handle this situation, but there is probably a cleaner way to handle it.
...
ld b, TELEPORT ; load Teleport's animation
+;;; teleport is the only battle move animation so we handle it separately
+ xor a
+ ld [wAnimationType], a
+ ld a, b
+ call PlayBattleAnimation
+ jr .doneWithAnimations
+;;;
.notFly
ld a, [de]
cp DIG
jr nz, .notDigOrFly
set INVULNERABLE, [hl] ; mon is now invulnerable to typical attacks (fly/dig)
ld b, SLIDE_DOWN_ANIM
.notDigOrFly
xor a
ld [wAnimationType], a
ld a, b
- call PlayBattleAnimation
+ call PlayAlternativeAnimation
+.doneWithAnimations
ld a, [de]
ld [wChargeMoveNum], a
...
Subroutine BideEffect:
. Need to use the alternative animation.
...
inc a
ld [bc], a ; set Bide counter to 2 or 3 at random
ldh a, [hWhoseTurn]
add XSTATITEM_ANIM
- jp PlayBattleAnimation2
+ jp PlayAlternativeAnimation2
...
Subroutine UpdateStatDone:
. The reason for this change will be more clear when we get to modifying the file engine/items/item_effects
.
...
and a
jr z, .playerTurn
ld hl, wEnemyBattleStatus2
ld de, wEnemyMoveNum
ld bc, wEnemyMonMinimized
.playerTurn
; check if we used an X-stat up item
+ ld a, [wAltAnimationID]
+ and a
+ jr nz, .notMinimize
ld a, [de]
cp MINIMIZE
...
Since all the item animations are not move animations, we need to change the animation IDs to be the alternative animation IDs. Make the following changes in the following subroutines:
Subroutine ItemUseBall:
...
; Do the animation.
ld a, TOSS_ANIM
- ld [wAnimationID], a
+ ld [wAltAnimationID], a
xor a
...
Subroutine BaitRockCommon:
BaitRockCommon:
- ld [wAnimationID], a
+ ld [wAltAnimationID], a
xor a
ld [wAnimationType], a
...
Subroutine ItemUseXStat:
. Note that this subroutine calls StatModifierUpEffect
which is in engine/battle/effects.asm
.
But will load the alternative animation ID, the call to StatModifierUpEffect
will behave as expected.
...
call PrintItemUseTextAndRemoveItem
ld a, XSTATITEM_ANIM ; X stat item animation ID
ld [wPlayerMoveNum], a
+ ld [wAltAnimationID], a
call LoadScreenTilesFromBuffer1 ; restore saved screen
call Delay3
...
Subroutine ThrowBallAtTrainerMon:
ThrowBallAtTrainerMon:
call RunDefaultPaletteCommand
call LoadScreenTilesFromBuffer1 ; restore saved screen
call Delay3
ld a, TOSS_ANIM
- ld [wAnimationID], a
+ ld [wAltAnimationID], a
predef MoveAnimation ; do animation
ld hl, ThrowBallAtTrainerMonText1
...
Next, open up engine/battle/trainer_ai.asm and make one change to the subroutine AIIncreaseStat:
...
ld a, [hl]
push af
push hl
ld a, XSTATITEM_DUPLICATE_ANIM
+ ld [wAltAnimationID], a
ld [hli], a
ld [hl], b
...
Lastly, the file engine/movie/trade.asm plays some animations, but the fix is quite simple.
Make the following change to the subroutine, Trade_ShowAnimation:
Trade_ShowAnimation:
- ld [wAnimationID], a
+ ld [wAltAnimationID], a
xor a
ld [wAnimationType], a
predef_jump MoveAnimation
Lastly, there is an unused field indexed by ANIM_B4
. We will get rid of this
FieldMoveDisplayData:
; move id, FieldMoveNames index, leftmost tile
; (leftmost tile = -1 + tile column in which the first
; letter of the move's name should be displayed)
db CUT, 1, $0C
db FLY, 2, $0C
- db ANIM_B4, 3, $0C ; unused
- db SURF, 4, $0C
+ db SURF, 3, $0C
- db STRENGTH, 5, $0A
+ db STRENGTH, 4, $0A
- db FLASH, 6, $0C
+ db FLASH, 5, $0C
- db DIG, 7, $0C
+ db DIG, 6, $0C
- db TELEPORT, 8, $0A
+ db TELEPORT, 7, $0A
- db SOFTBOILED, 9, $08
+ db SOFTBOILED, 8, $08
db -1 ; end
And also remove the corresponding text in, data/moves/field_move_names.asm
; see also FieldMoveDisplayData
FieldMoveNames:
db "CUT@"
db "FLY@"
- db "@"
db "SURF@"
...
Lastly in engine/menus/start_sub_menus.asm
, we need to remove the corresponding field move pointer. This is in StartMenu_Pokemon
.
...
jp hl
.outOfBattleMovePointers
dw .cut
dw .fly
dw .surf
- dw .surf
dw .strength
dw .flash
dw .dig
dw .teleport
dw .softboiled
.fly
bit BIT_THUNDERBADGE, a
...
Surf does double work for the unused field move, so we must get rid of it, otherwise, field moves won't work correctly.
That's it. You should now be able to get to 254 total moves in the game and you even have room to add other kinds of animations, say, for new items added to the game. I hope you can modify this to fit your current ROM hack. Again, it's all about keeping track of the different IDs that need to be loaded. If you do happen to miss a correction on an animation, most likely the game will crash or just play a different animation.
If you want to get more than 254 moves, then you will probably need to update a huge portion of the code to handle 16-bit IDs. This is an even more challenging task.