Nuzlocke Mode - pret/pokered GitHub Wiki

Nuzlocke Mode

This tutorial will help you get a rudimentary Nuzlocke mode running. The following changes will be implemented

  1. Pokemon cannot be resurrected when fainted.
  2. You may only catch/obtain the first Pokemon you encounter/find per area.

Note, this tutorial will not cover the so-called "dupes clause".

DISCLAIMER: This tutorial is currently under construction. Let me know on Discord (@Xillicis) if you discover any bugs or have suggestions.

Contents

  1. Handling Fainted Pokemon
  2. Catch First Encounters
  3. Cannot Trade Dead Pokemon
  4. Edge Cases
  5. Game over
  6. Balancing

1. Handling Fainted Pokemon

Remove revive and max revive effects

We start by removing the effects of revive and max revive. The plan is to simply make these items unusable. You could do more work by completely removing them, but that is beyond the scope of this tutorial. Open the file engine/items/item_effects.asm and make the following modification.

...
.checkItemType
 	ld a, [wcf91]
 	cp REVIVE
-	jr nc, .healHP ; if it's a Revive or Max Revive
+	jp nc, .healingItemNoEffect
 	cp FULL_HEAL
 	jr z, .cureStatusAilment ; if it's a Full Heal
 	cp HP_UP
...

Head down a little bit further and make the changes.

...
.fainted
 	ld a, [wcf91]
	cp REVIVE
-	jr z, .updateInBattleFaintedData
+	jp .healingItemNoEffect
	cp MAX_REVIVE
-	jr z, .updateInBattleFaintedData
+	jp .healingItemNoEffect
.updateInBattleFaintedData
	ld a, [wIsInBattle]
...

Find the next MAX_REVIVE

...
	cp HYPER_POTION
 	jr c, .setCurrentHPToMaxHp ; if using a Full Restore or Max Potion
 	cp MAX_REVIVE
-	jr z, .setCurrentHPToMaxHp ; if using a Max Revive
+	jr z, .healingItemNoEffect
 	jr .updateInBattleData
 .setCurrentHPToHalfMaxHP
 	dec hl
...

Finding the last REVIVE and MAX_REVIVE, we make the changes.

...
	ld a, REVIVE_MSG
	ld [wPartyMenuTypeOrMessageID], a
	ld a, [wcf91]
	cp REVIVE
-	jr z, .showHealingItemMessage
+	jr z, .healingItemNoEffect
	cp MAX_REVIVE
-	jr z, .showHealingItemMessage
+	jr z, .healingItemNoEffect
	ld a, POTION_MSG
...

This does it for revive type items. Next we need to stop the Pokemon centers (or other locations) from fully reviving fainted Pokemon.

Disable revive effect from PokeCenters and other locations

Next we need to make sure the heal events will not heal a fainted Pokemon. We do this by first checking if the Pokemon has zero HP, if it does, then we skip the healing process. This is all be done in the file engine/events/heal_party.asm. We need to modify the HealParty: subroutine for this. The HealParty: subroutine is a bit complicated, as it needs to loop through each Pokemon and apply the healing affects. However, we just need to modify a small part of this subroutine. Make the following changes to HealParty:,

...
.nextmove
        dec b
        jr nz, .pp 
        pop de

        ld hl, wPartyMon1MaxHP - wPartyMon1HP
        add hl, de
+       ; Check if current mon is fainted
+       push hl     ; need to work on the hl register 
+       ld h, d
+       ld l, e
+       ld a, [hli] ; load current HP
+       or a, [hl]  ; check if zero HP
+       pop hl
+       jr z, .noHealIfFainted     
+       ; heal current mon if not fainted
	ld a, [hli]
	ld [de], a
	inc de
        ld a, [hl]
	ld [de], a

+.noHealIfFainted

	pop de
	pop hl

	push hl
	ld bc, wPartyMon2 - wPartyMon1
	ld h, d
...

There is a small trick to check if the value stored across 2-bytes (pointed to by hl in this case), is zero. That is, the instructions, ld a, [hli] and or a, [hl]. These two instructions first load the most significant byte of data pointed to by hl into a, increments hl to the least significant byte, and finally executes a binary OR operation. The result of the OR operation is zero if and only if those two bytes of data are both zero. Hence the Pokemon would have fainted.

First Rival Fight Doesn't Count

A common rule adopted by Nuzlocke players, is that losing the first rival fight doesn't mean you must restart the game. After the rival fight, HealParty is called, but we've changed it to not heal fainted Pokemon. Therefore, we need to manually restore the Pokemon's health. Head to scripts/OaksLab.asm and make the following changes,

...
        xor a ; SPRITE_FACING_DOWN
        ldh [hSpriteFacingDirection], a
        call SetSpriteFacingDirectionAndDelay
+; Restore the health of the Pokemon after 1st rival fight
+       ld hl, wPartyMon1MaxHP
+       ld de, wPartyMon1HP
+       ld a, [hli]
+       ld [de], a
+
+       inc de
+       ld a, [hl]
+       ld [de], a
-       predef HealParty
        SetEvent EVENT_BATTLED_RIVAL_IN_OAKS_LAB

        ld a, $d
        ld [wOaksLabCurScript], a
        ret
...

Note, we don't restore the PP, but its not really necessary. That should cover all possible circumstances for handling fainted Pokemon.

2. Catch First Encounters

The idea for this section to set a flag for each encounter area. We can only catch the Pokemon if the flag is set to 0, but the flag will be set to 1 immediately after the encounter. This makes it so that every subsequent encounter in that region will not be capturable.

Defining the encounter flags

The first thing we'll do is to allocate space for these Nuzlocke flags. In ram/wram.asm add the following line

...
wCurMapScript:: db

-        ds 7
+        ds 1
+
+wNuzlockeRegions:: ds 6

wPlayTimeHours:: db
...

We will need 6 bytes of data as as there are 43 locations for encountering Pokemon (43 < 6 * 8 = 48). Note the location in wram.asm to define these bytes is up to you. Depending on the modifications you've made to your code already, you may have to define these bytes else where.

In order to better keep track of these locations we define some constants add the end of the file constants/battle_constants.asm.

...
+; wNuzlockeRegions
+	const_def ; First byte 
+	const RESET_ROUTES_NUZ    ; 0
+	const PALLET_TOWN_NUZ     ; 1
+	const VIRIDIAN_CITY_NUZ   ; 2
+	const CERULEAN_CITY_NUZ   ; 3
+	const VERMILION_CITY_NUZ  ; 4
+	const CELADON_CITY_NUZ    ; 5
+	const FUCHSIA_CITY_NUZ    ; 6
+	const CINNABAR_ISLAND_NUZ ; 7
	
+	const_def ; Second byte
+	const SAFFRON_CITY_NUZ    ; 0
+	const ROUTE_1_NUZ         ; 1
+	const ROUTE_2_NUZ         ; 2
+	const ROUTE_3_NUZ         ; 3
+	const ROUTE_4_NUZ         ; 4
+	const ROUTE_5_NUZ         ; 5
+	const ROUTE_6_NUZ         ; 6
+	const ROUTE_7_NUZ         ; 7
		
+	const_def ; Third byte
+	const ROUTE_8_NUZ         ; 0
+	const ROUTE_9_NUZ         ; 1
+	const ROUTE_10_NUZ        ; 2
+	const ROUTE_11_NUZ        ; 3
+	const ROUTE_12_NUZ        ; 4
+	const ROUTE_13_NUZ        ; 5
+	const ROUTE_14_NUZ        ; 6
+	const ROUTE_15_NUZ        ; 7
	
+	const_def ; Fourth byte
+	const ROUTE_16_NUZ        ; 0
+	const ROUTE_17_NUZ        ; 1
+	const ROUTE_18_NUZ        ; 2
+	const ROUTE_19_NUZ        ; 3
+	const ROUTE_20_NUZ        ; 4
+	const ROUTE_21_NUZ        ; 5
+	const ROUTE_22_NUZ        ; 6
+	const ROUTE_23_NUZ        ; 7
		
+	const_def ; Fifth byte
+	const ROUTE_24_NUZ        ; 0
+	const ROUTE_25_NUZ        ; 1
+	const VIRIDIAN_FOREST_NUZ ; 2
+	const MT_MOON_NUZ         ; 3
+	const ROCK_TUNNEL_NUZ     ; 4
+	const POWER_PLANT_NUZ     ; 5
+	const VICTORY_ROAD_NUZ    ; 6
+	const POKEMON_TOWER_NUZ   ; 7

+	const_def ; Sixth byte
+	const SEAFOAM_ISLANDS_NUZ ; 0
+	const DIGLETTS_CAVE_NUZ   ; 1
+	const POKEMON_MANSION_NUZ ; 2
+	const SAFARI_ZONE_NUZ     ; 3

These constant values will be used to reference the different bits of the different bytes of wNuzlockeRegions.

Now we need to set the appropriate Nuzlocke flag after we encounter a Pokemon and also to check the flag to determine if a Pokemon is catchable. We add two subroutines to do this: setNuzlockeFlag and checkNuzlockeStatus. These two functions are added into a new file, nuzlocke.asm in the engine/ directory. The file is long and cumbersome, so I recommend just copy pasting it in. Is it elegent? No. Is there a better way to do this? Probably. Feel free to suggest a better implementation if you have one.

setNuzlockeFlag::
	ld hl, wNuzlockeRegions
	ld a, [wCurMap]
	cp INDIGO_PLATEAU
	jr nc, .routeRegion1
	cp PALLET_TOWN
	jr z, .nuzPalletTownFlag
	cp VIRIDIAN_CITY
	jr z, .nuzViridianCityFlag
	cp CERULEAN_CITY
	jr z, .nuzCeruleanCityFlag
	cp VERMILION_CITY
	jr z, .nuzVermilionCityFlag
        cp VERMILION_DOCK
        jr z, .nuzVermilionCityFlag
	cp CELADON_CITY
	jr z, .nuzCeladonCityFlag
	cp FUCHSIA_CITY
	jr z, .nuzFuchsiaCityFlag
	cp CINNABAR_ISLAND
	jr z, .nuzCinnabarIslandFlag
	
.nuzPalletTownFlag
	set PALLET_TOWN_NUZ, [hl]	
	ret
.nuzViridianCityFlag
	set VIRIDIAN_CITY_NUZ, [hl]	
	ret
.nuzCeruleanCityFlag
	set CERULEAN_CITY_NUZ, [hl]	
	ret
.nuzVermilionCityFlag
	set VERMILION_CITY_NUZ, [hl]	
	ret
.nuzCeladonCityFlag
	set CELADON_CITY_NUZ, [hl]	
	ret
.nuzFuchsiaCityFlag
	set FUCHSIA_CITY_NUZ, [hl]
	ret
.nuzCinnabarIslandFlag
	set CINNABAR_ISLAND_NUZ, [hl]
	ret

.routeRegion1
	inc hl
	cp ROUTE_8
	jr nc, .routeRegion2
	cp SAFFRON_CITY
	jr z, .nuzSaffronCityFlag
	cp ROUTE_1
	jr z, .nuzRoute1Flag
	cp ROUTE_2
	jr z, .nuzRoute2Flag
	cp ROUTE_3
	jr z, .nuzRoute3Flag
	cp ROUTE_4
	jr z, .nuzRoute4Flag
	cp ROUTE_5
	jr z, .nuzRoute5Flag
	cp ROUTE_6
	jr z, .nuzRoute6Flag
	cp ROUTE_7
	jr z, .nuzRoute7Flag
	
.nuzSaffronCityFlag
	set SAFFRON_CITY_NUZ, [hl]	
	ret	
.nuzRoute1Flag
	set ROUTE_1_NUZ, [hl]	
	ret
.nuzRoute2Flag
	set ROUTE_2_NUZ, [hl]
	ret
.nuzRoute3Flag
	set ROUTE_3_NUZ, [hl]
	ret
.nuzRoute4Flag
	set ROUTE_4_NUZ, [hl]
	ret
.nuzRoute5Flag
	set ROUTE_5_NUZ, [hl]
	ret
.nuzRoute6Flag
	set ROUTE_6_NUZ, [hl]
	ret
.nuzRoute7Flag
	set ROUTE_7_NUZ, [hl]
	ret

.routeRegion2
	inc hl
	cp ROUTE_16
	jr nc, .routeRegion3
	cp ROUTE_8
	jr z, .nuzRoute8Flag
	cp ROUTE_9
	jr z, .nuzRoute9Flag
	cp ROUTE_10
	jr z, .nuzRoute10Flag
	cp ROUTE_11
	jr z, .nuzRoute11Flag
	cp ROUTE_12
	jr z, .nuzRoute12Flag
	cp ROUTE_13
	jr z, .nuzRoute13Flag
	cp ROUTE_14
	jr z, .nuzRoute14Flag
	cp ROUTE_15
	jr z, .nuzRoute15Flag

.nuzRoute8Flag
	set ROUTE_8_NUZ, [hl]
	ret
.nuzRoute9Flag
	set ROUTE_9_NUZ, [hl]
	ret
.nuzRoute10Flag
	set ROUTE_10_NUZ, [hl]
	ret
.nuzRoute11Flag
	set ROUTE_11_NUZ, [hl]
	ret
.nuzRoute12Flag
	set ROUTE_12_NUZ, [hl]
	ret
.nuzRoute13Flag
	set ROUTE_13_NUZ, [hl]
	ret
.nuzRoute14Flag
	set ROUTE_14_NUZ, [hl]
	ret
.nuzRoute15Flag
	set ROUTE_15_NUZ, [hl]
	ret

.routeRegion3
	inc hl
	cp ROUTE_24
	jr nc, .routeRegion4
	cp ROUTE_16
	jr z, .nuzRoute16Flag
	cp ROUTE_17
	jr z, .nuzRoute17Flag
	cp ROUTE_18
	jr z, .nuzRoute18Flag
	cp ROUTE_19
	jr z, .nuzRoute19Flag
	cp ROUTE_20
	jr z, .nuzRoute20Flag
	cp ROUTE_21
	jr z, .nuzRoute21Flag
	cp ROUTE_22
	jr z, .nuzRoute22Flag
	cp ROUTE_23
	jr z, .nuzRoute23Flag

.nuzRoute16Flag
	set ROUTE_16_NUZ, [hl]
	ret
.nuzRoute17Flag
	set ROUTE_17_NUZ, [hl]
	ret
.nuzRoute18Flag
	set ROUTE_18_NUZ, [hl]
	ret
.nuzRoute19Flag
	set ROUTE_19_NUZ, [hl]
	ret
.nuzRoute20Flag
	set ROUTE_20_NUZ, [hl]
	ret
.nuzRoute21Flag
	set ROUTE_21_NUZ, [hl]
	ret
.nuzRoute22Flag
	set ROUTE_22_NUZ, [hl]
	ret
.nuzRoute23Flag
	set ROUTE_23_NUZ, [hl]
	ret

.routeRegion4
	inc hl
	cp MR_FUJIS_HOUSE
	jr nc, .finalRegion
	cp ROUTE_24
	jr z, .nuzRoute24Flag
	cp ROUTE_25
	jr z, .nuzRoute25Flag
	cp VIRIDIAN_FOREST
	jr z, .nuzViridianForestFlag
	cp CERULEAN_TRASHED_HOUSE ; This will check all the Mt. Moon floors
	jr c, .nuzMtMoonFlag
        CERULEAN_GYM ; The gym counts for Cerulean city
        jp z, .nuzCeruleanCityFlag
	CP ROCK_TUNNEL_1F
	jr z, .nuzRockTunnelFlag
	cp POWER_PLANT
	jr z, .nuzPowerPlantFlag
	cp VICTORY_ROAD_1F
	jr z, .nuzVictoryRoadFlag
	jr .nuzPokemonTowerFlag ; Last remaining option is Pokemon Tower

.nuzRoute24Flag
	set ROUTE_24_NUZ, [hl]
	ret
.nuzRoute25Flag
	set ROUTE_25_NUZ, [hl]
	ret
.nuzViridianForestFlag
	set VIRIDIAN_FOREST_NUZ, [hl]
	ret
.nuzMtMoonFlag
	set MT_MOON_NUZ, [hl]
	ret
.nuzRockTunnelFlag
	set ROCK_TUNNEL_NUZ, [hl]
	ret
.nuzPowerPlantFlag
	set POWER_PLANT_NUZ, [hl]
	ret
.nuzVictoryRoadFlag
	set VICTORY_ROAD_NUZ, [hl]
	ret
.nuzPokemonTowerFlag
	set POKEMON_TOWER_NUZ, [hl]
	ret

.finalRegion
	inc hl
	cp VERMILION_OLD_ROD_HOUSE
	jr c, .nuzSeafoamIslandsFlag ; This checks B1F, B2F, B3F, and B4F
	cp POKEMON_MANSION_1F
	jr z, .nuzPokemonMansionFlag
	cp SEAFOAM_ISLANDS_1F
	jr z, .nuzSeafoamIslandsFlag
	cp DIGLETTS_CAVE
	jr z, .nuzDiglettsCaveFlag
	cp ROCKET_HIDEOUT_B1F ; Checks for victory road 2F and 3F
	jr c, .nuzVictoryRoadFlag
	cp SAFARI_ZONE_EAST ; Check pokemon mansion 2F, 3F, and B1F
	jr c, .nuzPokemonMansionFlag
	cp SAFARI_ZONE_CENTER_REST_HOUSE
	jr c, .nuzSafariZoneFlag
	jr .nuzRockTunnelFlag ; Last remaining zone for nuzlocke is rock tunnel B1F

.nuzSeafoamIslandsFlag
	set SEAFOAM_ISLANDS_NUZ, [hl]
	ret
.nuzDiglettsCaveFlag
	set DIGLETTS_CAVE_NUZ, [hl]
	ret
.nuzPokemonMansionFlag
	set POKEMON_MANSION_NUZ, [hl]
	ret
.nuzSafariZoneFlag
	set SAFARI_ZONE_NUZ, [hl]
	ret

checkNuzlockeStatus::
        ld hl, wNuzlockeRegions
        ld a, [wCurMap]
        cp INDIGO_PLATEAU
        jr nc, .routeRegion1
        cp PALLET_TOWN
        jr z, .nuzPalletTown
        cp VIRIDIAN_CITY
        jr z, .nuzViridianCity
        cp CERULEAN_CITY
        jr z, .nuzCeruleanCity
        cp VERMILION_CITY
        jr z, .nuzVermilionCity
        cp VERMILION_DOCK
        jr z, .nuzVermilionCity
        cp CELADON_CITY
        jr z, .nuzCeladonCity
	cp FUCHSIA_CITY
	jr z, .nuzFuchsiaCity
        cp CINNABAR_ISLAND
        jr z, .nuzCinnabarIsland
        
.nuzPalletTown
	bit PALLET_TOWN_NUZ, [hl]	
	ret
.nuzViridianCity
	bit VIRIDIAN_CITY_NUZ, [hl]	
	ret
.nuzCeruleanCity
	bit CERULEAN_CITY_NUZ, [hl]	
	ret
.nuzVermilionCity
	bit VERMILION_CITY_NUZ, [hl]	
	ret
.nuzCeladonCity
	bit CELADON_CITY_NUZ, [hl]	
	ret
.nuzFuchsiaCity
	bit FUCHSIA_CITY_NUZ, [hl]
	ret
.nuzCinnabarIsland
	bit CINNABAR_ISLAND_NUZ, [hl]
	ret

.routeRegion1
        inc hl
        cp ROUTE_8
        jr nc, .routeRegion2
	cp SAFFRON_CITY
        jr z, .nuzSaffronCity
        cp ROUTE_1
        jr z, .nuzRoute1
        cp ROUTE_2
        jr z, .nuzRoute2
        cp ROUTE_3
        jr z, .nuzRoute3
        cp ROUTE_4
        jr z, .nuzRoute4
        cp ROUTE_5
        jr z, .nuzRoute5
        cp ROUTE_6
        jr z, .nuzRoute6
        cp ROUTE_7
        jr z, .nuzRoute7

.nuzSaffronCity
	bit SAFFRON_CITY_NUZ, [hl]	
	ret	
.nuzRoute1
	bit ROUTE_1_NUZ, [hl]	
	ret
.nuzRoute2
	bit ROUTE_2_NUZ, [hl]
	ret
.nuzRoute3
	bit ROUTE_3_NUZ, [hl]
	ret
.nuzRoute4
	bit ROUTE_4_NUZ, [hl]
	ret
.nuzRoute5
	bit ROUTE_5_NUZ, [hl]
	ret
.nuzRoute6
	bit ROUTE_6_NUZ, [hl]
	ret
.nuzRoute7
	bit ROUTE_7_NUZ, [hl]
	ret

.routeRegion2
        inc hl
        cp ROUTE_16
        jr nc, .routeRegion3
	cp ROUTE_8
        jr z, .nuzRoute8
        cp ROUTE_9
        jr z, .nuzRoute9
        cp ROUTE_10
        jr z, .nuzRoute10
        cp ROUTE_11
        jr z, .nuzRoute11
        cp ROUTE_12
        jr z, .nuzRoute12
        cp ROUTE_13
        jr z, .nuzRoute13
        cp ROUTE_14
        jr z, .nuzRoute14
        cp ROUTE_15
        jr z, .nuzRoute15

.nuzRoute8
	bit ROUTE_8_NUZ, [hl]
	ret
.nuzRoute9
	bit ROUTE_9_NUZ, [hl]
	ret
.nuzRoute10
	bit ROUTE_10_NUZ, [hl]
	ret
.nuzRoute11
	bit ROUTE_11_NUZ, [hl]
	ret
.nuzRoute12
	bit ROUTE_12_NUZ, [hl]
	ret
.nuzRoute13
	bit ROUTE_13_NUZ, [hl]
	ret
.nuzRoute14
	bit ROUTE_14_NUZ, [hl]
	ret
.nuzRoute15
	bit ROUTE_15_NUZ, [hl]
	ret

.routeRegion3
        inc hl
        cp ROUTE_24
        jr nc, .routeRegion4
	cp ROUTE_16
        jr z, .nuzRoute16
        cp ROUTE_17
        jr z, .nuzRoute17
        cp ROUTE_18
        jr z, .nuzRoute18
        cp ROUTE_19
        jr z, .nuzRoute19
        cp ROUTE_20
        jr z, .nuzRoute20
        cp ROUTE_21
        jr z, .nuzRoute21
        cp ROUTE_22
        jr z, .nuzRoute22
        cp ROUTE_23
        jr z, .nuzRoute23
       
.nuzRoute16
	bit ROUTE_16_NUZ, [hl]
	ret
.nuzRoute17
	bit ROUTE_17_NUZ, [hl]
	ret
.nuzRoute18
	bit ROUTE_18_NUZ, [hl]
	ret
.nuzRoute19
	bit ROUTE_19_NUZ, [hl]
	ret
.nuzRoute20
	bit ROUTE_20_NUZ, [hl]
	ret
.nuzRoute21
	bit ROUTE_21_NUZ, [hl]
	ret
.nuzRoute22
	bit ROUTE_22_NUZ, [hl]
	ret
.nuzRoute23
	bit ROUTE_23_NUZ, [hl]
	ret

.routeRegion4
        inc hl
        cp MR_FUJIS_HOUSE
        jr nc, .finalRegion
	cp ROUTE_24
        jr z, .nuzRoute24
        cp ROUTE_25
        jr z, .nuzRoute25
        cp VIRIDIAN_FOREST
        jr z, .nuzViridianForest
        cp CERULEAN_TRASHED_HOUSE ; This will check all the Mt. Moon floors
        jr c, .nuzMtMoon
        CERULEAN_GYM ; The gym counts for Cerulean city
        jp z, .nuzCeruleanCity
        CP ROCK_TUNNEL_1F
        jr z, .nuzRockTunnel
        cp POWER_PLANT
        jr z, .nuzPowerPlant
        cp VICTORY_ROAD_1F
        jr z, .nuzVictoryRoad
        jr .nuzPokemonTower  ; Last remaining option is Pokemon Tower

.nuzRoute24
	bit ROUTE_24_NUZ, [hl]
	ret
.nuzRoute25
	bit ROUTE_25_NUZ, [hl]
	ret
.nuzViridianForest
	bit VIRIDIAN_FOREST_NUZ, [hl]
	ret
.nuzMtMoon
	bit MT_MOON_NUZ, [hl]
	ret
.nuzRockTunnel
	bit ROCK_TUNNEL_NUZ, [hl]
	ret
.nuzPowerPlant
	bit POWER_PLANT_NUZ, [hl]
	ret
.nuzVictoryRoad
	bit VICTORY_ROAD_NUZ, [hl]
	ret
.nuzPokemonTower
	bit POKEMON_TOWER_NUZ, [hl]
	ret

.finalRegion
        inc hl
        cp VERMILION_OLD_ROD_HOUSE
        jr c, .nuzSeafoamIslands  ; This checks B1F, B2F, B3F, and B4F
        cp POKEMON_MANSION_1F
        jr z, .nuzPokemonMansion
        cp SEAFOAM_ISLANDS_1F
        jr z, .nuzSeafoamIslands
        cp DIGLETTS_CAVE
        jr z, .nuzDiglettsCave
        cp ROCKET_HIDEOUT_B1F ; Checks for victory road 2F and 3F
        jr c, .nuzVictoryRoad
        cp SAFARI_ZONE_EAST ; Check pokemon mansion 2F, 3F, and B1F
        jr c, .nuzPokemonMansion
        cp SAFARI_ZONE_CENTER_REST_HOUSE
        jr c, .nuzSafariZone
        jr .nuzRockTunnel  ; Last remaining zone for nuzlocke is rock tunnel B1F

.nuzSeafoamIslands
	bit SEAFOAM_ISLANDS_NUZ, [hl]
	ret
.nuzDiglettsCave
	bit DIGLETTS_CAVE_NUZ, [hl]
	ret
.nuzPokemonMansion
	bit POKEMON_MANSION_NUZ, [hl]
	ret
.nuzSafariZone
	bit SAFARI_ZONE_NUZ, [hl]
	ret

Since we are adding a new file, we also need to include it in main.asm. So head to main.asm and add the line

...
INCLUDE "engine/menus/players_pc.asm"
INCLUDE "engine/pokemon/remove_mon.asm"
INCLUDE "engine/events/display_pokedex.asm"
+INCLUDE "engine/nuzlocke.asm"

...

There are few things to mention about this code. The ordering of the different locations is based on the ordering in constants/map_constants.asm. The code also handles regions like MT_MOON; that is, it considers all the different floors that you could be on and treats it as a single location.

The second thing you may notice is that the very first bit in wNuzlockeRegions is not specified. This bit will be used in order to initialize the Nuzlocke mode after acquiring Pokeballs. I personally, believe a Nuzlocke doesn't start until one acquires a Pokeball, so we will add some more changes to reset the Nuzlocke flags for ROUTE 1 and ROUTE 2 as these two regions can be explored before acquiring Pokeballs.

Reset Route 1 and Route 2 for Nuzlocke mode

Head to engine/items/inventory.asm and make the following changes:

...
	ld c, a
        ld b, 0
        add hl, bc ; hl = address to store the item
        ld a, [wcf91]
+
+       cp POKE_BALL
+	push hl
+	jr nz, .noNuzlockeReset
+
+.startNuzlocke
+       ld hl, wNuzlockeRegions
+       bit RESET_ROUTES_NUZ, [hl] ; Check if we have already aquired pokeballs
+       jr nz, .noNuzlockeReset
+       set RESET_ROUTES_NUZ, [hl] ; Set the flag declaring the start of the Nuzlocke
+       inc hl      ; Increment to the next byte which points to the routes
+       res ROUTE_1_NUZ, [hl] ; Reset Route 1 Nuzlocke flag
+       res ROUTE_2_NUZ, [hl] ; Reset Route 2 Nuzlocke flag
+
+.noNuzlockeReset
+       pop hl
	ld [hli], a ; store item ID
        ld a, [wItemQuantity]
        ld [hli], a ; store item quantity
        ld [hl], $ff ; store terminator
...

This code checks to see if we are adding a Pokeball to our inventory (which would happen at the first Pokemart in Viridian City). It then checks to see if this is the first time we are getting a Pokeball and if it is the first time, then we reset Routes 1 and 2.

That does it for all the initializing; we now need to handle the Pokemon encounters and a few edge cases.

Handling Nuzlocke encounters

For wild Nuzlocke encounters there are essentially four cases to handle.

  1. The wild Pokemon flees.
  2. You defeat the wild Pokemon.
  3. You run away from the wild Pokemon.
  4. You or the wild Pokemon casts Roar, Whirlwind, or Teleport.

For the first case head to engine/battle/core.asm and set the Nuzlocke flag,

...
EnemyRan:
	call LoadScreenTilesFromBuffer1
+       callfar setNuzlockeFlag
	ld a, [wLinkState]
	cp LINK_STATE_BATTLING
	ld hl, WildRanText
	jr nz, .printText
; link battle
        xor a
        ld [wBattleResult], a
        ld hl, EnemyRanText
...

Head down a little bit further and add the "set Nuzlocke flag" for defeating the wild Pokemon in the subroutine FaintEnemyPokemon:,

...
.wild_win
        call EndLowHealthAlarm
+       callfar setNuzlockeFlag
        ld a, MUSIC_DEFEATED_WILD_MON
        call PlayBattleVictoryMusic
...

For the third case, in the same file, head down to the subroutine TryRunningFromBattle: and make the following change,

...
	and a ; reset carry
        ret
.canEscape
+       callfar setNuzlockeFlag
        ld a, [wLinkState]
        cp LINK_STATE_BATTLING
...

This handles the case for fleeing from battle. Finally, we need to account for the case when roar, whirlwind, or teleport is used. We make the following change in engine/battle/effects.asm, to the subroutine SwitchAndTeleportEffect:,

...
        cp ROAR
        jr z, .printText
        ld hl, WasBlownAwayText
.printText
+       push hl
+       callfar setNuzlockeFlag
+       pop hl
        jp PrintText
...

Note we push hl and pop hl since the callfar instruction modifies the hl register and we need that for printing the text.

Indicator for catchable Pokemon

We need to let the player know that a Pokemon is or is not catchable, as the player may not be keeping track of all the locations. To do this, we will simply display some extra text letting the player know that they can catch the Pokemon.

First, we will create the text that is displayed when the Pokemon is catchable. Open up data/text/text_2.asm and add the following,

...
+_WildMonCatchableText::
+	text "@"
+	text_ram wEnemyMonNick
+	text_start
+	line "is catchable!"
+	prompt

_HookedMonAttackedText::
 	text "The hooked"
...

Now we display this text if the Pokemon is catchable, otherwise, no additional text is displayed. Open up engine/battle/common_text.asm and add the following,

...
	callfar DrawAllPokeballs
 	pop hl
 	call PrintText
+       ld a, [wIsInBattle]
+       dec a 
+       jr nz, .done ; Check if trainer battle or not
+	callfar checkNuzlockeStatus
+	jr nz, .done
+	ld hl, WildMonCatchableText
+	call PrintText
 	jr .done
 .pokemonTower
 	ld b, SILPH_SCOPE
...

and a bit further down add

...
WildMonAppearedText:
        text_far _WildMonAppearedText
        text_end

+WildMonCatchableText:
+       text_far _WildMonCatchableText
+       text_end
...

And that covers it for this section. I'd prefer a visual cue, like a some kind of Pokeball next to the Pokemon name or health bar for indicating the catchable status. But currently that is beyond my ability. Edit this wiki if you have a way to do that.

3. Cannot Trade Dead Pokemon

We now need to account for the in-game trades. Without making any modifications, the player would be able to trade dead Pokemon and receive a new fully healed Pokemon. We will modify the in-game trades to check that the Pokemon is not dead, if it is, then we will display some appropriate text. The modifications must be made in engine/events/in_game_trades.asm. Make the following change to the subroutine InGameTrade_DoTrade:,

InGameTrade_DoTrade:
        xor a ; NORMAL_PARTY_MENU
        ld [wPartyMenuTypeOrMessageID], a
        dec a
        ld [wUpdateSpritesEnabled], a
        call DisplayPartyMenu
        push af
        call InGameTrade_RestoreScreen
        pop af
        ld a, $1
        jp c, .tradeFailed ; jump if the player didn't select a pokemon
        ld a, [wInGameTradeGiveMonSpecies]
        ld b, a
        ld a, [wcf91]
        cp b
        ld a, $2
        jp nz, .tradeFailed ; jump if the selected mon's species is not the required one
+       ; check for dead mon
+       ld a, [wWhichPokemon]
+       ld hl, wPartyMon1HP
+       ld bc, wPartyMon2 - wPartyMon1
+       call AddNTimes ; iterate to the selected mon
+       ld a, [hli]    ; load current HP
+       or a, [hl]     ; check if zero HP
+       ld a, $5       ; load indicator for dead mon dialog
+       jr z, .tradeFailed
        ld a, [wWhichPokemon]
        ld hl, wPartyMon1Level
        ld bc, wPartyMon2 - wPartyMon1
        call AddNTimes
...

The variable wWhichPokemon stores a number between 0 and 5 and indicates which of the 6 Pokemon we are considering. Because of the data structure for Pokemon's stats, we start by loading the first Pokemon's HP, wPartyMon1HP and then adding wPartyMon2 - wPartyMon1 to wPartyMon1HP, wWhichPokemon times. Doing this allows hl to point to the HP of the selected Pokemon, whether it's the first Pokemon in our part or the last. For checking the HP, we repeat the same process just like when we modified the healing event. We check if the current Pokemon is fainted by the instructions, ld a, [hli] and or a, [hl]. Lastly, we load $5 into a which is used to load the dialog for trying to trade a dead Pokemon.

Head down a bit further and we will add the text pointers for the dead Pokemon dialog,

...
TradeTextPointers1:
        dw WannaTrade1Text
        dw NoTrade1Text
        dw WrongMon1Text
        dw Thanks1Text
        dw AfterTrade1Text
+       dw DeadMon1Text

TradeTextPointers2:
        dw WannaTrade2Text
        dw NoTrade2Text
        dw WrongMon2Text
        dw Thanks2Text
        dw AfterTrade2Text
+       dw DeadMon2Text

TradeTextPointers3:
        dw WannaTrade3Text
        dw NoTrade3Text
        dw WrongMon3Text
        dw Thanks3Text
        dw AfterTrade3Text
+       dw DeadMon3Text
...

and also add the text_far's for loading the dialog,

...
AfterTrade1Text:
        text_far _AfterTrade1Text
        text_end

+DeadMon1Text:
+       text_far _DeadMon1Text
+       text_end

+DeadMon2Text:
+       text_far _DeadMon2Text
+       text_end

+DeadMon3Text:
+       text_far _DeadMon3Text
+       text_end

WannaTrade2Text:
        text_far _WannaTrade2Text
        text_end
...

Finally, we can add the dialog in data/text/text_7.asm

...
+_DeadMon1Text::
+       text "Come on man."
    
+       para "Trying to trade"
+       line "a dead #MON?"
+       done

+_DeadMon2Text::
+       text "Wow. You don't"
+       line "have any shame!"

+       para "Trading dead"
+       line "#MON!"
+       done

+_DeadMon3Text::
+       text "I can't believe"
+       line "you would try to"
+       cont "trade a dead"
+       cont "#MON!"

+       para "You don't deserve"
+       line "those badges!"
+       done
...

You can add these dialogs at the bottom of the file if you wish. The reason there are three different dialogs is because, out of all the in-game trades you can make, they only have 3 different sets of dialog. You can see these three different dialog types in data/events/trades.asm.

4. Edge Cases

This section mainly concerns gifted Pokemon or Pokemon attained through the Gamecorner. Some people allow these Pokemon to be attained independently of the Nuzlocke rules, but we will not be giving any freebies. This section details how to actually handle these cases for the Nuzlocke rules.

Magikarp salesman at Mt. Moon

For the Magikarp salesman, we will check if the Pokemon has been encountered already on Route 3. If the player has, then they will be unable to purchase the Magikarp and a different text will be displayed.

First, lets add the alternative text for when the Route 3 flag has been set. Make the following changes to text/MtMoonPokecenter.asm

...
+_MagikarpSalesmanNoPokemon::
+	text "What do I look"
+       line "like? A MAGIKARP"
+       cont "salesman?"
+
+       para "Get outa' here!"
+       done
...

You're free to make him say whatever. Also this text can be added anywhere in this file (except in the middle of a subroutine!)

Now open up scripts/MtMoonPokecenter.asm and make the following changes,

...
MagikarpSalesmanText:
        text_asm
        CheckEvent EVENT_BOUGHT_MAGIKARP, 1
        jp c, .alreadyBoughtMagikarp
+       ; Since our current position is not in one of the regions defined 
+       ; in checkNuzlockeStatus, we manually check the status.
+       ld hl, wNuzlockeRegions
+       inc hl
+       bit ROUTE_3_NUZ, [hl] 
+       jr nz, .noPokemon
        ld hl, .Text1
        call PrintText
...

and

...
        jr .printText
.enoughMoney
        lb bc, MAGIKARP, 5
+       ; Hard set Route 3 when purchasing Pokemon.
+       ld hl, wNuzlockeRegions
+       inc hl
+       set ROUTE_3_NUZ, [hl]   
        call GivePokemon
        jr nc, .done
...

I also recommend changing the Pokemon from MAGIKARP to something like GOLDEEN as Gyarados is often considered overpowered in Nuzlockes. But that's entirely up to you.

Celadon City

In Celadon City, one can aquire an Eevee at the top of the Celadon mansion or one can purchase a Pokemon from the Game Corner using coins. We only want to allow the player one of these two options. Starting with the Game Corner, open up engine/events/prize_menu.asm and make the following change at the beginning,

CeladonPrizeMenu::
+       ld hl, wNuzlockeRegions
+       bit CELADON_CITY, [hl]
+       jr z, .nuzGameCornerOkay
+       ld hl, NuzlockeNoPrizeTextPtr
+       jp PrintText
+.nuzGameCornerOkay
        ld b, COIN_CASE
        call IsItemInBag
        jr nz, .havingCoinCase
...
        cp 3 ; "NO,THANKS" choice
        jr z, .noChoice
        call HandlePrizeChoice
.noChoice
        ld hl, wd730
        res 6, [hl]
        ret

+NuzlockeNoPrizeTextPtr:
+       text_far _NuzlockeNoPrizeText
+       text_waitbutton
+       text_end

RequireCoinCaseTextPtr:
        text_far _RequireCoinCaseText
        text_waitbutton
        text_end
...

This change checks if we have already aquired a Pokemon in Celadon City, if we have, then we will print some text saying we can't purchase a Pokemon. The last thing to modify in the file, is to set the Nuzlocke flag once we have purchased a Pokemon. In the subroutine HandlePrizeChocie add the following,

...
        call z, WaitForTextScrollButtonPress
        pop af

; If the mon couldn't be given to the player (because both the party and box
; were full), return without subtracting coins.
        ret nc
+       ld hl, wNuzlockeRegions
+       set CELADON_CITY, [hl]     

.subtractCoins
...

Next we need to add the text that is displayed at the Game Corner if the Nuzlocke flag has been set. Open data/text/text_2.asm and add the following,

...
_MonWasReleasedText::
        text_ram wStringBuffer
        text " was" 
        line "released outside."
        cont "Bye @"
        text_ram wStringBuffer
        text "!"
        prompt

+_NuzlockeNoPrizeText::
+       text "Sorry, prizes are" 
+       line "unavailable.@"
+       text_end

_RequireCoinCaseText::
        text "A COIN CASE is"
        line "required!@"
        text_end
...

Now to handl the Eevee at the top of the Celadon Mansion. Open scripts/CeladMansionRoofHouse.asm and make the change,

...
CeladonMansion5Text2:
        text_asm
+       ld hl, wNuzlockeRegions
+       bit CELADON_CITY, [hl]
+       jr nz, .nuzNoGift
        lb bc, EEVEE, 25
        call GivePokemon
        jr nc, .party_full
+       ld hl, wNuzlockeRegions
+       set CELADON_CITY, [hl]
        ld a, HS_CELADON_MANSION_EEVEE_GIFT
        ld [wMissableObjectIndex], a
        predef HideObject
.party_full
        jp TextScriptEnd
+.nuzNoGift
+       ld hl, CeladonMansion5Text3
+       call PrintText
+       jp TextScriptEnd

+CeladonMansion5Text3:
+       text_far _CeladonMansion5Text3
+       text_end

Just as before, if the Nuzlocke flag for Celadon City has been set, they we display some text saying we can't take the Pokemon, otherwise, evertyhing follows normally. Lastly, we need to add the text that is displayed when we can't take the Eevee. Open text/CeladonMansionRoofHouse.asm and add the following,

_CeladonMansion5Text1::
        text "I know everything"
        line "about the world"
        cont "of #MON in"
        cont "your GAME BOY!"

        para "Get together with"
        line "your friends and"
        cont "trade #MON!"
        done

+_CeladonMansion5Text3::
+       text "One #MON is"
+       line "enough from this"
+       cont "city."
+       done

That does it for Celadon City. Feel free to change the text to whatever you'd like.

Saffron City (Lapras, Hitmonlee, Hitmonchan)

For Saffron City, the player can either receive Hitmonlee or Hitmonchan from the fighting dojo and they can also receive a free Lapras on the 7th floor of Silph Co. To keep with the rules they can either get the Lapras or Hitmonlee or Hitmonchan. Let's start with Silph Co. Open up scripts/SilphCo7F.asm and make the following changes,

...
SilphCo7Text1:
; lapras guy
	text_asm
	ld a, [wd72e]
	bit 0, a ; got lapras?
	jr z, .givelapras
+.noLapras
	CheckEvent EVENT_BEAT_SILPH_CO_GIOVANNI
	jr nz, .savedsilph
	ld hl, .LaprasGuyText
	call PrintText
	jr .done
.givelapras
+	ld hl, wNuzlockeRegions
+	inc hl ; Saffron City is in byte 2
+	bit SAFFRON_CITY_NUZ, [hl]
+	jr nz, .noLapras
	ld hl, .MeetLaprasGuyText
	call PrintText
	lb bc, LAPRAS, 15
	call GivePokemon
	jr nc, .done
	ld a, [wSimulatedJoypadStatesEnd]
	and a
	call z, WaitForTextScrollButtonPress
	call EnableAutoTextBoxDrawing
+	ld hl, wNuzlockeRegions
+       inc hl ; Saffron city is in byte 2
+       set SAFFRON_CITY_NUZ, [hl]
	ld hl, .HeresYourLaprasText
	call PrintText
	ld hl, wd72e
	set 0, [hl]
	jr .done
.savedsilph
	ld hl, .LaprasGuySavedText
	call PrintText
.done
	jp TextScriptEnd
...

As usual we check if the Nuzlocke flag has been set for this region and if so, we don't give the Pokemon otherwise, we do give the Pokemon and set the corresponding bit to be 1. Note, the text for talking to the Lapras guy after receiving the Lapras is the same text as if we had already received Hitmonlee or Hitmonchan. I've gone ahead and changed to the text to bit a more informative, you can find it in text/SilphCo7F.asm

...
_LaprasGuyText::
-	text "TEAM ROCKET's"
-	line "BOSS went to the"
-	cont "boardroom! Is our"
-	cont "PRESIDENT OK?"
+	text "Sorry, I'm all"
+	line "out of #MON."
	done
...

Now for the Fighting Dojo. Open up scripts/FightingDojo.asm and make the following modifications for choosing Hitmonlee:

...
FightingDojoText6:
; Hitmonlee Poké Ball
	text_asm
+	ld hl, wNuzlockeRegions
+       inc hl
+       bit SAFFRON_CITY_NUZ, [hl]
+       jr z, .checkHitmonEvents
+       ld hl, NuzlockeDojoText
+       call PrintText
+       jr .done
+.checkHitmonEvents
	CheckEitherEventSet EVENT_GOT_HITMONLEE, EVENT_GOT_HITMONCHAN
	jr z, .GetMon
	ld hl, OtherHitmonText
	call PrintText
	jr .done
.GetMon
	ld a, HITMONLEE
	call DisplayPokedex
	ld hl, WantHitmonleeText
	call PrintText
	call YesNoChoice
	ld a, [wCurrentMenuItem]
	and a
	jr nz, .done
	ld a, [wcf91]
	ld b, a
	ld c, 30
	call GivePokemon
	jr nc, .done
+	ld hl, wNuzlockeRegions
+       inc hl
+       set SAFFRON_CITY_NUZ, [hl]
	; once Poké Ball is taken, hide sprite
	ld a, HS_FIGHTING_DOJO_GIFT_1
	ld [wMissableObjectIndex], a
	predef HideObject
	SetEvents EVENT_GOT_HITMONLEE, EVENT_DEFEATED_FIGHTING_DOJO
.done
	jp TextScriptEnd
...

and same thing for Hitmonchan. We also add in the jump to the Nuzlocke text.

...
FightingDojoText7:
	; Hitmonchan Poké Ball
	text_asm
+       ld hl, wNuzlockeRegions
+       inc hl
+       bit SAFFRON_CITY_NUZ, [hl]
+       jr z, .checkHitmonEvents
+       ld hl, NuzlockeDojoText
+       call PrintText
+       jr .done
+.checkHitmonEvents
	CheckEitherEventSet EVENT_GOT_HITMONLEE, EVENT_GOT_HITMONCHAN
	jr z, .GetMon
	ld hl, OtherHitmonText
	call PrintText
	jr .done
.GetMon
	ld a, HITMONCHAN
	call DisplayPokedex
	ld hl, WantHitmonchanText
	call PrintText
	call YesNoChoice
	ld a, [wCurrentMenuItem]
	and a
	jr nz, .done
	ld a, [wcf91]
	ld b, a
	ld c, 30
	call GivePokemon
	jr nc, .done
+	ld hl, wNuzlockeRegions
+	inc hl
+	set SAFFRON_CITY_NUZ, [hl]
	SetEvents EVENT_GOT_HITMONCHAN, EVENT_DEFEATED_FIGHTING_DOJO

	; once Poké Ball is taken, hide sprite
	ld a, HS_FIGHTING_DOJO_GIFT_2
	ld [wMissableObjectIndex], a
	predef HideObject
.done
	jp TextScriptEnd

WantHitmonchanText:
        text_far _WantHitmonchanText
        text_end

OtherHitmonText:
        text_far _OtherHitmonText
        text_end

+NuzlockeDojoText:
+       text_far _NuzlockeDojoText
+       text_end

Lastly, we need to write out the comment if we have already received the Lapras and can no longer get a new Pokemon. Open up text/FightingDojo.asm and add the Nuzlocke text:

...
_OtherHitmonText::
        text "Better not get"
        line "greedy..."
        done

+_NuzlockeDojoText::
+       text "Sorry #MON"
+       line "prizes have been"
+       cont "suspended for the"
+       cont "time being."
+       done

I'd also suggest increasing the Lapras' level so you don't torture the player with grinding. Level 15 is just too low at this point in the game. That's it for Saffron City.

Cinnabar Island (Fossil Pokemon)

I'm not actually sure how others handle the fossil Pokemon for their Nuzlockes, but I'm going to treat it in this way. If the player catches a Pokemon in Cinnabar Island (by fishing/surfing) then they cannot get the fossil Pokemon and vice versa. Go ahead and open up scripts/CinnabarLabFossilRoom.asm and make the following changes,

...
Lab4Text1:
        text_asm
+	ld hl, wNuzlockeRegions
+	bit CINNABAR_ISLAND_NUZ, [hl]
+	jr nz, .nuzlockeCinnabarText
        CheckEvent EVENT_GAVE_FOSSIL_TO_LAB
        jr nz, .asm_75d96
...
        call GivePokemon
        jr nc, .asm_75d93
+	ld hl, wNuzlockeRegions
+	set CINNABAR_ISLAND_NUZ, [hl]
	ResetEvents EVENT_GAVE_FOSSIL_TO_LAB, EVENT_LAB_STILL_REVIVING_FOSSIL, EVENT_LAB_HANDING_OVER_FOSSIL_MON
        jr .asm_75d93
+.nuzlockeCinnabarText
+	ld hl, NuzlockeCinnabarLabText
+	call PrintText
+	jr .exitLab4Text1

+NuzlockeCinnabarLabText:
+       text_far _NuzlockeCinnabarLabText
+       text_end
...

This is very similar to the other edge cases. Note that, if the player gives the fossil and then trys to cheat by fishing up another Pokemon, they won't be able to get the fossil Pokemon. Lastly, we need to include the text that is displayed if we can't get the fossil Pokemon. Open up text/CinnabarLabFossilRoom.asm and add the following text,

...
+_NuzlockeCinnabarLabText::
+       text "Sorry, the"
+       line "Resurrection"
+       cont "machine is out of"
+       cont "order."
+       done

Fishing in Statues

There is a bug in the game where the player can fish inside some of the statues. This would allow the player to fish up an unlimited number of Pokemon in, say, Vermillion Gym. Check out Disable fishing in statues in the Bug Fixes tutorials.

5. Game over

There are many possible ways that you could end the game when the player blacks out. I won't go into any specific way of doing this. For my own personal ROM hack, it is set up so when you lose the game, you're teleported to a graveyard that you can't escape from. You can read tombstones there and look at the PC to see all your Pokemon. Another possible option would be to just role the credits when you lose.

6. Balancing

In this section, I present a few ideas and solutions for balancing the Nuzlocke game mode.

Old rod catches Goldeens

This is an easy change. Open up engine/items/item_effects.asm and make the following change,

ItemUseOldRod:
	call FishingInit
	jp c, ItemUseNotTime
-       lb bc, 5, MAGIKARP
+       lb bc, 5, GOLDEEN
	ld a, $1 ; set bite
	jr RodResponse

Make Snorlax to be Ditto

Snorlax is another very powerful Pokemon which the player can (essentially) get for free in Routes 12 and 16. A simple fix without hard removing the Snorlax's blocking the road is to simply replace the encounter with a Ditto. This fix keeps the theme of Pokemon intact (and trolls the player). Open up scripts/Route12.asm and scripts/Route16.asm and change SNORLAX to DITTO.

...
        call DisplayTextID
-	ld a, SNORLAX
+       ld a, DITTO
	ld [wCurOpponent], a
...

Remove legendary birds from world map

Open up data/maps/hide_show_data.asm and switch Zapdos, Moltres, and Articuno from SHOW to HIDE. For example,

...
SeafoamIslandsB4FHS:
        db SEAFOAM_ISLANDS_B4F, SEAFOAMISLANDSB4F_BOULDER1, HIDE
        db SEAFOAM_ISLANDS_B4F, SEAFOAMISLANDSB4F_BOULDER2, HIDE
-       db SEAFOAM_ISLANDS_B4F, SEAFOAMISLANDSB4F_ARTICUNO, SHOW
+       db SEAFOAM_ISLANDS_B4F, SEAFOAMISLANDSB4F_ARTICUNO, HIDE
        db $FF, $01, SHOW ; end
...

Soft level cap

A common rule for balancing Nuzlocke runs is to impose a level cap based on the next gym leaders highest level Pokemon. I've personally chosen to implement a soft level cap. That is to repurpose the "loafing around" mechanic for traded Pokemon. Head to engine/battle/core.asm and make the following changes,

...
	call AddNTimes
        ld a, [wPlayerID]
        cp [hl]
-	jr nz, .monIsTraded
-	inc hl
-	ld a, [wPlayerID + 1]
-	cp [hl]
-	jp z, .canUseMove
; it was traded
.monIsTraded
; what level might disobey?
+	CheckEvent EVENT_BEAT_CHAMPION_RIVAL
+	ld a, 101
+	jr nz, .next
        ld hl, wObtainedBadges
        bit BIT_EARTHBADGE, [hl]
-	ld a, 101
+	ld a, 65 ; Venasaur/Charizard/Blastoise's level
        jr nz, .next
+	bit BIT_VOLCANOBADGE, [hl]
+	ld a, 50 ; Rhydon's level
+	jr nz, .next
        bit BIT_MARSHBADGE, [hl]
-	ld a, 70
+	ld a, 47 ; Arcanine's level
        jr nz, .next
+	bit BIT_SOULBADGE, [hl]
+	ld a, 43 ; Alakazam's level
+	jr nz, .next
        bit BIT_RAINBOWBADGE, [hl]
-	ld a, 50
+	ld a, 43 ; Weezing's level
        jr nz, .next
+	bit BIT_THUNDERBADGE, [hl]
+	ld a, 29 ; Vileplume's level
+	jr nz, .next
        bit BIT_CASCADEBADGE, [hl]
-       ld a, 30
+       ld a, 24 ; Raichu's level
        jr nz, .next
+	bit BIT_BOULDERBADGE, [hl]
+	ld a, 21 ; Starmie's level
+	jr nz, .next
-	ld a, 10
+	ld a, 14 ; Onix's level
.next
	ld b, a
...

At the beginning we remove the check to see if the Pokemon is traded so that the level check applys to all Pokemon. Then as might be evident, we check for which badge we have and set the level requirement accordingly. You are free to change the level cap to whatever you'd like. I've chosen the values to be the highest levels of the gym leaders' Pokemon.

An alternative to this would be a strict level cap. That could be implemented by stopping experience gain once the Pokemon has attained the restricted level. We could do this by implementing this bitwise check in the routine where experience is awarded. Feel free to update this tutorial if you've done this.