Enemy Pokémon Use PP - pret/pokered GitHub Wiki
If you are reading this, you likely already know that enemy Pokémon never run out of PP in generation one. However, it is not exactly correct to say that they have 'infinite PP' or 'don't have PP', although, the result is essentially the same.
Enemy Pokémon, both wild and those used by NPC's do have PP, but it never gets decreased and the game doesn't bother to check if they have PP or not when using moves. This tutorial will go through all the steps required to get enemy Pokémon using their PP in battle.
This tutorial does not currently cover the topic of capturing wild Pokémon. Upon successful capture, your new companion will magically have full PP (as per the base game). It is possible to solve this, but the act of catching a Pokémon triggers LoadEnemyMonData, which sets the enemy Pokémon's PP to the maximum values (as it also does at the start of the battle). Including some kind of exception here will likely achieve the desired result, although it is likely also necessary to tweak other things including the function that sends Pokémon to Bill's PC.
I have not done any testing with link cable battles, so there is a good chance they will be buggy.
A function called 'DecrementPP' exists which is responsible for making the player's PP drop by one in both the battlemon data and party data when we use a move. This is not used for the enemy, so first, we'll trigger this function when an enemy uses a move too. In engine\battle\core.asm:
EnemyCanExecuteMove:
xor a
ld [wMonIsDisobedient], a
call PrintMonName1Text
+ ld hl, DecrementPP
+ ld de, wEnemySelectedMove ; pointer to the move just used
+ ld b, BANK(DecrementPP)
+ call Bankswitch
ld a, [wEnemyMoveEffect]
ld hl, ResidualEffects1
This is all well and good, but the DecrementPP function currently has no idea what an enemy is, let's teach it how to deal with enemy Pokémon! For this one, we need to modify engine\battle\decrement_PP.asm:
DecrementPP:
; after using a move, decrement pp in battle and (if not transformed?) in party
ld a, [de]
cp STRUGGLE
ret z ; if the pokemon is using "struggle", there's nothing to do
; we don't decrement PP for "struggle"
ld hl, wPlayerBattleStatus1
+ ldh a, [hWhoseTurn]
+ and a
+ jr z, .playersTurn
+ ld hl, wEnemyBattleStatus1
+.playersTurn
ld a, [hli] ; load the wPlayerBattleStatus1 pokemon status flags and increment hl to load the
; wPlayerBattleStatus2 status flags later
and (1 << STORING_ENERGY) | (1 << THRASHING_ABOUT) | (1 << ATTACKING_MULTIPLE_TIMES)
ret nz ; if any of these statuses are true, don't decrement PP
bit USING_RAGE, [hl]
ret nz ; don't decrement PP either if Pokemon is using Rage
ld hl, wBattleMonPP ; PP of first move (in battle)
+ ldh a, [hWhoseTurn]
+ and a
+ jr z, .playersTurn2
+ ld hl, wEnemyMonPP
+.playersTurn2
; decrement PP in the battle struct
call .DecrementPP
; decrement PP in the party struct
- ld a, [wPlayerBattleStatus3]
+ ld hl, wPlayerBattleStatus3
+ ldh a, [hWhoseTurn]
+ and a
+ jr z, .playersTurn3
+ ld hl, wPlayerBattleStatus3
+.playersTurn3
+ ld a, [hl]
bit TRANSFORMED, a
ret nz ; Return if transformed. Pokemon Red stores the "current pokemon's" PP
; separately from the "Pokemon in your party's" PP. This is
; duplication -- in all cases *other* than Pokemon with Transform.
; Normally, this means we have to go on and make the same
; modification to the "party's pokemon" PP that we made to the
; "current pokemon's" PP. But, if we're dealing with a Transformed
; Pokemon, it has separate PP for the move set that it copied from
; its opponent, which is *not* the same as its real PP as part of your
; party. So we return, and don't do that part.
ld hl, wPartyMon1PP ; PP of first move (in party)
+ ldh a, [hWhoseTurn]
+ and a
+ jr z, .playersTurn4
+ ld hl, wEnemyMon1PP
+.playersTurn4
- ld a, [wPlayerMonNumber] ; which mon in party is active
+ push hl
+ ld hl, wPlayerMonNumber
+ ldh a, [hWhoseTurn]
+ and a
+ jr z, .playersTurn5
+ ld hl, wEnemyMonPartyPos
+.playersTurn5
+ ld a, [hl]
+ pop hl
ld bc, wPartyMon2 - wPartyMon1
call AddNTimes ; calculate address of the mon to modify
.DecrementPP:
- ld a, [wPlayerMoveListIndex] ; which move (0, 1, 2, 3) did we use?
+ push hl
+ ld hl, wPlayerMoveListIndex
+ ldh a, [hWhoseTurn]
+ and a
+ jr z, .playersTurn6
+ ld hl, wEnemyMoveListIndex
+.playersTurn6
+ ld a, [hl]
+ pop hl
ld c, a
ld b, 0
add hl, bc ; calculate the address in memory of the PP we need to decrement
; based on the move chosen.
dec [hl] ; Decrement PP
ret
You might find that this makes your DecrementPP file too big and the compiler complains about "Battle Engine 11" being too full. In this case, you can remove DecrementPP from Battle Engine 11 by modifying main.asm ...
SECTION "Battle Engine 11", ROMX
-INCLUDE "engine/battle/decrement_pp.asm"
INCLUDE "gfx/version.asm"
... and then include it somewhere else:
SECTION "Itemfinder 2", ROMX
INCLUDE "engine/menus/league_pc.asm"
INCLUDE "engine/events/hidden_items.asm"
+INCLUDE "engine/battle/decrement_pp.asm"
Regardless of if the enemy is a wild Pokémon or a trainer, every time an enemy Pokémon is loaded up, their PP is set to the max for each move. This means that trainers like Jugglers or Agatha who love to switch, will get their PP restored each time they switch out. To avoid this, we'll make it load the PP from the party instead, but only for trainer battles. In engine\battle\core.asm:
.loadMovePPs
+ ld a, [wIsInBattle]
+ cp $2 ; is it a trainer battle?
+ jr z, .copyPPFromEnemyPartyData
ld hl, wEnemyMonMoves
ld de, wEnemyMonPP - 1
predef LoadMovePPs
+ jr .loadMovePPs_2
+.copyPPFromEnemyPartyData
+ ld hl, wEnemyMon1PP ;Copy Data source
+ ld a, [wWhichPokemon]
+ ld bc, wEnemyMon2 - wEnemyMon1
+ call AddNTimes ;Copy Data Source now point to the PP of the correct Enemy Party Mon
+ ld de, wEnemyMonPP ;Copy Data Destination
+ ld bc, NUM_MOVES ;Number of Bytes to Copy
+ call CopyData
+.loadMovePPs_2 ;End of the original .loadMovePPs needs to be common to set up for the copyBaseStatsLoop properly
ld hl, wMonHBaseStats
ld de, wEnemyMonBaseStats
ld b, NUM_STATS
For this one, we are going to modify the AnyMoveToSelect function. This function is normally only used for the player, but we now want the enemy to use it too.
First, we need to tell AnyMoveToSelect who is selecting a move, and unfortunately, hWhoseTurn won't be useful here, so we'll go ahead and make use of the e register. Firstly, we'll set e to 0 if we want to use the function for the player. In engine\battle\core.asm:
ldh [hUILayoutFlags], a
ret
.regularmenu
+ xor a
+ ld e, a
call AnyMoveToSelect
ret z
ld hl, wBattleMonMoves
For the Enemy, we'll remove the old code which only triggers Struggle if they have exactly one move and it is disabled. We'll replace this with a call to AnyMoveToSelect, after setting the e register to 1. In engine\battle\core.asm
SelectEnemyMove:
ld a, [wLinkState]
sub LINK_STATE_BATTLING
jr nz, .noLinkBattle
; link battle
call SaveScreenTilesToBuffer1
call LinkBattleExchangeData
call LoadScreenTilesFromBuffer1
ld a, [wSerialExchangeNybbleReceiveData]
cp LINKBATTLE_STRUGGLE
jp z, .linkedOpponentUsedStruggle
cp LINKBATTLE_NO_ACTION
jr z, .unableToSelectMove
cp 4
ret nc
ld [wEnemyMoveListIndex], a
ld c, a
ld hl, wEnemyMonMoves
ld b, 0
add hl, bc
ld a, [hl]
- jr .done
+ jp .done
.noLinkBattle
ld a, [wEnemyBattleStatus2]
and (1 << NEEDS_TO_RECHARGE) | (1 << USING_RAGE) ; need to recharge or using rage
ret nz
ld hl, wEnemyBattleStatus1
ld a, [hl]
and (1 << CHARGING_UP) | (1 << THRASHING_ABOUT) ; using a charging move or thrash/petal dance
ret nz
ld a, [wEnemyMonStatus]
and (1 << FRZ) | SLP_MASK
ret nz
ld a, [wEnemyBattleStatus1]
and (1 << USING_TRAPPING_MOVE) | (1 << STORING_ENERGY) ; using a trapping move like wrap or bide
ret nz
ld a, [wPlayerBattleStatus1]
bit USING_TRAPPING_MOVE, a ; caught in player's trapping move (e.g. wrap)
jr z, .canSelectMove
.unableToSelectMove
ld a, $ff
jr .done
.canSelectMove
- ld hl, wEnemyMonMoves+1 ; 2nd enemy move
- ld a, [hld]
- and a
- jr nz, .atLeastTwoMovesAvailable
- ld a, [wEnemyDisabledMove]
- and a
- ld a, STRUGGLE ; struggle if the only move is disabled
- jr nz, .done
-.atLeastTwoMovesAvailable
+ ld a, 1
+ ld e, a
+ call AnyMoveToSelect
+ jr z, .done2
+ ld hl, wEnemyMonMoves
ld a, [wIsInBattle]
dec a
Now we'll modify the AnyMoveToSelect function itself. This mainly consists of extending the function to work for both the Player and the Enemy. In engine\battle\core.asm
AnyMoveToSelect:
; return z and Struggle as the selected move if all moves have 0 PP and/or are disabled
+ ld a, e
+ and a
+ ld hl, wPlayerSelectedMove
+ jr z, .playerTurn
+ ld hl, wEnemySelectedMove
+.playerTurn
ld a, STRUGGLE
- ld [wPlayerSelectedMove], a
+ ld [hl], a
+ ld a, e
+ and a
+ ld hl, wBattleMonPP
ld a, [wPlayerDisabledMove]
+ jr z, .playerTurn2
+ ld hl, wEnemyMonPP
+ ld a, [wEnemyDisabledMove]
+.playerTurn2
and a
- ld hl, wBattleMonPP
jr nz, .handleDisabledMove
ld a, [hli]
or [hl]
inc hl
or [hl]
inc hl
or [hl]
and $3f
ret nz
jr .noMovesLeft
.handleDisabledMove
swap a
and $f ; get disabled move
ld b, a
ld d, NUM_MOVES + 1
xor a
.handleDisabledMovePPLoop
dec d
jr z, .allMovesChecked
ld c, [hl] ; get move PP
inc hl
dec b ; is this the disabled move?
jr z, .handleDisabledMovePPLoop ; if so, ignore its PP value
or c
jr .handleDisabledMovePPLoop
.allMovesChecked
- and a ; any PP left?
+ and $3f
ret nz ; return if a move has PP left
.noMovesLeft
+;;;If Enemy, don't display the "No Moves Left Text"
+ ld a, e
+ and a
+ jr z, .playerTurn3
+ xor a
+ and a ;set the z flag again because checking whose turn it was overwrote it
+ ret
+.playerTurn3
ld hl, NoMovesLeftText
call PrintText
ld c, 60
call DelayFrames
xor a
+ and a ;set the z flag again because checking whose turn it was overwrote it
ret
NoMovesLeftText:
text_far _NoMovesLeftText
text_end
For this one, we need to modify the trainer AI. Without going into too many details of how the trainer AI works, I'll mention briefly that each move gets a 'score' allocated to it based on the various decisions taken by the AI. Only the moves with the lowest scores are considered for use.
The following example is a simplification of everything that happens, but should be enough to get the basic concept.
Example: An enemy Pokémon has three moves and one empty slot, and in this particular battle, they are allocated scores as follows:
Move 1 : 10
Move 2 : 10
Move 3 : 15
In this case, only Moves 1 & 2 will be usable, and one of them will be selected at random.
If a move is disabled, it is given a score of 80 (or $50 in hex). This essentially means the move will never get used.
In generation two, the same is true for moves with zero PP, so we'll follow the same, and apply a score of 80 to those moves.
In fact, we're going to pretty much just use the gen 2 code here.
In engine\battle\trainer_ai.asm
.noMoveDisabled
+ ld hl, wBuffer - 1
+ ld de, wEnemyMonPP
+ ld b, 0
+.checkMovePP
+ inc b
+ ld a, b
+ cp NUM_MOVES + 1
+ jr z, .endPPCheck
+ inc hl
+ ld a, [de]
+ inc de
+ and $3f
+ jr nz, .checkMovePP
+ ld [hl], $50
+ jr .checkMovePP
+.endPPCheck
ld hl, TrainerClassMoveChoiceModifications
ld a, [wTrainerClass]
ld b, a
While the above is specific to trainers the following snippet is used for both wild encounters and trainer battles. Just before this, a move has been selected at random. For wild Pokémon, or trainers with no AI, this will be a completely random move. For trainers with AI, some moves may have been removes as candidates by the code we just modified above. The code we are about to modify normally just checks if the selected move is disabled or not. We'll add a check for zero PP here too. This is largely unnecessary for trainers with AI since the moves with zero PP were already removed as candidates, but the disable check happens regardless in the base game, so we'll do the PP check again as well for good measure. In engine\battle\core.asm:
.moveChosen
ld a, b
dec a
ld [wEnemyMoveListIndex], a
+ push hl
+ push bc
+ ld b, 0
+ ld c, a
+ ld hl, wEnemyMonPP
+ add hl, bc
+ ld a, [hl]
+ pop bc
+ pop hl
+ and a
+ jr nz, .disabledCheck
+ pop hl
+ jr z, .chooseRandomMove
+.disabledCheck
ld a, [wEnemyDisabledMove]
swap a
and $f
cp b
ld a, [hl]
pop hl
jr z, .chooseRandomMove ; move disabled, try again
and a
jr z, .chooseRandomMove ; move non-existant, try again
.done
ld [wEnemySelectedMove], a
+.done2 ;if jumping from after AnyMoveToSelect, wEnemySelectedMove has already been set to STRUGGLE
ret
.linkedOpponentUsedStruggle
ld a, STRUGGLE
jr .done
When an enemy uses Disable against the player, it will choose a random move to Disable from the moves which currently have PP left. This PP check is skipped when we use Disable against the enemy, except for in link cable battles. Therefore, we can simply remove the "is this a link cable battle?" check and always do this check.
In engine/battle/effects.asm:
cp LINK_STATE_BATTLING
pop hl ; wEnemyMonMoves
- jr nz, .playerTurnNotLinkBattle
;.playerTurnLinkBattle
push hl
ld hl, wEnemyMonPP
Because Gen 1 doesn't have any moves that reduce the opponent's PP, PP is only checked during move selection, and there is no check in the game to ensure that a selected move still has PP by the time we try to use it. However, a check like this does exist for Disable.
The following is completely optional, but if you plan to implement moves like Spite, then I'd recommend including it.
First, we'll add some new text. This will get used when a move we already selected, somehow ran out of PP before we used it. In data\text\text_2.asm:
_ContCharText::
text "<_CONT>@"
text_end
+_MoveHasNoPPText::
+ text "<USER>'s"
+ line "@"
+ text_ram wcd6d
+ text " has"
+ cont "no PP left!"
+ prompt
Next we'll include a PP check during the move execution of the player. I've placed this right at the end as the very last check, but similar code could be included at a different point in the check with different results. For example, the previous check that runs before this one is the 'Rage Check'. If the PP check comes before this, then Rage would become unusable if the user has no PP left for Rage. In Gen 1, Rage only uses PP on the first turn it is used, and then the user continues to Rage every turn for the rest of the battle. Since PP is only needed on the first turn, it made sense to me that a Pokémon that is already raging, shouldn't have to stop raging just because the PP of Rage was reduced to 0. Indeed, it would actually get stuck trying to use Rage but unable to do so, therefore, I think the check is in the right place here. In Engine\battle\core.asm
.RageCheck
ld a, [wPlayerBattleStatus2]
bit USING_RAGE, a ; is mon using rage?
- jp z, .checkPlayerStatusConditionsDone ; if we made it this far, mon can move normally this turn
+ jp z, .TriedToUseNoPPMoveCheck
ld a, RAGE
ld [wd11e], a
call GetMoveName
call CopyToStringBuffer
xor a
ld [wPlayerMoveEffect], a
ld hl, PlayerCanExecuteMove
jp .returnToHL
+.TriedToUseNoPPMoveCheck
+ ld a, [wPlayerSelectedMove]
+ cp STRUGGLE
+ jr z, .checkPlayerStatusConditionsDone ; if Struggle, don't check PP
+ ld a, [wPlayerMoveListIndex] ;0-3
+ ld c, a
+ ld b, 0
+ ld hl, wBattleMonPP
+ add hl, bc
+ ld a, [hl]
+ cp 0
+ jr nz, .checkPlayerStatusConditionsDone ; we can normally move this turn
+ call PrintMoveHasNoPPText ;User's Move has no PP left!
+ ld hl, ExecutePlayerMoveDone ; player can't move this turn
.returnToHL
xor a
ret
Now we'll include an almost identical check for the enemy:
.checkIfUsingRage
ld a, [wEnemyBattleStatus2]
bit USING_RAGE, a ; is mon using rage?
- jp z, .checkEnemyStatusConditionsDone ; if we made it this far, mon can move normally this turn
+ jp z, .TriedToUseNoPPMoveCheck
ld a, RAGE
ld [wd11e], a
call GetMoveName
call CopyToStringBuffer
xor a
ld [wEnemyMoveEffect], a
ld hl, EnemyCanExecuteMove
jp .enemyReturnToHL
;;;New This is before Decrement PP so PP not decremented yet!
.TriedToUseNoPPMoveCheck
ld a, [wEnemySelectedMove]
cp STRUGGLE
jr z, .checkEnemyStatusConditionsDone ; if Struggle, don't check PP
ld a, [wEnemyMoveListIndex] ;0-3
ld c, a
ld b, 0
ld hl, wEnemyMonPP
add hl, bc
ld a, [hl]
cp 0
jr nz, .checkEnemyStatusConditionsDone ; enemy can normally move this turn
call PrintMoveHasNoPPText ;User's Move has no PP left!
ld hl, ExecuteEnemyMoveDone ; enemy can't move this turn
;jp .enemyReturnToHL; just fallthrough
;;;;;;;;;;;;;;;;;;
.enemyReturnToHL
xor a ; set Z flag
ret
Once move is established to have no PP:
MoveIsDisabledText:
text_far _MoveIsDisabledText
text_end
+PrintMoveHasNoPPText:
+ ld hl, wPlayerSelectedMove
+ ld de, wPlayerBattleStatus1
+ ldh a, [hWhoseTurn]
+ and a
+ jr z, .playerTurn
+ inc hl
+ ld de, wEnemyBattleStatus1
+.playerTurn
+ ld a, [de]
+ res CHARGING_UP, a ;SolarBeam PP taken on Turn 2 in Gen 1. Spite can kill SolarBeam. if we don't clear the charging flag, it will get stuck
+ ld [de], a
+ ld a, [hl]
+ ld [wd11e], a
+ call GetMoveName
+ ld hl, MoveHasNoPPText
+ jp PrintText
+MoveHasNoPPText:
+ text_far _MoveHasNoPPText
+ text_end
For completions sake, I'll include here a bit of code I wrote for trying to get captured enemy Pokémon to have their PP from the battle. In engine\pokemon\add_mon.asm:
.writeEVsLoop ; set all EVs to 0
inc de
ld [de], a
dec b
jr nz, .writeEVsLoop
inc de
inc de ;this probably leaves it pointing at the second byte of DVs
pop hl
+ ld a, [wIsInBattle]
+ and a ; is this a wild mon caught in battle?
+ jr nz, .copyEnemyMonPP
call AddPartyMon_WriteMovePP
inc de
+ jr .newStuffDone
+.copyEnemyMonPP
+ call CopyEnemyMonsPP
+.newStuffDone
ld a, [wCurEnemyLVL]
ld [de], a
inc de
...
.empty
inc de
ld [de], a
dec b
jr nz, .pploop ; there are still moves to read
ret
+CopyEnemyMonsPP:
+ ld hl, wEnemyMonPP
+ inc de ; need to increment once to get it pointing to to PartyMonPP
+ ld a, NUM_MOVES
+ ld c, a
+ ld b, $0
+ call CopyData; Copy bc bytes from hl to de.
+ ret
As mentioned in the 'Limitations' section, this alone is not enough because by this time, LoadEnemyMonData has already reset the enemy Pokémon's PP values to the max Therefore, even though we are now copying PP from wEnemyMonPP, it is too late. Also, the function which handles sending Pokémon to Bill's PC is separate from this.