Check base power before skipping damage calculation - pret/pokered GitHub Wiki

DISCLAIMER: As with any changes to the battle engine, one should always make sure to test and check for bugs; especially if these changes will conflict with preexisting changes that you have already made. Please make a note in this tutorial or let me know on Discord (@Xillicis) if you discover any issues or realize a more efficient implementation.

WHY? The main motivation for this tutorial is to make it easier to implement new moves such as Mud Slap, Fire Lash, Chilling Water, Low Sweep, Mud Shot, etc...

EDIT: We can now also implement guaranteed STATUS on damaging moves, making it possible to introduce moves like Zap Cannon, Sacred Fire, DynamicPunch, etc...

In particular, this applies to any move with a move effect belonging to the list ResidualEffects2 which is found in data/battle/residual_effects_2.asm. The ResidualEffects2 include all stat up and stat down moves along with BIDE_EFFECT and SLEEP_EFFECT which correspond to the moves BIDE and SPORE, respectively.

Let's consider an example move: Mud Slap. The base power is 20 and the move always lowers the targets accuracy (assuming the move didn't miss). In generation 1, moves that always lower a stat like Sand Attack or Tail Whip will automatically skip the damage calculation. In particular, any move with the move effect belonging to the list residual_effects_2.asm will skip the damage calculation. Therefore, if we try to implement Mud Slap following this tutorial Add a New Move and use the ACCURACY_DOWN1_EFFECT as the move effect, then the move will never deal damage.

We solve this issue by checking that the base power is not zero. Open up engine/battle/core.asm and make the following change to the subroutine MirrorMoveCheck:,

...
.next
	ld a, [wPlayerMoveEffect]
	ld hl, ResidualEffects2
	ld de, 1
	call IsInArray
-	jp c, JumpMoveEffect ; done here after executing effects of ResidualEffects2
+	jr nc, .notResidual2Effect
+	ld a, [wPlayerMovePower]
+	and a ; check if zero base power
+	jp z, JumpMoveEffect
+.notResidual2Effect
	ld a, [wMoveMissed]
	and a
	jr z, .moveDidNotMiss
	call PrintMoveFailureText
	ld a, [wPlayerMoveEffect]
	cp EXPLODE_EFFECT ; even if Explosion or Selfdestruct missed, its effect still needs to be activated
	jr z, .notDone
	jp ExecutePlayerMoveDone ; otherwise, we're done if the move missed
.moveDidNotMiss
	call ApplyAttackToEnemyPokemon
...

Notice that old code checks if the move effect belongs to the list ResidualEffects2 and if so it jumps to the move effect which effectively leaves the current subroutine hence skipping the ApplyAttackToEnemyPokemon. (Note: IsInArray sets the carry flag c if the value stored in register a belongs to the array pointed to by hl.) So instead, we change the code to check if the move effect is in ResidualEffects2 and check if the base power is zero before jumping to the move effect.

This code is only for the players move, so we also need to make a similar change for the enemy's move. Head down a bit farther to the subroutine EnemyCheckIfMirrorMoveEffect: make the following change,

...
	ld hl, ResidualEffects2
	ld de, $1
	call IsInArray
-	jp c, JumpMoveEffect
+	jr nc, .notResidual2EffectEnemy
+	ld a, [wEnemyMovePower]
+	and a ; Check if zero base power
+	jp z, JumpMoveEffect
+.notResidual2EffectEnemy
	ld a, [wMoveMissed]
	and a
	jr z, .moveDidNotMiss
...

Damage will now be applied properly; however, if you try the move out, you'll find that the animation will play twice. Once for the damage and once for the stat reduction. There are two corrections that have to be made for this. The first, is if the move increased the user's stat, e.g. Flame Charge. The second is if the move lowered the opponent's stat, e.g. Mud Slap. To correct this, open up engine/battle/effects.asm and modify the subroutine UpdateStat: with the following changes,

...
        call nz, Bankswitch
        pop de
.notMinimize
+       ldh a, [hWhoseTurn]
+       and a
+       ld a, [wPlayerMovePower]
+       jr z, .gotUsersPower1
+       ld a, [wEnemyMovePower]
+.gotUsersPower1
+       and a ; Skip animation if damage dealing move
+       jr nz, .skipAnimation
        call PlayCurrentMoveAnimation
+.skipAnimation
        ld a, [de]
        cp MINIMIZE
        jr nz, .applyBadgeBoostsAndStatusPenalties
...

Go down a bit further and make the following changes to the subroutine UpdateLoweredStatDone:,

...
        ld a, [de]
        cp $44
        jr nc, .ApplyBadgeBoostsAndStatusPenalties
+       ldh a, [hWhoseTurn] ; check who is using the move
+       and a
+	ld a, [wPlayerMovePower]
+       jr z, .gotUsersPower2
+       ld a, [wEnemyMovePower]
+.gotUsersPower2
+	and a ; Skip animation if damage dealing move
+	jr nz, .ApplyBadgeBoostsAndStatusPenalties
        call PlayCurrentMoveAnimation2
.ApplyBadgeBoostsAndStatusPenalties
...

Now move back up just a bit to StatModifierDownEffect: and insert the following:

StatModifierDownEffect:
	ld hl, wEnemyMonStatMods
	ld de, wPlayerMoveEffect
	ld bc, wEnemyBattleStatus1
	ldh a, [hWhoseTurn]
	and a
	jr z, .statModifierDownEffect
	ld hl, wPlayerMonStatMods
	ld de, wEnemyMoveEffect
	ld bc, wPlayerBattleStatus1
	ld a, [wLinkState]
	cp LINK_STATE_BATTLING
	jr z, .statModifierDownEffect
+	ld a, [wEnemyMovePower]			; if enemy move power != 0, we skip the RNG call that is
+	and a							; applied to NPC trainers' status moves... otherwise, their
+	jr nz, .statModifierDownEffect	; "guaranteed" post-damage effects will fail 25% of the time
	call BattleRandom
	cp 25 percent + 1 ; chance to miss by in regular battle
	jp c, MoveMissed
.statModifierDownEffect
...

As noted in the annotations, if we don't insert this Move Power check, there will be a 25% chance that enemy attacks won't trigger the stat reduction, even if they're supposed to be guaranteed to occur.

However, something funky happens when the "guaranteed stat drop" move user has had its accuracy lowered. This is my best guess at what's happening in the background:

  • The RNG first rolls to check whether the moves hits and deals damage
  • If that roll is successful, the RNG then rolls AGAIN to determine whether the stat drop "hits" as well
  • If the second roll fails, the move will not apply its stat drop after dealing damage

To prevent this, we need to create another safeguard. Go to .nonSideEffect and insert the following to bypass this separate accuracy check:

.nonSideEffect ; non-side effects only
	push hl
	push de
	push bc
+	ld a, [hWhoseTurn]			; begin process for finding power of last move used
+	and a						; identify whose turn it is, 0 = player, nonzero = enemy
+	ld a, [wPlayerMovePower]	; load player move power
+	jr z, .checkDamagingMove	; jump if it is player's turn
+	ld a, [wEnemyMovePower]		; otherwise, load enemy move power
+.checkDamagingMove		; checks if status move or damaging move w/ guaranteed side effect
+	and a				; did the last move have 0 power?
+	jr nz, .getStatMod1	; if not (i.e. it's a damaging move), skip acc. check on side effect
	call MoveHitTest ; apply accuracy tests
	pop bc
	pop de
	pop hl
	ld a, [wMoveMissed]
	and a
	jp nz, MoveMissed
	ld a, [bc]
	bit INVULNERABLE, a ; fly/dig
	jp nz, MoveMissed
+	jr .getStatMod2
+.getStatMod1	; we still need to pop these values, as in the subroutine above
+	pop bc
+	pop de
+	pop hl
+.getStatMod2	; now back to the usual prep for stat modification
	ld a, [de]
	sub ATTACK_DOWN1_EFFECT
	cp EVASION_DOWN1_EFFECT + $3 - ATTACK_DOWN1_EFFECT ; covers all -1 effects
	jr c, .decrementStatMod
	sub ATTACK_DOWN2_EFFECT - ATTACK_DOWN1_EFFECT ; map -2 effects to corresponding -1 effect
.decrementStatMod
...

We have yet another issue to address now; if a stat-boosting damaging move (i.e. Flame Charge) KO's the target, the game handles the Pokemon-fainting routine without applying the stat boost. First we need to find StatModifierUpEffect: and add an extra colon, as follows:

-StatModifierUpEffect:
+StatModifierUpEffect::
	ld hl, wPlayerMonStatMods
	ld de, wPlayerMoveEffect
	ldh a, [hWhoseTurn]
	and a
...

This exports the StatModifierUpEffect routine so that it can be called via the call instruction elsewhere outside this file. Now open engine\battle\core.asm and find HandleEnemyMonFainted:. We'll insert the following move effect check:

HandleEnemyMonFainted:
+	ld b, ATTACK_UP1_EFFECT			; the first +1 effect (see \constants\move_effect_constants.asm)
+	ld c, EVASION_UP1_EFFECT		; the last +1 effect, will be used to end the upcoming loop
+	ld de, wPlayerMoveEffect
+.checkOneStageBoostLoop
+	ld a, [de]						; 'a' now equals [wPlayerMoveEffect]
+	cp b							; does the effect in 'a' match the current effect in 'b'?
+	jr z, .boostBattleMonStats		; if so, boost appropriate Mon stat before the enemy faints
+	ld a, b							; copy 'b' into 'a'
+	inc b							; 'b' now points to the next +1 effect
+	cp c							; does 'a' equal 'c' (EVASION_UP1_EFFECT, i.e. end of +1 effects list)?
+	jr nz, .checkOneStageBoostLoop	; if not, loop back and check for next +1 effect
+; we're done checking for +1 effects, now we check for +2 effects
+	ld b, ATTACK_UP2_EFFECT			; the first +2 effect
+	ld c, EVASION_UP2_EFFECT		; the last +2 effect, will be used to end the upcoming loop
+.checkTwoStageBoostLoop
+	ld a, [de]						; 'a' once again equals [wPlayerMoveEffect]
+	cp b							; same process as above, except we're checking for +2 boosts instead
+	jr z, .boostBattleMonStats
+	ld a, b
+	inc b
+	cp c
+	jr nz, .checkTwoStageBoostLoop
+	jr .faintEnemyPokemon			; if the KO-ing move does not boost stats, jump to .faintEnemyPokemon subroutine
+.boostBattleMonStats
+	call StatModifierUpEffect		; this is why we needed the "double colon"
+.faintEnemyPokemon					; finally, things proceed as normal from here
	xor a
	ld [wInHandlePlayerMonFainted], a
	call FaintEnemyPokemon
	call AnyPartyAlive
...

Of course, in the interest of fairness we'll want the same benefit for our opponent. Find HandlePlayerMonFainted: and Copy/Paste the same check as above (with the appropriate addresses swapped...):

HandlePlayerMonFainted:
+	ld b, ATTACK_UP1_EFFECT
+	ld c, EVASION_UP1_EFFECT
+	ld de, wEnemyMoveEffect
+.checkOneStageBoostLoop
+	ld a, [de]
+	cp b
+	jr z, .boostEnemyMonStats
+	ld a, b
+	inc b
+	cp c
+	jr nz, .checkOneStageBoostLoop
+; we're done checking for +1 effects, now we check for +2 effects
+	ld b, ATTACK_UP2_EFFECT
+	ld c, EVASION_UP2_EFFECT
+.checkTwoStageBoostLoop
+	ld a, [de]
+	cp b
+	jr z, .boostEnemyMonStats
+	ld a, b
+	inc b
+	cp c
+	jr nz, .checkTwoStageBoostLoop
+	jr .faintPlayerPokemon
+.boostEnemyMonStats
+	call StatModifierUpEffect
+.faintPlayerPokemon
	ld a, 1
	ld [wInHandlePlayerMonFainted], a
	call RemoveFaintedPlayerMon
...

Now to get that pesky "Nothing happened." text to stop displaying when you land a hit and the stat in question is already at +/-6. First go to PrintNothingHappenedText: and insert the following:

PrintNothingHappenedText:
+	ld a, [hWhoseTurn]
+	and a
+	ld a, [wPlayerMovePower]
+	jr z, .gotUsersPower3
+	ld a, [wEnemyMovePower]
+.gotUsersPower3
+	and a
+	ret nz
	ld hl, NothingHappenedText
	jp PrintText

That takes care of the "Nothing happened" message when attempting to boost a stat at +6. Now find the CantLowerAnymore: subroutine and insert the following (these additions are pretty much identical):

CantLowerAnymore:
	ld a, [de]
	cp ATTACK_DOWN_SIDE_EFFECT
	ret nc
+	ld a, [hWhoseTurn]
+	and a
+	ld a, [wPlayerMovePower]
+	jr z, .gotUsersPower4
+	ld a, [wEnemyMovePower]
+.gotUsersPower4
+	and a
+	ret nz
	ld hl, NothingHappenedText
	jp PrintText

Now we've done the same message elimination when trying to lower a stat that's already at -6.

That's it for guaranteed stat alterations on damaging moves. Note that these changes should work for any of the stat up or down effects listed in data/battle/residual_effects_2.asm

Guaranteed Status Effects on Damaging Moves

You'll first go to constants\move_effect_constants.asm. Near the end of the list you'll find a convenient pocket of slots labeled const_skip. Replace them with the following:

...
	const SPECIAL_DOWN_SIDE_EFFECT   ; $47
-	const_skip                       ; $48
-	const_skip                       ; $49
-	const_skip                       ; $4A
-	const_skip                       ; $4B
+	const CONFUSION_SIDE_EFFECT2     ; $48 guaranteed confusion after dealing damage
+	const POISON_SIDE_EFFECT3        ; $49 guaranteed poison after dealing damage
+	const PARALYZE_SIDE_EFFECT3      ; $4A guaranteed paralysis after dealing damage
+	const BURN_SIDE_EFFECT3          ; $4B guaranteed burn after dealing damage
	const CONFUSION_SIDE_EFFECT      ; $4C
...

Now go to data\moves\effects_pointers.asm and make the following insertions in the corresponding slots:

...
	dw StatModifierDownEffect    ; SPECIAL_DOWN_SIDE_EFFECT
-	dw StatModifierDownEffect    ; unused effect
-	dw StatModifierDownEffect    ; unused effect
-	dw StatModifierDownEffect    ; unused effect
-	dw StatModifierDownEffect    ; unused effect
+	dw ConfusionSideEffect       ; CONFUSION_SIDE_EFFECT2
+	dw PoisonEffect              ; POISON_SIDE_EFFECT3
+	dw FreezeBurnParalyzeEffect  ; PARALYZE_SIDE_EFFECT3
+	dw FreezeBurnParalyzeEffect  ; BURN_SIDE_EFFECT3
	dw ConfusionSideEffect       ; CONFUSION_SIDE_EFFECT
...

Note that the entries need to be in the same order on both of these lists, if you're considering adding some new effect to the game.

Now we incorporate these new effects into the battle engine. Move over to engine\battle\effects.asm. We'll start with the guaranteed poison effect. Find the subroutine .poisonEffect and insert these two lines:

...
.poisonEffect
	call CheckTargetSubstitute
	jr nz, .noEffect ; can't poison a substitute target
	ld a, [hli]
	ld b, a
	and a
	jr nz, .noEffect ; miss if target is already statused
	ld a, [hli]
	cp POISON ; can't poison a poison-type target
	jr z, .noEffect
	ld a, [hld]
	cp POISON ; can't poison a poison-type target
	jr z, .noEffect
	ld a, [de]
+	cp POISON_SIDE_EFFECT3	; is 'guaranteed poison' the current move's side effect?
+	jr z, .inflictPoison	; if so, bypass RNG and jump straight to .inflictPoison
	cp POISON_SIDE_EFFECT1
	ld b, 20 percent + 1 ; chance of poisoning
	jr z, .sideEffectTest
...

Guaranteed poison is done! It's that easy! Now we'll do paralysis and burn. Move on down to the FreezeBurnParalyzeEffect: routine:

FreezeBurnParalyzeEffect:
	xor a
	ld [wAnimationType], a
	call CheckTargetSubstitute
	ret nz ; return if they have a substitute, can't effect them
	ldh a, [hWhoseTurn]
	and a
	jp nz, .opponentAttacker
	ld a, [wEnemyMonStatus]
	and a
	jp nz, CheckDefrost ; can't inflict status if opponent is already statused
	ld a, [wPlayerMoveType]
	ld b, a
	ld a, [wEnemyMonType1]
	cp b ; do target type 1 and move type match?
	ret z  ; return if they match (an ice move can't freeze an ice-type, body slam can't paralyze a normal-type, etc.)
	ld a, [wEnemyMonType2]
	cp b ; do target type 2 and move type match?
	ret z  ; return if they match
	ld a, [wPlayerMoveEffect]
+	cp PARALYZE_SIDE_EFFECT3 ; is 'guaranteed paralysis' our move's secondary effect?
+	jr z, .paralyze1         ; if so, bypass RNG call and jump straight to paralyzing the enemy
+	cp BURN_SIDE_EFFECT3     ; same check for 'guaranteed burn'
+	jr z, .burn1             ; jump straight to burning the enemy
	cp PARALYZE_SIDE_EFFECT1 + 1
	ld b, 10 percent + 1
	jr c, .regular_effectiveness
; extra effectiveness
	ld b, 30 percent + 1
	ASSERT PARALYZE_SIDE_EFFECT2 - PARALYZE_SIDE_EFFECT1 == BURN_SIDE_EFFECT2 - BURN_SIDE_EFFECT1
	ASSERT PARALYZE_SIDE_EFFECT2 - PARALYZE_SIDE_EFFECT1 == FREEZE_SIDE_EFFECT2 - FREEZE_SIDE_EFFECT1
	sub PARALYZE_SIDE_EFFECT2 - PARALYZE_SIDE_EFFECT1 ; treat extra effective as regular from now on
.regular_effectiveness
	push af
	call BattleRandom ; get random 8bit value for probability test
	cp b
	pop bc
	ret nc ; do nothing if random value is >= 1A or 4D [no status applied]
	ld a, b ; what type of effect is this?
	cp BURN_SIDE_EFFECT1
	jr z, .burn1
	cp FREEZE_SIDE_EFFECT1
	jr z, .freeze1
-; .paralyze1
+.paralyze1 ; we're just un-commenting this to create a destination for the 'jr z, .paralyze1' inserted above
	ld a, 1 << PAR
	ld [wEnemyMonStatus], a
	call QuarterSpeedDueToParalysis ; quarter speed of affected mon
	ld a, ENEMY_HUD_SHAKE_ANIM
	call PlayBattleAnimation
	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
	ld hl, BurnedText
	jp PrintText

That takes care of auto-burn and auto-paralysis for player moves. Now we implement the same for opponent moves. Find the .opponentAttacker subroutine just a bit further down:

.opponentAttacker
	ld a, [wBattleMonStatus] ; mostly same as above with addresses swapped for opponent
	and a
	jp nz, CheckDefrost
	ld a, [wEnemyMoveType]
	ld b, a
	ld a, [wBattleMonType1]
	cp b
	ret z
	ld a, [wBattleMonType2]
	cp b
	ret z
	ld a, [wEnemyMoveEffect]
+	cp PARALYZE_SIDE_EFFECT3 ; is 'guaranteed paralysis' our opponent's secondary effect?
+	jr z, .paralyze2         ; if so, bypass RNG call and jump straight to paralyzing our Mon
+	cp BURN_SIDE_EFFECT3     ; same check for burn
+	jr z, .burn2             ; jump straight to burning our Mon
	cp PARALYZE_SIDE_EFFECT1 + 1
	ld b, 10 percent + 1
	jr c, .regular_effectiveness2
; extra effectiveness
	ld b, 30 percent + 1
	sub BURN_SIDE_EFFECT2 - BURN_SIDE_EFFECT1 ; treat extra effective as regular from now on
.regular_effectiveness2
	push af
	call BattleRandom
	cp b
	pop bc
	ret nc
	ld a, b
	cp BURN_SIDE_EFFECT1
	jr z, .burn2
	cp FREEZE_SIDE_EFFECT1
	jr z, .freeze2
-; .paralyze2
+.paralyze2 ; un-commenting this, same as before
	ld a, 1 << PAR
	ld [wBattleMonStatus], a
	call QuarterSpeedDueToParalysis
	jp PrintMayNotAttackText
.burn2
	ld a, 1 << BRN
	ld [wBattleMonStatus], a
	call HalveAttackDueToBurn
	ld hl, BurnedText
	jp PrintText

Paralysis and burn are done. Now there's quite a bit to do for confusion. Move way down and find ConfusionSideEffect: and add the following:

ConfusionSideEffect:
+	ld a, [hWhoseTurn]
+	and a
+	ld a, [wPlayerMoveEffect]
+	jr z, .checkGuaranteedConfusionEffect
+	ld a, [wEnemyMoveEffect]
+.checkGuaranteedConfusionEffect
+	cp CONFUSION_SIDE_EFFECT2
+	jr z, ConfusionSideEffectSuccess
	call BattleRandom
	cp 10 percent ; chance of confusion
	ret nc
	jr ConfusionSideEffectSuccess

This checks whether the current move's effect is CONFUSION_SIDE_EFFECT2 (our new guaranteed confusion effect); if it is, skip the RNG call.

Now move down to .confuseTarget. Here we'll prevent the move animation from replaying when the opponent becomes Confused:

.confuseTarget
	bit CONFUSED, [hl] ; is mon confused?
	jr nz, ConfusionEffectFailed
	set CONFUSED, [hl] ; mon is now confused
	push af
	call BattleRandom
	and $3
	inc a
	inc a
	ld [bc], a ; confusion status will last 2-5 turns
	pop af
	cp CONFUSION_SIDE_EFFECT
-	call nz, PlayCurrentMoveAnimation2
+	jr z, .displayBecameConfusedText
+	cp CONFUSION_SIDE_EFFECT2
+	jr z, .displayBecameConfusedText
+	call PlayCurrentMoveAnimation2
+.displayBecameConfusedText
	ld hl, BecameConfusedText
	jp PrintText

And finally, let's eliminate that 50 frame (nearly 1 second) delay if the target is already Confused and Confusion therefore fails to be reapplied. Move down just a bit further and find ConfusionEffectFailed:

ConfusionEffectFailed:
	cp CONFUSION_SIDE_EFFECT
	ret z
+	cp CONFUSION_SIDE_EFFECT2
+	ret z
	ld c, 50
	call DelayFrames
	jp ConditionalPrintButItFailed

And with that, we can have our damaging moves apply guaranteed Confusion, Paralysis, Burn, or Poison! Using this knowledge, you can probably figure out how to apply 100% Freeze as well... I didn't include it because I assumed everyone might find it just a wee bit overpowered...

Thanks for reading, and good luck!