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!