Skip to content

Separate Move Animations from Other Battle Animations

Xillicis edited this page Nov 10, 2023 · 15 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. Modify Core Animation Code
  5. Edge Cases
  6. 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. 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

 	; if throwing a Poké Ball, skip the regular animation code
+.checkTossAnimation
 	cp TOSS_ANIM
 	jr nz, .moveAnimation
...

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.

5. 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
        ldh a, [hWhoseTurn]
        and a
        ld a, [wPlayerMoveNum]
        jr z, .notEnemyTurn
        ld a, [wEnemyMoveNum]
.notEnemyTurn
        and a
        ret z
+;;; check for which type of animation to play
+       ld a, [wAltAnimationID]
+       and a
+       jr nz, PlayAlternativeAnimation
+;;; 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. There aren't too many situations in this file.

Subroutine FreezeBurnParalyzeEffect:

...
 	call QuarterSpeedDueToParalysis ; quarter speed of affected mon
 	ld a, ENEMY_HUD_SHAKE_ANIM
-	call PlayBattleAnimation
+	call PlayAlternativeAnimation2
 	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 PlayAlternativeAnimation2
 	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 PlayAlternativeAnimation2
 	ld hl, FrozenText
 	jp PrintText
 .opponentAttacker
...

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

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