Trainers switch their Pokemon out in a consecutive order instead of AI determining it - pret/pokecrystal GitHub Wiki

This tutorial guides you in creating a Linear Enemy Party routine.

What do I mean by that? Well, in Generation I, when you battled Trainers, after defeating a Pokemon they would always send out the next Pokemon in the party slot, as defined by their Trainer Party Data.

In Generation II, things are handled a little differently. After defeating a Trainer's Pokemon, the AI does a small calculation and determines which is the best Pokemon to send out to face you. This adds variety and replay value.

However, some Trainer Parties in Generation I were specifically designed to show off certain Pokemon in a certain order. For example, there is a Bug Catcher on Route 3 who sends out Weedle, Kakuna, Caterpie and Metapod in that order. This is clearly designed to show off an early evolution tree to the player on an early Route, where the player might be discovering such concepts for the first time. Even if you're aware of Evolution and have the Pokedex memorized, it can still seem jarring if the Trainer had the Generation II behaviour and decided to send out Weedle, Metapod, Caterpie, Kakuna.

Plus, perhaps you want this behaviour for certain custom trainers, if you wanted them to send out Articuno, Zapdos and Moltres in that order, or all three/four stages of an evolutionary line, or even if you just wanted some trainers to be more rigid than others.

Contents

  1. Create WRAM labels
  2. Modify the battle engine's enemy switch logic
  3. Decide which trainers you want to use the Linear Switching Type

1. Create WRAM Labels

First of all, we need to create two new WRAM labels. One to store a Flag, and one to store an Index. For that we'll edit ram/wram.asm and find somewhere where you can replace two unused bytes. You should be familiar with this through other tutorials, so I won't point you to any specific unused bytes - just know that you need space for two.

+ wEnemyMonLinearFlag:: db
+ wEnemyMonLinearIndex:: db	

And now let's initialize them. We can give them an initial value of 0 whenever we start a new game, and for that we'll edit engine/menus/intro_menu.asm.

 _ResetWRAM:
 	...
 	ld hl, wMomItemTriggerBalance
 	ld [hl], HIGH(MOM_MONEY >> 8)
 	inc hl
 	ld [hl], HIGH(MOM_MONEY) ; mid
 	inc hl
 	ld [hl], LOW(MOM_MONEY)
 
+	xor a
+	ld [wEnemyMonLinearFlag], a
+	ld [wEnemyMonLinearIndex], a
 
 	call InitializeNPCNames
 
 	farcall InitDecorations
 	...

These values will get cleared to zero elsewhere in our code, but it's nice practice to just have them cleared at the start.

2. Modify the battle engine's enemy switch logic.

We'll have to edit the battle engine to determine how the enemy Trainer will switch their Pokemon in after a defeat. First, let's go to engine/battle/core.asm and find CheckWhetherSwitchmonIsPredetermined:

CheckWhetherSwitchmonIsPredetermined gets called during the routines EnemySwitch and EnemySwitch_SetMode, and runs through a few options of predetermined decisions about Enemy Switching. The Pokemon getting switched in (determined by number) gets loaded into b, then it sets the carry flag and returns.

CheckWhetherSwitchmonIsPredetermined:
+ ; returns with carry flag (c) set and b = index (0-based) if predetermined
+ ; otherwise c clear and fall through to "normal"
+
+    ; forced switch (Roar/Whirlwind/Teleport) must bypass linear
+    ld a, [wForcedSwitch]
+    and a
+    jr nz, .normal
+	
+    ; if not using linear order, do normal
+    ld a, [wEnemyMonLinearFlag]
+    and a
+    jr z, .normal
+
+    ; use persistent zero-based linear pointer
+    ld a, [wEnemyLinearIndex]
+    ld b, a
+    jr .return_carry
+
+.normal	
; returns the enemy switchmon index in b, or
; returns carry if the index is not yet determined.
	ld a, [wLinkMode]
	and a
	jr z, .not_linked

	ld a, [wBattleAction]
	sub BATTLEACTION_SWITCH1
	ld b, a
	jr .return_carry

.not_linked
	ld a, [wEnemySwitchMonIndex]
	and a
	jr z, .check_wBattleHasJustStarted

	dec a
	ld b, a
	jr .return_carry

.check_wBattleHasJustStarted
	ld a, [wBattleHasJustStarted]
	and a
	ld b, 0
	jr nz, .return_carry

	and a
	ret

.return_carry
	scf
	ret

Still in engine/battle/core.asm, now find EnemyPartyMonEntrance:

EnemyPartyMonEntrance:
	push af
	xor a
	ld [wEnemySwitchMonIndex], a
	call NewEnemyMonStatus
	call ResetEnemyStatLevels
	call BreakAttraction
	pop af
	and a
	jr nz, .set
	call EnemySwitch
	jr .done_switch

.set
	call EnemySwitch_SetMode
.done_switch
	call ResetBattleParticipants
	call SetEnemyTurn
	call SpikesDamage
	xor a
	ld [wEnemyMoveStruct + MOVE_ANIM], a
	ld [wBattlePlayerAction], a
-	inc a

+; --- Linear Switching --- Advance persistent point to the next alive Pokemon after the one that just entered ---
+    ld a, [wEnemyMonLinearFlag]
+    and a
+    jr z, .linear_done
+
+;;  -- The next two lines will ensure that when you've Whirlwinded a Pokemon away and defeated the next one,
+;;  -- it will still go onto the next Pokemon in the list.
+
+     ld a, b                     ; b = chosen mon slot (already set earlier)
+    ld [wEnemyLinearIndex], a   ; update linear pointer to match actual entry
+
+    ; start from the slot AFTER the current one
+    ld a, [wCurOTMon]
+    inc a                         ; candidate = current + 1
+    ld b, a                       ; B = candidate (0-based)
+
+    ; C = party count
+    ld a, [wOTPartyCount]
+    ld c, a
+
+.linear_scan
+    ld a, b
+    cp c
+    jr nc, .linear_no_more        ; ran past end of party -> no next
+
+ ; Check for Alive Pokemon 
+    push bc
+    ld hl, wOTPartyMon1HP
+    call GetPartyLocation         ; expects A = index
+    ld a, [hli]
+    or [hl]
+    pop bc
+    jr nz, .linear_found          ; found an alive mon
+
+    inc b
+    jr .linear_scan
+
+.linear_found
+    ld a, b
+    ld [wEnemyLinearIndex], a     ; persist next (0-based)
+    jr .linear_done
+
+.linear_no_more
+    xor a
+    ld [wEnemyLinearIndex], a     ; no next; zero it (harmless)
+.linear_done
+; --- end LINEAR hook ---	
+	inc a 
+	ret

Still in engine/battle/core.asm, now find HandleEnemyMonFaint:

 HandleEnemyMonFaint:
	call FaintEnemyPokemon
	ld hl, wBattleMonHP
	ld a, [hli]
	or [hl]
	call z, FaintYourPokemon
	xor a
	ld [wWhichMonFaintedFirst], a
	call UpdateBattleStateAndExperienceAfterEnemyFaint
	call CheckPlayerPartyForFitMon
	ld a, d
	and a
	jp z, LostBattle


+	; --- Resync the Linear Mode Index (advance from the slot that just fainted) ---
+	ld a, [wEnemyMonLinearFlag]
+	and a
+	jr z, .no_linear_resync
+
+	ld a, [wCurOTMon]        ; 0-based slot of the mon that just fainted
+	inc a                    ; candidate = next slot
+	ld b, a
+	ld a, [wOTPartyCount]
+	ld c, a                  ; safety counter = party size
+	dec a                    ; A = last valid index (partycount - 1)
+	cp b
+	jr nc, .ok_linear
+	ld b, 0                  ; wrap if past end
+.ok_linear
+
+	; If wild battle, just store and exit
+	ld a, [wBattleMode]
+	dec a
+	jr z, .store_linear_index
+
+.check_loop
+	; if we've checked all slots, bail out
+	dec c
+	jr z, .all_fainted
+
+	; test candidate mon HP
+	ld a, b
+	ld hl, wOTPartyMon1HP
+	push bc
+	call GetPartyLocation
+	ld a, [hli]
+	or [hl]
+	pop bc
+	jr nz, .store_linear_index   ; found a living mon
+
+	; candidate fainted โ†’ advance and wrap
+	inc b
+	ld a, [wOTPartyCount]
+	dec a
+	cp b
+	jr nc, .check_loop
+	ld b, 0
+	jr .check_loop
+
+.all_fainted
+	; every slot fainted, donโ€™t update linear index
+	jr .no_linear_resync
+
+.store_linear_index
+	ld a, b
+	ld [wEnemyLinearIndex], a
+.no_linear_resync
+	; --- end resync ---

	ld hl, wBattleMonHP
	ld a, [hli]
	or [hl]
	call nz, UpdatePlayerHUD

        ...

And still in engine/battle/core.asm, just find CleanUpBattleRAM and make sure it clears these two values. They're cleared elsewhere in the code, but again, good practice to always be sure.

CleanUpBattleRAM:
	call BattleEnd_HandleRoamMons
	xor a
+	ld [wEnemyMonLinearFlag], a
+	ld [wEnemyLinearIndex], a
        ...

3. Decide which trainers you want to use the Linear Switching Type.

So now we need some kind of logic to determine which trainers use the Linear Switching type. How do we accomplish that? We can accomplish that by calling it when we talk to, or are seen by a trainer. But we can't call any code from the map script itself, as each trainer's .Script only happens upon the end of a battle when you return to the overworld

So, we go to engine/events/trainer_scripts.asm, and find TalkToTrainerScript and SeenByTrainerScript:

TalkToTrainerScript::
	faceplayer
	trainerflagaction CHECK_FLAG
	iftrue AlreadyBeatenTrainerScript
	loadtemptrainer
+       callasm DetermineSwitchOrder
	encountermusic
	sjump StartBattleWithMapTrainerScript
SeenByTrainerScript::
	loadtemptrainer
+	callasm DetermineSwitchOrder
	encountermusic
	showemote EMOTE_SHOCK, LAST_TALKED, 30
	callasm TrainerWalkToPlayer
	applymovementlasttalked wMovementBuffer
	writeobjectxy LAST_TALKED
	faceobject PLAYER, LAST_TALKED
	sjump StartBattleWithMapTrainerScript

Add this section at the end of the file. This checks the temporary Trainer ID [wTempTrainerID] of the trainer you just encountered in the overworld. Simply add a trainer (as per their label in Trainer Constants) that you want the Linear Switch Order to apply to (e.g. CAL2), to the list, and jump to .SetLinearFlag.

DetermineSwitchOrder:
    ld a, [wTempTrainerID]
	cp CAL2 ; This will be the name of your Trainer, as per Trainer Constants. 
	jr z, .SetLinearFlag
	ret
	
.SetLinearFlag
 ld a, 1
 ld [wEnemyMonLinearFlag], a
 ret

And voila! That's it! Now a Trainer will cycle through their Pokemon in a strict linear order. Even when you use Whirlwind and bring out another Pokemon, the sequence will stay updated.