Scripted Losses - pret/pokered GitHub Wiki

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.

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