Enemy Pokémon Use PP - pret/pokered GitHub Wiki

Introduction

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.

Limitations

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.

Decrementing the Enemy's PP

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"

Make Enemy Trainers Load PP From Their Party Data

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

Handing Enemy Pokémon PP = 0

Part 1: Struggling

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

Part 2: Preventing the Enemy From Choosing a Move With Zero PP

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

Make Disable only choose enemy moves that have PP left

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

[OPTIONAL] Additional Provisions for PP Reducing Moves

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

Appendix

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.

⚠️ **GitHub.com Fallback** ⚠️