Skip to content

Separate Move Animations from Other Battle Animations

Xillicis edited this page Feb 10, 2024 · 13 revisions

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.

Contents

  1. Split Move Constants
  2. Split Animation Pointers
  3. Define Separate Animation ID
  4. Split the Special Effects
  5. Modify Core Animation Code
  6. Edge Cases
  7. Fix Field Moves
  8. Closing Remarks

1. Split Move Constants

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.

2. Split Animation Pointers

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
...

3. Define Separate Animation ID

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.

4. Split the Special Effects

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

5. Modify Core 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.

6. Edge Cases

The remainder of this tutorial is just making sure we correctly load/modify wAnimationID and wAltAnimationId.

engine/battle/core.asm

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
...

engine/battle/effects.asm

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
...

engine/items/item_effects.asm

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
...

engine/battle/trainer_ai.asm

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
...

engine/movie/trade.asm

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

7. Fix field moves

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.

8. Closing Remarks

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.

Clone this wiki locally