-
Notifications
You must be signed in to change notification settings - Fork 806
Hard coded logic
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.
- Tilesets that have per-mapgroup roofs
- Maps that don't display a location sign
- Outdoor maps within indoor maps don't confuse Dig or Escape Rope
- Landmark limits when scrolling in the Town Map
- Spawn points when you start and finish the game
- Trainer classes with different battle music
- Trainer classes with different victory music
RIVAL1
's first Pokémon has no held itemRIVAL1
andRIVAL2
don't print their trainer class in battle- Vital Throw always goes last
- Flame Wheel and Sacred Fire always defrost the user
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
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
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
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)
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.)
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
This is caused by PlayBattleMusic
in engine/battle/start_battle.asm. The routine's logic is:
- If
[wBattleType]
isBATTLETYPE_SUICUNE
orBATTLETYPE_ROAMING
, playMUSIC_SUICUNE_BATTLE
. - If it's a wild battle, check the region and time.
- If we're in Kanto, play
MUSIC_KANTO_WILD_BATTLE
. - If it's night (and we must be in Johto), play
MUSIC_JOHTO_WILD_BATTLE_NIGHT
. - We must be in Johto during morning or day; play
MUSIC_JOHTO_WILD_BATTLE
.
- If we're in Kanto, play
- It must be a trainer battle; check the values of
[wOtherTrainerClass]
and[wOtherTrainerID]
:- If
[wOtherTrainerClass]
isCHAMPION
orRED
, playMUSIC_CHAMPION_BATTLE
. - If
[wOtherTrainerClass]
isGRUNTM
orGRUNTF
, playMUSIC_ROCKET_BATTLE
. (They should have includedEXECUTIVEM
,EXECUTIVEF
, andSCIENTIST
too…) - If
[wOtherTrainerClass]
is listed underKantoGymLeaders
in data/trainers/leaders.asm, playMUSIC_KANTO_GYM_LEADER_BATTLE
. - If
[wOtherTrainerClass]
is listed underGymLeaders
in data/trainers/leaders.asm, playMUSIC_JOHTO_GYM_LEADER_BATTLE
. (CHAMPION
,RED
, and the Kanto Gym leaders are listed but were already handled in step 3.i.) - If
[wOtherTrainerClass]
isRIVAL2
and[wOtherTrainerID]
is at leastRIVAL2_2_CHIKORITA
(i.e. we're battling our rival in Indigo Plateau), playMUSIC_CHAMPION_BATTLE
. - If
[wOtherTrainerClass]
isRIVAL1
orRIVAL2
, playMUSIC_RIVAL_BATTLE
.
- If
- If it's a link battle, play
MUSIC_JOHTO_TRAINER_BATTLE
. - If we're in Kanto, play
MUSIC_KANTO_TRAINER_BATTLE
. - We must be in Johto; play
MUSIC_JOHTO_TRAINER_BATTLE
.
This is caused by PlayVictoryMusic
in engine/battle/core.asm. The routine's logic is:
- Play
MUSIC_NONE
, silencing the battle music. - If
[wBattleMode]
is notWILD_BATTLE
(and so must beTRAINER_BATTLE
):- If
[wOtherTrainerClass]
is listed underGymLeaders
in data/trainers/leaders.asm, playMUSIC_GYM_VICTORY
. - It must be a regular trainer battle; play
MUSIC_TRAINER_VICTORY
.
- If
- 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
. - Do not play any victory music.
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
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
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