-
Notifications
You must be signed in to change notification settings - Fork 993
Scripted Losses
In Red/Blue, there is only one "scripted loss" (I use that term a bit loosely) in the game:
The initial battle you have against your rival in Prof. Oak's lab
By "scripted loss", I mean the result of the battle, win or lose, does not cause you to "black out", which also means winning or losing does not matter. There is no penalty for losing, the game just continues, win or lose.
How this was implemented is quite fascinating, and I want to share it as this sort of mechanic can be utilized for homebrewed story beats, or maybe even for a hidden trainer boss.
This mechanic relies on two files:
engine\battle\core.asm
and home\overworld.asm
Let's look at engine\battle\core.asm
first, at the HandlePlayerBlackout
symbol
; called when player is out of usable mons.
; prints appropriate lose message, sets carry flag if player blacked out (special case for initial rival fight)
HandlePlayerBlackOut:
ld a, [wLinkState]
cp LINK_STATE_BATTLING
jr z, .notRival1Battle
ld a, [wCurOpponent]
cp OPP_RIVAL1
jr nz, .notRival1Battle
hlcoord 0, 0 ; rival 1 battle
lb bc, 8, 21
call ClearScreenArea
call ScrollTrainerPicAfterBattle
ld c, 40
call DelayFrames
ld hl, Rival1WinText
call PrintText
ld a, [wCurMap]
cp OAKS_LAB
ret z ; starter battle in oak's lab: don't black out
.notRival1Battle
ld b, SET_PAL_BATTLE_BLACK
call RunPaletteCommand
ld hl, PlayerBlackedOutText2
ld a, [wLinkState]
cp LINK_STATE_BATTLING
jr nz, .noLinkBattle
ld hl, LinkBattleLostText
.noLinkBattle
call PrintText
ld a, [wd732]
res 5, a
ld [wd732], a
call ClearScreen
scf
ret
What the above code basically says is that when a player's last Pokemon has fainted, it's checking both if the battle was a link battle, and also if the battle was against your rival, specifically in Prof. Oak's lab, where you face him first and never have to face him there again.
This special condition tells this sub-routine to jump out early, without handling a black out if we lose during that particular battle.
So you would think, adding more conditions to check for specific battles in certain maps would be the only thing needed, right?
Well, that's where home/overworld.asm
comes into play, but more on that
in a moment. Let me show you how to edit this symbol to get the ball
rolling.
HandlePlayerBlackOut:
ld a, [wLinkState]
cp LINK_STATE_BATTLING
jr z, .notRival1Battle
ld a, [wCurOpponent]
+ cp <TRAINER ID> ; TRAINER ID can be found in constants\trainer_constants.asm
+ jr nz, .notThatTrainer
+ hlcoord 0, 0
+ lb bc, 8, 21
+ call ClearScreenArea
+ call ScrollTrainerPicAfterBattle
+ ld c, 40
+ call DelayFrames
+ ld hl, <WIN TEXT> ; WIN TEXT is whatever text you have set up
+ call PrintText
+ ld a, [wCurMap]
+ cp <MAP ID> ; MAP ID can be found in constants\map_constants.asm
+ ret
+ .notThatTrainer
cp OPP_RIVAL1
jr nz, .notRival1Battle
hlcoord 0, 0 ; rival 1 battle
lb bc, 8, 21
call ClearScreenArea
call ScrollTrainerPicAfterBattle
ld c, 40
call DelayFrames
ld hl, Rival1WinText
call PrintText
ld a, [wCurMap]
cp OAKS_LAB
ret z ; starter battle in oak's lab: don't black out
.notRival1Battle
ld b, SET_PAL_BATTLE_BLACK
call RunPaletteCommand
ld hl, PlayerBlackedOutText2
ld a, [wLinkState]
cp LINK_STATE_BATTLING
jr nz, .noLinkBattle
ld hl, LinkBattleLostText
.noLinkBattle
call PrintText
ld a, [wd732]
res 5, a
ld [wd732], a
call ClearScreen
scf
ret
I'm not going to dive too much on how to set up text pointers, but
I do want to mention core.asm
contains the text pointer for your
rival's victory text in the situation that the player loses. It's
defined right underneath the HandlePlayerBlackout
sub-routine.
Rival1WinText:
text_far _Rival1WinText
text_end
Now that we have that squared away on the battle engine side,
it's time to look at home\overworld.asm
, specifically in
the depths of the OverworldLoop
symbol.
.notCinnabarGym
ld hl, wd72e
set 5, [hl]
ld a, [wCurMap]
cp OAKS_LAB
jp z, .noFaintCheck ; no blacking out if the player lost to the rival in Oak's lab
callfar AnyPartyAlive
ld a, d
and a
jr z, .allPokemonFainted
.noFaintCheck
ld c, 10
call DelayFrames
jp EnterMap
.allPokemonFainted
ld a, $ff
ld [wIsInBattle], a
call RunMapScript
jp HandleBlackOut
This loop routine has a lot going on within it, and one thing it checks for, is if a battle had just occurred and if so, does the player have any usable Pokemon.
Strangely enough, the script that handles the overworld is what actually calls the routine to handle black out functions (halving the player's money and sending them to the last used Pokemon center, or your house in Pallett), which means there's a check for a battle that was initiated in Prof Oak's lab as well.
This is also easily edited to account for more scenarios
.notCinnabarGym
ld hl, wd72e
set 5, [hl]
ld a, [wCurMap]
cp OAKS_LAB
jp z, .noFaintCheck ; no blacking out if the player lost to the rival in Oak's lab
+ cp <MAP ID> ; Reminder: MAP ID is found in constants\map_constants.asm
+ jp z, .noFaintCheck
callfar AnyPartyAlive
ld a, d
and a
jr z, .allPokemonFainted
.noFaintCheck
ld c, 10
call DelayFrames
jp EnterMap
.allPokemonFainted
ld a, $ff
ld [wIsInBattle], a
call RunMapScript
jp HandleBlackOut
Once you edit this file, any save file that isn't a fresh one do break, as when you load back in, you will be locked in place.
Once you've made the necessary changes to both engine\battle\core.asm
and home\overworld.asm
, you have then implemented a new "scripted loss"
scenario.