-
Notifications
You must be signed in to change notification settings - Fork 831
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
- Some high values for maps' music IDs play incorrectly
- Trainer classes with different battle music
RIVAL1
's first Pokémon has no held itemRIVAL1
andRIVAL2
don't print their trainer class in battle- Vital Throw always goes last
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 SPECIAL_MAP
ret z
cp RADIO_TOWER
ret z
cp LAV_RADIO_TOWER
ret z
cp UNDERGROUND_PATH
ret z
cp INDIGO_PLATEAU
ret z
cp 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 ; GROUP_TIN_TOWER_ROOF
jr nz, .not_mt_moon_or_tin_tower
ld a, [wPrevMapNumber]
cp MAP_MOUNT_MOON_SQUARE
ret z
cp MAP_TIN_TOWER_ROOF
ret z
.not_mt_moon_or_tin_tower
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, SILVER_CAVE
ld e, NEW_BARK_TOWN
PokegearMap_ContinueMap:
...
TownMap_GetKantoLandmarkLimits:
ld a, [wStatusFlags]
bit STATUSFLAGS_HALL_OF_FAME_F, a
jr z, .not_hof
ld d, ROUTE_28
ld e, PALLET_TOWN
ret
.not_hof
ld d, ROUTE_28
ld e, 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)
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
If a map's music ID in data/maps/maps.asm is $64 (the value of MUSIC_MAHOGANY_MART
or MUSIC_SUICUNE_BATTLE
) it will play either MUSIC_ROCKET_HIDEOUT
or MUSIC_CHERRYGROVE_CITY
. Moreover, if a map's music ID is $80 or above (the value of RADIO_TOWER_MUSIC
) it might play MUSIC_ROCKET_OVERTURE
or something else.
This is caused by GetMapMusic
in home/map.asm:
GetMapMusic::
push hl
push bc
ld de, MAP_MUSIC
call GetMapField
ld a, c
cp MUSIC_MAHOGANY_MART
jr z, .mahoganymart
bit RADIO_TOWER_MUSIC_F, c
jr nz, .radiotower
farcall Function8b342
ld e, c
ld d, 0
.done
pop bc
pop hl
ret
.radiotower
ld a, [wStatusFlags2]
bit STATUSFLAGS2_ROCKETS_IN_RADIO_TOWER_F, a
jr z, .clearedradiotower
ld de, MUSIC_ROCKET_OVERTURE
jr .done
.clearedradiotower
; the rest of the byte
ld a, c
and RADIO_TOWER_MUSIC - 1
ld e, a
ld d, 0
jr .done
.mahoganymart
ld a, [wStatusFlags2]
bit STATUSFLAGS2_ROCKETS_IN_MAHOGANY_F, a
jr z, .clearedmahogany
ld de, MUSIC_ROCKET_HIDEOUT
jr .done
.clearedmahogany
ld de, MUSIC_CHERRYGROVE_CITY
jr .done
This can cause problems if you add too many new songs, or rearrange the existing ones. A solution would be to redefine the special music constants in constants/music_constants.asm:
; GetMapMusic picks music for these values (see home/map.asm)
MUSIC_MAHOGANY_MART EQU $fc
MUSIC_RADIO_TOWER EQU $fd
; ExitPokegearRadio_HandleMusic uses these values
RESTART_MAP_MUSIC EQU $fe
ENTER_MAP_MUSIC EQU $ff
And then edit GetMapMusic
:
GetMapMusic::
push hl
push bc
ld de, MAP_MUSIC
call GetMapField
ld a, c
cp MUSIC_MAHOGANY_MART
jr z, .mahoganymart
cp MUSIC_RADIO_TOWER
jr z, .radiotower
farcall Function8b342
ld e, c
ld d, 0
.done
pop bc
pop hl
ret
.radiotower
ld a, [wStatusFlags2]
bit STATUSFLAGS2_ROCKETS_IN_RADIO_TOWER_F, a
jr z, .clearedradiotower
ld de, MUSIC_ROCKET_OVERTURE
jr .done
.clearedradiotower
ld de, MUSIC_GOLDENROD_CITY
jr .done
.mahoganymart
ld a, [wStatusFlags2]
bit STATUSFLAGS2_ROCKETS_IN_MAHOGANY_F, a
jr z, .clearedmahogany
ld de, MUSIC_ROCKET_HIDEOUT
jr .done
.clearedmahogany
ld de, MUSIC_CHERRYGROVE_CITY
jr .done
You'll also need to edit data/maps/maps.asm so the Radio Tower maps use MUSIC_RADIO_TOWER
instead of RADIO_TOWER_MUSIC | MUSIC_GOLDENROD_CITY
.
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, play
MUSIC_KANTO_WILD_BATTLE
if we're in Kanto,MUSIC_JOHTO_WILD_BATTLE_NIGHT
in Johto at night, orMUSIC_JOHTO_WILD_BATTLE
during morning and day. - 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 have already been accounted for at this point.) - 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 it's a link battle, play
MUSIC_JOHTO_TRAINER_BATTLE
. - Play
MUSIC_KANTO_TRAINER_BATTLE
if we're in Kanto orMUSIC_JOHTO_TRAINER_BATTLE
if we're in Johto.
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