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
- Create WRAM labels
- Modify the battle engine's enemy switch logic
- 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.