Hard coded logic - pret/pokecrystal GitHub Wiki

Much of the game logic can be changed via the files in data/, but some things are hard-coded and can be tricky to find. This page lists things that may trip you up when hacking.

Contents

Tilesets that have per-mapgroup roofs

This is caused by LoadTilesetGFX in home/map.asm:

 ; These tilesets support dynamic per-mapgroup roof tiles.
 	ld a, [wTileset]
 	cp TILESET_JOHTO
 	jr z, .load_roof
 	cp TILESET_JOHTO_MODERN
 	jr z, .load_roof
 	cp TILESET_BATTLE_TOWER_OUTSIDE
 	jr z, .load_roof
 	jr .skip_roof
 
 .load_roof
 	farcall LoadMapGroupRoof
 
 .skip_roof

Maps that don't display a location sign

This is caused by ReturnFromMapSetupScript.CheckSpecialMap in engine/events/map_name_sign.asm:

 .CheckSpecialMap:
 ; These landmarks do not get pop-up signs.
 	cp -1
 	ret z
 	cp LANDMARK_SPECIAL ; redundant check
 	ret z
 	cp LANDMARK_RADIO_TOWER
 	ret z
 	cp LANDMARK_LAV_RADIO_TOWER
 	ret z
 	cp LANDMARK_UNDERGROUND_PATH
 	ret z
 	cp LANDMARK_INDIGO_PLATEAU
 	ret z
 	cp LANDMARK_POWER_PLANT
 	ret z
 	ld a, 1
 	and a
 	ret

Outdoor maps within indoor maps don't confuse Dig or Escape Rope

Dig and Escape Rope take you out of a dungeon and back to the entrance you used. However, some dungeons are designed with an enclosed outdoor portion, and it would be bad if visiting those portions made Dig or Escape Rope take you back to them instead of properly outside the dungeon.

There's no "outdoor-within-indoor" map environment, so the few maps in this situation have to be hard-coded. It's caused by LoadWarpData.SaveDigWarp in engine/overworld/warp_connection.asm:

 ; MOUNT_MOON_SQUARE and TIN_TOWER_ROOF are outdoor maps within indoor maps.
 ; Dig and Escape Rope should not take you to them.
 	ld a, [wPrevMapGroup]
 	cp GROUP_MOUNT_MOON_SQUARE
 	jr nz, .not_mt_moon_square_or_tin_tower_roof
 	assert GROUP_MOUNT_MOON_SQUARE == GROUP_TIN_TOWER_ROOF
 	ld a, [wPrevMapNumber]
 	cp MAP_MOUNT_MOON_SQUARE
 	ret z
 	cp MAP_TIN_TOWER_ROOF
 	ret z
 .not_mt_moon_square_or_tin_tower_roof

Landmark limits when scrolling in the Town Map

This is caused by PokegearMap_KantoMap and PokegearMap_JohtoMap in engine/pokegear/pokegear.asm:

 PokegearMap_KantoMap:
 	call TownMap_GetKantoLandmarkLimits
 	jr PokegearMap_ContinueMap
 
 PokegearMap_JohtoMap:
 	ld d, LANDMARK_SILVER_CAVE
 	ld e, LANDMARK_NEW_BARK_TOWN
 PokegearMap_ContinueMap:
 	...

 TownMap_GetKantoLandmarkLimits:
 	ld a, [wStatusFlags]
 	bit STATUSFLAGS_HALL_OF_FAME_F, a
 	jr z, .not_hof
 	ld d, LANDMARK_ROUTE_28
 	ld e, LANDMARK_PALLET_TOWN
 	ret
 
 .not_hof
 	ld d, LANDMARK_ROUTE_28
 	ld e, LANDMARK_VICTORY_ROAD
 	ret

If you access a map that's outside the limits, then scrolling through the Town Map can underflow and go past the defined landmark data, displaying garbage. (Video)

Spawn points when you start and finish the game

These are defined in engine/menus/intro_menu.asm:

 	ld a, LANDMARK_NEW_BARK_TOWN
 	ld [wPrevLandmark], a
 
 	ld a, SPAWN_HOME
 	ld [wDefaultSpawnpoint], a
 .SpawnAfterE4:
 	ld a, SPAWN_NEW_BARK
 	ld [wDefaultSpawnpoint], a
 	call PostCreditsSpawn
 	jp FinishContinueFunction
 
 SpawnAfterRed:
 	ld a, SPAWN_MT_SILVER
 	ld [wDefaultSpawnpoint], a

(The maps and coordinates that correspond to those spawn points are not hard-coded; they're in the SpawnPoints table in data/maps/spawn_points.asm.)

RIVAL1's first Pokémon has no held item

This is caused by InitEnemyTrainer in engine/battle/core.asm:

 	; RIVAL1's first mon has no held item
 	ld a, [wTrainerClass]
 	cp RIVAL1
 	jr nz, .ok
 	xor a
 	ld [wOTPartyMon1Item], a
 .ok

Trainer classes with different battle music

This is caused by PlayBattleMusic in engine/battle/start_battle.asm. The routine's logic is:

  1. If [wBattleType] is BATTLETYPE_SUICUNE or BATTLETYPE_ROAMING, play MUSIC_SUICUNE_BATTLE.
  2. If it's a wild battle, check the region and time.
    1. If we're in Kanto, play MUSIC_KANTO_WILD_BATTLE.
    2. If it's night (and we must be in Johto), play MUSIC_JOHTO_WILD_BATTLE_NIGHT.
    3. We must be in Johto during morning or day; play MUSIC_JOHTO_WILD_BATTLE.
  3. It must be a trainer battle; check the values of [wOtherTrainerClass] and [wOtherTrainerID]:
    1. If [wOtherTrainerClass] is CHAMPION or RED, play MUSIC_CHAMPION_BATTLE.
    2. If [wOtherTrainerClass] is GRUNTM or GRUNTF, play MUSIC_ROCKET_BATTLE. (They should have included EXECUTIVEM, EXECUTIVEF, and SCIENTIST too…)
    3. If [wOtherTrainerClass] is listed under KantoGymLeaders in data/trainers/leaders.asm, play MUSIC_KANTO_GYM_LEADER_BATTLE.
    4. If [wOtherTrainerClass] is listed under GymLeaders in data/trainers/leaders.asm, play MUSIC_JOHTO_GYM_LEADER_BATTLE. (CHAMPION, RED, and the Kanto Gym leaders are listed but were already handled in step 3.i.)
    5. If [wOtherTrainerClass] is RIVAL2 and [wOtherTrainerID] is at least RIVAL2_2_CHIKORITA (i.e. we're battling our rival in Indigo Plateau), play MUSIC_CHAMPION_BATTLE.
    6. If [wOtherTrainerClass] is RIVAL1 or RIVAL2, play MUSIC_RIVAL_BATTLE.
  4. If it's a link battle, play MUSIC_JOHTO_TRAINER_BATTLE.
  5. If we're in Kanto, play MUSIC_KANTO_TRAINER_BATTLE.
  6. We must be in Johto; play MUSIC_JOHTO_TRAINER_BATTLE.

Trainer classes with different victory music

This is caused by PlayVictoryMusic in engine/battle/core.asm. The routine's logic is:

  1. Play MUSIC_NONE, silencing the battle music.
  2. If [wBattleMode] is not WILD_BATTLE (and so must be TRAINER_BATTLE):
    1. If [wOtherTrainerClass] is listed under GymLeaders in data/trainers/leaders.asm, play MUSIC_GYM_VICTORY.
    2. It must be a regular trainer battle; play MUSIC_TRAINER_VICTORY.
  3. It must a wild battle. If any mon is holding an Exp. Share, or we collect money from Pay Day, or we have not lost the battle, play MUSIC_WILD_VICTORY.
  4. Do not play any victory music.

RIVAL1 and RIVAL2 don't print their trainer class in battle

Both of these classes are named "RIVAL", but battles just print "SILVER wants to battle!", not "RIVAL SILVER wants to battle!"

This is caused by PlaceEnemysName in home/text.asm:

 	ld a, [wTrainerClass]
 	cp RIVAL1
 	jr z, .rival
 	cp RIVAL2
 	jr z, .rival

Vital Throw always goes last

Most move effects' priorities are specified in MoveEffectPriorities in data/moves/effects_priorities.asm. ...except for Vital Throw. This move shares its effect with a lot of other moves, and they couldn't be bothered to make a new move effect ID for it like EFFECT_PRIORITY_HIT, so they hard-coded this case, in GetMovePriority of engine/battle/core.asm:

 GetMovePriority:
 ; Return the priority (0-3) of move a.
 
 	ld b, a
 
 	; Vital Throw goes last.
 	cp VITAL_THROW
 	ld a, 0
 	ret z

Flame Wheel and Sacred Fire always defrost the user

These two moves allow the user to attack and defrost if they were frozen. While both their effects do indeed call Defrost as seen in data/moves/effects.asm #1482 and data/moves/effects.asm #1693, there is actually hard-coded logic dictating which moves can defrost the user. This logic resides in two places: BattleCommand_CheckTurn and CheckEnemyTurn; in engine/battle/effect_commands.asm (BattleCommand_CheckTurn) and engine/battle/effect_commands.asm (CheckEnemyTurn):

 .not_asleep
 
 	ld hl, wBattleMonStatus
 	bit FRZ, [hl]
 	jr z, .not_frozen
 
 	; Flame Wheel and Sacred Fire thaw the user.
 	ld a, [wCurPlayerMove]
 	cp FLAME_WHEEL
 	jr z, .not_frozen
 	cp SACRED_FIRE
 	jr z, .not_frozen
 
 	ld hl, FrozenSolidText
 	call StdBattleTextbox
 
 	call CantMove
 	jp EndTurn