summaryrefslogtreecommitdiff
path: root/engine
diff options
context:
space:
mode:
Diffstat (limited to 'engine')
-rwxr-xr-xengine/battle/core.asm63
-rwxr-xr-xengine/battle/end_of_battle.asm2
-rw-r--r--engine/battle/experience.asm6
-rw-r--r--engine/black_out.asm46
-rwxr-xr-xengine/cable_club.asm4
-rw-r--r--engine/debug1.asm33
-rw-r--r--engine/display_pokedex.asm19
-rw-r--r--engine/display_text_id_init.asm78
-rwxr-xr-xengine/evolution.asm2
-rwxr-xr-xengine/hidden_object_functions7.asm2
-rwxr-xr-xengine/items/items.asm10
-rw-r--r--engine/load_mon_data.asm49
-rw-r--r--engine/menu/draw_start_menu.asm89
-rw-r--r--engine/menu/swap_items.asm149
-rw-r--r--engine/menu/text_box.asm767
-rwxr-xr-xengine/overworld/elevator.asm2
-rw-r--r--engine/overworld/map_sprite_functions1.asm356
-rw-r--r--engine/overworld/set_blackout_map.asm29
-rwxr-xr-xengine/predefs.asm5
-rw-r--r--engine/print_waiting_text.asm20
-rw-r--r--engine/remove_pokemon.asm95
-rw-r--r--engine/special_warps.asm149
-rw-r--r--engine/subtract_paid_money.asm17
-rw-r--r--engine/test_battle.asm45
24 files changed, 1989 insertions, 48 deletions
diff --git a/engine/battle/core.asm b/engine/battle/core.asm
index 1d7cd474..a0c4ba6a 100755
--- a/engine/battle/core.asm
+++ b/engine/battle/core.asm
@@ -432,13 +432,13 @@ MainInBattleLoop:
jr nz, .noLinkBattle
; link battle
ld a, [wSerialExchangeNybbleReceiveData]
- cp $f
+ cp LINKBATTLE_RUN
jp z, EnemyRan
- cp $e
+ cp LINKBATTLE_STRUGGLE
jr z, .noLinkBattle
- cp $d
+ cp LINKBATTLE_NO_ACTION
jr z, .noLinkBattle
- sub $4
+ sub 4
jr c, .noLinkBattle
; the link battle enemy has switched mons
ld a, [wPlayerBattleStatus1]
@@ -488,8 +488,8 @@ MainInBattleLoop:
jr nc, .playerMovesFirst ; if player is faster
jr .enemyMovesFirst ; if enemy is faster
.speedEqual ; 50/50 chance for both players
- ld a, [$ffaa]
- cp $2
+ ld a, [hSerialConnectionStatus]
+ cp USING_INTERNAL_CLOCK
jr z, .invertOutcome
call BattleRandom
cp $80
@@ -872,7 +872,7 @@ FaintEnemyPokemon:
ld a, SFX_FAINT_FALL
call PlaySoundWaitForCurrent
.sfxwait
- ld a, [wChannelSoundIDs + CH4]
+ ld a, [wChannelSoundIDs + Ch4]
cp SFX_FAINT_FALL
jr z, .sfxwait
ld a, SFX_FAINT_THUD
@@ -956,7 +956,7 @@ EndLowHealthAlarm:
; the low health alarm and prevents it from reactivating until the next battle.
xor a
ld [wLowHealthAlarm], a ; turn off low health alarm
- ld [wChannelSoundIDs + CH4], a
+ ld [wChannelSoundIDs + Ch4], a
inc a
ld [wLowHealthAlarmDisabled], a ; prevent it from reactivating
ret
@@ -990,7 +990,7 @@ ReplaceFaintedEnemyMon:
; link battle
call LinkBattleExchangeData
ld a, [wSerialExchangeNybbleReceiveData]
- cp $f
+ cp LINKBATTLE_RUN
ret z
call LoadScreenTilesFromBuffer1
.notLinkBattle
@@ -1679,12 +1679,12 @@ TryRunningFromBattle:
call SaveScreenTilesToBuffer1
xor a
ld [wActionResultOrTookBattleTurn], a
- ld a, $f
+ ld a, LINKBATTLE_RUN
ld [wPlayerMoveListIndex], a
call LinkBattleExchangeData
call LoadScreenTilesFromBuffer1
ld a, [wSerialExchangeNybbleReceiveData]
- cp $f
+ cp LINKBATTLE_RUN
ld a, $2
jr z, .playSound
dec a
@@ -1954,7 +1954,7 @@ DrawPlayerHUDAndHPBar:
ld [hl], $0
ret z
xor a
- ld [wChannelSoundIDs + CH4], a
+ ld [wChannelSoundIDs + Ch4], a
ret
.setLowHealthAlarm
ld hl, wLowHealthAlarm
@@ -3000,16 +3000,16 @@ SelectEnemyMove:
call LinkBattleExchangeData
call LoadScreenTilesFromBuffer1
ld a, [wSerialExchangeNybbleReceiveData]
- cp $e
+ cp LINKBATTLE_STRUGGLE
jp z, .linkedOpponentUsedStruggle
- cp $d
+ cp LINKBATTLE_NO_ACTION
jr z, .unableToSelectMove
- cp $4
+ cp 4
ret nc
ld [wEnemyMoveListIndex], a
ld c, a
ld hl, wEnemyMonMoves
- ld b, $0
+ ld b, 0
add hl, bc
ld a, [hl]
jr .done
@@ -3088,7 +3088,7 @@ LinkBattleExchangeData:
ld a, $ff
ld [wSerialExchangeNybbleReceiveData], a
ld a, [wPlayerMoveListIndex]
- cp $f ; is the player running from battle?
+ cp LINKBATTLE_RUN ; is the player running from battle?
jr z, .doExchange
ld a, [wActionResultOrTookBattleTurn]
and a ; is the player switching in another mon?
@@ -3096,10 +3096,10 @@ LinkBattleExchangeData:
; the player used a move
ld a, [wPlayerSelectedMove]
cp STRUGGLE
- ld b, $e
+ ld b, LINKBATTLE_STRUGGLE
jr z, .next
- dec b
- inc a
+ dec b ; LINKBATTLE_NO_ACTION
+ inc a ; does move equal -1 (i.e. no action)?
jr z, .next
ld a, [wPlayerMoveListIndex]
jr .doExchange
@@ -4503,10 +4503,10 @@ GetEnemyMonStat:
CalculateDamage:
; input:
-; b: attack
-; c: opponent defense
-; d: base power
-; e: level
+; b: attack
+; c: opponent defense
+; d: base power
+; e: level
ld a, [H_WHOSETURN] ; whose turn?
and a
@@ -5677,9 +5677,9 @@ ExecuteEnemyMove:
jr nz, .executeEnemyMove
ld b, $1
ld a, [wSerialExchangeNybbleReceiveData]
- cp $e
+ cp LINKBATTLE_STRUGGLE
jr z, .executeEnemyMove
- cp $4
+ cp 4
ret nc
.executeEnemyMove
ld hl, wAILayer2Encouragement
@@ -7517,7 +7517,7 @@ FrozenText:
CheckDefrost:
; any fire-type move that has a chance inflict burn (all but Fire Spin) will defrost a frozen target
- and a, 1 << FRZ ; are they frozen?
+ and a, 1 << FRZ ; are they frozen?
ret z ; return if so
ld a, [H_WHOSETURN]
and a
@@ -7526,7 +7526,7 @@ CheckDefrost:
ld a, [wPlayerMoveType]
sub a, FIRE
ret nz ; return if type of move used isn't fire
- ld [wEnemyMonStatus], a ; set opponent status to 00 ["defrost" a frozen monster]
+ ld [wEnemyMonStatus], a ; set opponent status to 00 ["defrost" a frozen monster]
ld hl, wEnemyMon1Status
ld a, [wEnemyMonPartyPos]
ld bc, wEnemyMon2 - wEnemyMon1
@@ -7536,7 +7536,7 @@ CheckDefrost:
ld hl, FireDefrostedText
jr .common
.opponent
- ld a, [wEnemyMoveType] ; same as above with addresses swapped
+ ld a, [wEnemyMoveType] ; same as above with addresses swapped
sub a, FIRE
ret nz
ld [wBattleMonStatus], a
@@ -8052,9 +8052,8 @@ SwitchAndTeleportEffect:
cp c ; get a random number between 0 and c
jr nc, .rejectionSampleLoop1
srl b
- srl b ; b = enemy level * 4
-; bug: does not account for overflow, so levels above 63 can lead to erroneousness results
- cp b ; is rand[0, playerLevel + enemyLevel] > enemyLevel?
+ srl b ; b = enemyLevel / 4
+ cp b ; is rand[0, playerLevel + enemyLevel) >= (enemyLevel / 4)?
jr nc, .playerMoveWasSuccessful ; if so, allow teleporting
ld c, 50
call DelayFrames
diff --git a/engine/battle/end_of_battle.asm b/engine/battle/end_of_battle.asm
index 368cb00a..92a1bcda 100755
--- a/engine/battle/end_of_battle.asm
+++ b/engine/battle/end_of_battle.asm
@@ -46,7 +46,7 @@ EndOfBattle:
.resetVariables
xor a
ld [wLowHealthAlarm], a ;disable low health alarm
- ld [wChannelSoundIDs + CH4], a
+ ld [wChannelSoundIDs + Ch4], a
ld [wIsInBattle], a
ld [wBattleType], a
ld [wMoveMissed], a
diff --git a/engine/battle/experience.asm b/engine/battle/experience.asm
index 9aee8bd7..24748338 100644
--- a/engine/battle/experience.asm
+++ b/engine/battle/experience.asm
@@ -119,11 +119,11 @@ GainExperience:
ld d, MAX_LEVEL
callab CalcExperience ; get max exp
; compare max exp with current exp
- ld a, [$ff96]
+ ld a, [hExperience]
ld b, a
- ld a, [$ff97]
+ ld a, [hExperience + 1]
ld c, a
- ld a, [$ff98]
+ ld a, [hExperience + 2]
ld d, a
pop hl
ld a, [hld]
diff --git a/engine/black_out.asm b/engine/black_out.asm
new file mode 100644
index 00000000..6c358ce3
--- /dev/null
+++ b/engine/black_out.asm
@@ -0,0 +1,46 @@
+ResetStatusAndHalveMoneyOnBlackout::
+; Reset player status on blackout.
+ xor a
+ ld [wBattleResult], a
+ ld [wWalkBikeSurfState], a
+ ld [wIsInBattle], a
+ ld [wMapPalOffset], a
+ ld [wNPCMovementScriptFunctionNum], a
+ ld [hJoyHeld], a
+ ld [wNPCMovementScriptPointerTableNum], a
+ ld [wFlags_0xcd60], a
+
+ ld [hMoney], a
+ ld [hMoney + 1], a
+ ld [hMoney + 2], a
+ call HasEnoughMoney
+ jr c, .lostmoney ; never happens
+
+ ; Halve the player's money.
+ ld a, [wPlayerMoney]
+ ld [hMoney], a
+ ld a, [wPlayerMoney + 1]
+ ld [hMoney + 1], a
+ ld a, [wPlayerMoney + 2]
+ ld [hMoney + 2], a
+ xor a
+ ld [hDivideBCDDivisor], a
+ ld [hDivideBCDDivisor + 1], a
+ ld a, 2
+ ld [hDivideBCDDivisor + 2], a
+ predef DivideBCDPredef3
+ ld a, [hDivideBCDQuotient]
+ ld [wPlayerMoney], a
+ ld a, [hDivideBCDQuotient + 1]
+ ld [wPlayerMoney + 1], a
+ ld a, [hDivideBCDQuotient + 2]
+ ld [wPlayerMoney + 2], a
+
+.lostmoney
+ ld hl, wd732
+ set 2, [hl]
+ res 3, [hl]
+ set 6, [hl]
+ ld a, %11111111
+ ld [wJoyIgnore], a
+ predef_jump HealParty
diff --git a/engine/cable_club.asm b/engine/cable_club.asm
index 70be26c2..a488e8f1 100755
--- a/engine/cable_club.asm
+++ b/engine/cable_club.asm
@@ -551,7 +551,7 @@ TradeCenter_SelectMon:
Coorda 1, 16
.cancelMenuItem_JoypadLoop
call JoypadLowSensitivity
- ld a, [$ffb5]
+ ld a, [hJoy5]
and a ; pressed anything?
jr z, .cancelMenuItem_JoypadLoop
bit 0, a ; A button pressed?
@@ -914,7 +914,7 @@ CableClub_Run:
ld [wGrassRate], a
inc a ; LINK_STATE_IN_CABLE_CLUB
ld [wLinkState], a
- ld [$ffb5], a
+ ld [hJoy5], a
ld a, 10
ld [wAudioFadeOutControl], a
ld a, BANK(Music_Celadon)
diff --git a/engine/debug1.asm b/engine/debug1.asm
new file mode 100644
index 00000000..a5eb7dde
--- /dev/null
+++ b/engine/debug1.asm
@@ -0,0 +1,33 @@
+; This function appears to never be used.
+; It is likely a debugging feature to give the player Tsunekazu Ishihara's
+; favorite Pokemon. This is indicated by the overpowered Exeggutor, which
+; Ishihara (president of Creatures Inc.) said was his favorite Pokemon in an ABC
+; interview on February 8, 2000.
+; "Exeggutor is my favorite. That's because I was always using this character
+; while I was debugging the program."
+; http://www.ign.com/articles/2000/02/09/abc-news-pokamon-chat-transcript
+
+SetIshiharaTeam:
+ ld de, IshiharaTeam
+.loop
+ ld a, [de]
+ cp $ff
+ ret z
+ ld [wcf91], a
+ inc de
+ ld a, [de]
+ ld [wCurEnemyLVL], a
+ inc de
+ call AddPartyMon
+ jr .loop
+
+IshiharaTeam:
+ db EXEGGUTOR,90
+ db MEW,20
+ db JOLTEON,56
+ db DUGTRIO,56
+ db ARTICUNO,57
+ db $FF
+
+EmptyFunc:
+ ret
diff --git a/engine/display_pokedex.asm b/engine/display_pokedex.asm
new file mode 100644
index 00000000..96a2dd6c
--- /dev/null
+++ b/engine/display_pokedex.asm
@@ -0,0 +1,19 @@
+_DisplayPokedex:
+ ld hl, wd730
+ set 6, [hl]
+ predef ShowPokedexData
+ ld hl, wd730
+ res 6, [hl]
+ call ReloadMapData
+ ld c, 10
+ call DelayFrames
+ predef IndexToPokedex
+ ld a, [wd11e]
+ dec a
+ ld c, a
+ ld b, FLAG_SET
+ ld hl, wPokedexSeen
+ predef FlagActionPredef
+ ld a, $1
+ ld [wDoNotWaitForButtonPressAfterDisplayingText], a
+ ret
diff --git a/engine/display_text_id_init.asm b/engine/display_text_id_init.asm
new file mode 100644
index 00000000..312d6329
--- /dev/null
+++ b/engine/display_text_id_init.asm
@@ -0,0 +1,78 @@
+; function that performs initialization for DisplayTextID
+DisplayTextIDInit:
+ xor a
+ ld [wListMenuID],a
+ ld a,[wAutoTextBoxDrawingControl]
+ bit 0,a
+ jr nz,.skipDrawingTextBoxBorder
+ ld a,[hSpriteIndexOrTextID] ; text ID (or sprite ID)
+ and a
+ jr nz,.notStartMenu
+; if text ID is 0 (i.e. the start menu)
+; Note that the start menu text border is also drawn in the function directly
+; below this, so this seems unnecessary.
+ CheckEvent EVENT_GOT_POKEDEX
+; start menu with pokedex
+ coord hl, 10, 0
+ ld b,$0e
+ ld c,$08
+ jr nz,.drawTextBoxBorder
+; start menu without pokedex
+ coord hl, 10, 0
+ ld b,$0c
+ ld c,$08
+ jr .drawTextBoxBorder
+; if text ID is not 0 (i.e. not the start menu) then do a standard dialogue text box
+.notStartMenu
+ coord hl, 0, 12
+ ld b,$04
+ ld c,$12
+.drawTextBoxBorder
+ call TextBoxBorder
+.skipDrawingTextBoxBorder
+ ld hl,wFontLoaded
+ set 0,[hl]
+ ld hl,wFlags_0xcd60
+ bit 4,[hl]
+ res 4,[hl]
+ jr nz,.skipMovingSprites
+ call UpdateSprites
+.skipMovingSprites
+; loop to copy C1X9 (direction the sprite is facing) to C2X9 for each sprite
+; this is done because when you talk to an NPC, they turn to look your way
+; the original direction they were facing must be restored after the dialogue is over
+ ld hl,wSpriteStateData1 + $19
+ ld c,$0f
+ ld de,$0010
+.spriteFacingDirectionCopyLoop
+ ld a,[hl]
+ inc h
+ ld [hl],a
+ dec h
+ add hl,de
+ dec c
+ jr nz,.spriteFacingDirectionCopyLoop
+; loop to force all the sprites in the middle of animation to stand still
+; (so that they don't like they're frozen mid-step during the dialogue)
+ ld hl,wSpriteStateData1 + 2
+ ld de,$0010
+ ld c,e
+.spriteStandStillLoop
+ ld a,[hl]
+ cp a,$ff ; is the sprite visible?
+ jr z,.nextSprite
+; if it is visible
+ and a,$fc
+ ld [hl],a
+.nextSprite
+ add hl,de
+ dec c
+ jr nz,.spriteStandStillLoop
+ ld b,$9c ; window background address
+ call CopyScreenTileBufferToVRAM ; transfer background in WRAM to VRAM
+ xor a
+ ld [hWY],a ; put the window on the screen
+ call LoadFontTilePatterns
+ ld a,$01
+ ld [H_AUTOBGTRANSFERENABLED],a ; enable continuous WRAM to VRAM transfer each V-blank
+ ret
diff --git a/engine/evolution.asm b/engine/evolution.asm
index c0a3434a..a2c52765 100755
--- a/engine/evolution.asm
+++ b/engine/evolution.asm
@@ -8,7 +8,7 @@ EvolveMon:
push af
xor a
ld [wLowHealthAlarm], a
- ld [wChannelSoundIDs + CH4], a
+ ld [wChannelSoundIDs + Ch4], a
dec a
ld [wNewSoundID], a
call PlaySound
diff --git a/engine/hidden_object_functions7.asm b/engine/hidden_object_functions7.asm
index c0b78119..2f9a6aa5 100755
--- a/engine/hidden_object_functions7.asm
+++ b/engine/hidden_object_functions7.asm
@@ -71,7 +71,7 @@ SafariZoneGameOver:
ld a, SFX_SAFARI_ZONE_PA
call PlayMusic
.waitForMusicToPlay
- ld a, [wChannelSoundIDs + CH4]
+ ld a, [wChannelSoundIDs + Ch4]
cp SFX_SAFARI_ZONE_PA
jr nz, .waitForMusicToPlay
ld a, TEXT_SAFARI_GAME_OVER
diff --git a/engine/items/items.asm b/engine/items/items.asm
index 643fb524..159f3a45 100755
--- a/engine/items/items.asm
+++ b/engine/items/items.asm
@@ -1,7 +1,7 @@
UseItem_:
ld a,1
ld [wActionResultOrTookBattleTurn],a ; initialise to success value
- ld a,[wcf91] ;contains item_ID
+ ld a,[wcf91] ;contains item_ID
cp a,HM_01
jp nc,ItemUseTMHM
ld hl,ItemUsePtrTable
@@ -235,7 +235,7 @@ ItemUseBall:
ld b,a
.skipAilmentValueSubtraction
- push bc ; save (Rand1 - Status)
+ push bc ; save (Rand1 - Status)
; Calculate MaxHP * 255.
xor a
@@ -990,7 +990,7 @@ ItemUseMedicine:
.notFullHP ; if the pokemon's current HP doesn't equal its max HP
xor a
ld [wLowHealthAlarm],a ;disable low health alarm
- ld [wChannelSoundIDs + CH4],a
+ ld [wChannelSoundIDs + Ch4],a
push hl
push de
ld bc,wPartyMon1MaxHP - (wPartyMon1HP + 1)
@@ -1777,7 +1777,7 @@ ItemUsePokeflute:
call WaitForSoundToFinish ; wait for sound to end
callba Music_PokeFluteInBattle ; play in-battle pokeflute music
.musicWaitLoop ; wait for music to finish playing
- ld a,[wChannelSoundIDs + CH6]
+ ld a,[wChannelSoundIDs + Ch6]
and a ; music off?
jr nz,.musicWaitLoop
.skipMusic
@@ -1850,7 +1850,7 @@ PlayedFluteHadEffectText:
ld c, BANK(SFX_Pokeflute)
call PlayMusic
.musicWaitLoop ; wait for music to finish playing
- ld a,[wChannelSoundIDs + CH2]
+ ld a,[wChannelSoundIDs + Ch2]
cp a, SFX_POKEFLUE
jr z,.musicWaitLoop
call PlayDefaultMusic ; start playing normal music again
diff --git a/engine/load_mon_data.asm b/engine/load_mon_data.asm
new file mode 100644
index 00000000..a71a81c5
--- /dev/null
+++ b/engine/load_mon_data.asm
@@ -0,0 +1,49 @@
+LoadMonData_:
+; Load monster [wWhichPokemon] from list [wMonDataLocation]:
+; 0: partymon
+; 1: enemymon
+; 2: boxmon
+; 3: daycaremon
+; Return monster id at wcf91 and its data at wLoadedMon.
+; Also load base stats at wMonHeader for convenience.
+
+ ld a, [wDayCareMonSpecies]
+ ld [wcf91], a
+ ld a, [wMonDataLocation]
+ cp DAYCARE_DATA
+ jr z, .GetMonHeader
+
+ ld a, [wWhichPokemon]
+ ld e, a
+ callab GetMonSpecies
+
+.GetMonHeader
+ ld a, [wcf91]
+ ld [wd0b5], a ; input for GetMonHeader
+ call GetMonHeader
+
+ ld hl, wPartyMons
+ ld bc, wPartyMon2 - wPartyMon1
+ ld a, [wMonDataLocation]
+ cp ENEMY_PARTY_DATA
+ jr c, .getMonEntry
+
+ ld hl, wEnemyMons
+ jr z, .getMonEntry
+
+ cp 2
+ ld hl, wBoxMons
+ ld bc, wBoxMon2 - wBoxMon1
+ jr z, .getMonEntry
+
+ ld hl, wDayCareMon
+ jr .copyMonData
+
+.getMonEntry
+ ld a, [wWhichPokemon]
+ call AddNTimes
+
+.copyMonData
+ ld de, wLoadedMon
+ ld bc, wPartyMon2 - wPartyMon1
+ jp CopyData
diff --git a/engine/menu/draw_start_menu.asm b/engine/menu/draw_start_menu.asm
new file mode 100644
index 00000000..4df9de27
--- /dev/null
+++ b/engine/menu/draw_start_menu.asm
@@ -0,0 +1,89 @@
+; function that displays the start menu
+DrawStartMenu:
+ CheckEvent EVENT_GOT_POKEDEX
+; menu with pokedex
+ coord hl, 10, 0
+ ld b,$0e
+ ld c,$08
+ jr nz,.drawTextBoxBorder
+; shorter menu if the player doesn't have the pokedex
+ coord hl, 10, 0
+ ld b,$0c
+ ld c,$08
+.drawTextBoxBorder
+ call TextBoxBorder
+ ld a,D_DOWN | D_UP | START | B_BUTTON | A_BUTTON
+ ld [wMenuWatchedKeys],a
+ ld a,$02
+ ld [wTopMenuItemY],a ; Y position of first menu choice
+ ld a,$0b
+ ld [wTopMenuItemX],a ; X position of first menu choice
+ ld a,[wBattleAndStartSavedMenuItem] ; remembered menu selection from last time
+ ld [wCurrentMenuItem],a
+ ld [wLastMenuItem],a
+ xor a
+ ld [wMenuWatchMovingOutOfBounds],a
+ ld hl,wd730
+ set 6,[hl] ; no pauses between printing each letter
+ coord hl, 12, 2
+ CheckEvent EVENT_GOT_POKEDEX
+; case for not having pokdex
+ ld a,$06
+ jr z,.storeMenuItemCount
+; case for having pokedex
+ ld de,StartMenuPokedexText
+ call PrintStartMenuItem
+ ld a,$07
+.storeMenuItemCount
+ ld [wMaxMenuItem],a ; number of menu items
+ ld de,StartMenuPokemonText
+ call PrintStartMenuItem
+ ld de,StartMenuItemText
+ call PrintStartMenuItem
+ ld de,wPlayerName ; player's name
+ call PrintStartMenuItem
+ ld a,[wd72e]
+ bit 6,a ; is the player using the link feature?
+; case for not using link feature
+ ld de,StartMenuSaveText
+ jr z,.printSaveOrResetText
+; case for using link feature
+ ld de,StartMenuResetText
+.printSaveOrResetText
+ call PrintStartMenuItem
+ ld de,StartMenuOptionText
+ call PrintStartMenuItem
+ ld de,StartMenuExitText
+ call PlaceString
+ ld hl,wd730
+ res 6,[hl] ; turn pauses between printing letters back on
+ ret
+
+StartMenuPokedexText:
+ db "POKéDEX@"
+
+StartMenuPokemonText:
+ db "POKéMON@"
+
+StartMenuItemText:
+ db "ITEM@"
+
+StartMenuSaveText:
+ db "SAVE@"
+
+StartMenuResetText:
+ db "RESET@"
+
+StartMenuExitText:
+ db "EXIT@"
+
+StartMenuOptionText:
+ db "OPTION@"
+
+PrintStartMenuItem:
+ push hl
+ call PlaceString
+ pop hl
+ ld de,SCREEN_WIDTH * 2
+ add hl,de
+ ret
diff --git a/engine/menu/swap_items.asm b/engine/menu/swap_items.asm
new file mode 100644
index 00000000..b1fa78be
--- /dev/null
+++ b/engine/menu/swap_items.asm
@@ -0,0 +1,149 @@
+HandleItemListSwapping:
+ ld a,[wListMenuID]
+ cp a,ITEMLISTMENU
+ jp nz,DisplayListMenuIDLoop ; only rearrange item list menus
+ push hl
+ ld hl,wListPointer
+ ld a,[hli]
+ ld h,[hl]
+ ld l,a
+ inc hl ; hl = beginning of list entries
+ ld a,[wCurrentMenuItem]
+ ld b,a
+ ld a,[wListScrollOffset]
+ add b
+ add a
+ ld c,a
+ ld b,0
+ add hl,bc ; hl = address of currently selected item entry
+ ld a,[hl]
+ pop hl
+ inc a
+ jp z,DisplayListMenuIDLoop ; ignore attempts to swap the Cancel menu item
+ ld a,[wMenuItemToSwap] ; ID of item chosen for swapping (counts from 1)
+ and a ; has the first item to swap already been chosen?
+ jr nz,.swapItems
+; if not, set the currently selected item as the first item
+ ld a,[wCurrentMenuItem]
+ inc a
+ ld b,a
+ ld a,[wListScrollOffset] ; index of top (visible) menu item within the list
+ add b
+ ld [wMenuItemToSwap],a ; ID of item chosen for swapping (counts from 1)
+ ld c,20
+ call DelayFrames
+ jp DisplayListMenuIDLoop
+.swapItems
+ ld a,[wCurrentMenuItem]
+ inc a
+ ld b,a
+ ld a,[wListScrollOffset]
+ add b
+ ld b,a
+ ld a,[wMenuItemToSwap] ; ID of item chosen for swapping (counts from 1)
+ cp b ; is the currently selected item the same as the first item to swap?
+ jp z,DisplayListMenuIDLoop ; ignore attempts to swap an item with itself
+ dec a
+ ld [wMenuItemToSwap],a ; ID of item chosen for swapping (counts from 1)
+ ld c,20
+ call DelayFrames
+ push hl
+ push de
+ ld hl,wListPointer
+ ld a,[hli]
+ ld h,[hl]
+ ld l,a
+ inc hl ; hl = beginning of list entries
+ ld d,h
+ ld e,l ; de = beginning of list entries
+ ld a,[wCurrentMenuItem]
+ ld b,a
+ ld a,[wListScrollOffset]
+ add b
+ add a
+ ld c,a
+ ld b,0
+ add hl,bc ; hl = address of currently selected item entry
+ ld a,[wMenuItemToSwap] ; ID of item chosen for swapping (counts from 1)
+ add a
+ add e
+ ld e,a
+ jr nc,.noCarry
+ inc d
+.noCarry ; de = address of first item to swap
+ ld a,[de]
+ ld b,a
+ ld a,[hli]
+ cp b
+ jr z,.swapSameItemType
+.swapDifferentItems
+ ld [$ff95],a ; [$ff95] = second item ID
+ ld a,[hld]
+ ld [$ff96],a ; [$ff96] = second item quantity
+ ld a,[de]
+ ld [hli],a ; put first item ID in second item slot
+ inc de
+ ld a,[de]
+ ld [hl],a ; put first item quantity in second item slot
+ ld a,[$ff96]
+ ld [de],a ; put second item quantity in first item slot
+ dec de
+ ld a,[$ff95]
+ ld [de],a ; put second item ID in first item slot
+ xor a
+ ld [wMenuItemToSwap],a ; 0 means no item is currently being swapped
+ pop de
+ pop hl
+ jp DisplayListMenuIDLoop
+.swapSameItemType
+ inc de
+ ld a,[hl]
+ ld b,a
+ ld a,[de]
+ add b ; a = sum of both item quantities
+ cp a,100 ; is the sum too big for one item slot?
+ jr c,.combineItemSlots
+; swap enough items from the first slot to max out the second slot if they can't be combined
+ sub a,99
+ ld [de],a
+ ld a,99
+ ld [hl],a
+ jr .done
+.combineItemSlots
+ ld [hl],a ; put the sum in the second item slot
+ ld hl,wListPointer
+ ld a,[hli]
+ ld h,[hl]
+ ld l,a
+ dec [hl] ; decrease the number of items
+ ld a,[hl]
+ ld [wListCount],a ; update number of items variable
+ cp a,1
+ jr nz,.skipSettingMaxMenuItemID
+ ld [wMaxMenuItem],a ; if the number of items is only one now, update the max menu item ID
+.skipSettingMaxMenuItemID
+ dec de
+ ld h,d
+ ld l,e
+ inc hl
+ inc hl ; hl = address of item after first item to swap
+.moveItemsUpLoop ; erase the first item slot and move up all the following item slots to fill the gap
+ ld a,[hli]
+ ld [de],a
+ inc de
+ inc a ; reached the $ff terminator?
+ jr z,.afterMovingItemsUp
+ ld a,[hli]
+ ld [de],a
+ inc de
+ jr .moveItemsUpLoop
+.afterMovingItemsUp
+ xor a
+ ld [wListScrollOffset],a
+ ld [wCurrentMenuItem],a
+.done
+ xor a
+ ld [wMenuItemToSwap],a ; 0 means no item is currently being swapped
+ pop de
+ pop hl
+ jp DisplayListMenuIDLoop
diff --git a/engine/menu/text_box.asm b/engine/menu/text_box.asm
new file mode 100644
index 00000000..12067dd4
--- /dev/null
+++ b/engine/menu/text_box.asm
@@ -0,0 +1,767 @@
+; function to draw various text boxes
+DisplayTextBoxID_:
+ ld a,[wTextBoxID]
+ cp a,TWO_OPTION_MENU
+ jp z,DisplayTwoOptionMenu
+ ld c,a
+ ld hl,TextBoxFunctionTable
+ ld de,3
+ call SearchTextBoxTable
+ jr c,.functionTableMatch
+ ld hl,TextBoxCoordTable
+ ld de,5
+ call SearchTextBoxTable
+ jr c,.coordTableMatch
+ ld hl,TextBoxTextAndCoordTable
+ ld de,9
+ call SearchTextBoxTable
+ jr c,.textAndCoordTableMatch
+.done
+ ret
+.functionTableMatch
+ ld a,[hli]
+ ld h,[hl]
+ ld l,a ; hl = address of function
+ ld de,.done
+ push de
+ jp [hl] ; jump to the function
+.coordTableMatch
+ call GetTextBoxIDCoords
+ call GetAddressOfScreenCoords
+ call TextBoxBorder
+ ret
+.textAndCoordTableMatch
+ call GetTextBoxIDCoords
+ push hl
+ call GetAddressOfScreenCoords
+ call TextBoxBorder
+ pop hl
+ call GetTextBoxIDText
+ ld a,[wd730]
+ push af
+ ld a,[wd730]
+ set 6,a ; no pauses between printing each letter
+ ld [wd730],a
+ call PlaceString
+ pop af
+ ld [wd730],a
+ call UpdateSprites
+ ret
+
+; function to search a table terminated with $ff for a byte matching c in increments of de
+; sets carry flag if a match is found and clears carry flag if not
+SearchTextBoxTable:
+ dec de
+.loop
+ ld a,[hli]
+ cp a,$ff
+ jr z,.notFound
+ cp c
+ jr z,.found
+ add hl,de
+ jr .loop
+.found
+ scf
+.notFound
+ ret
+
+; function to load coordinates from the TextBoxCoordTable or the TextBoxTextAndCoordTable
+; INPUT:
+; hl = address of coordinates
+; OUTPUT:
+; b = height
+; c = width
+; d = row of upper left corner
+; e = column of upper left corner
+GetTextBoxIDCoords:
+ ld a,[hli] ; column of upper left corner
+ ld e,a
+ ld a,[hli] ; row of upper left corner
+ ld d,a
+ ld a,[hli] ; column of lower right corner
+ sub e
+ dec a
+ ld c,a ; c = width
+ ld a,[hli] ; row of lower right corner
+ sub d
+ dec a
+ ld b,a ; b = height
+ ret
+
+; function to load a text address and text coordinates from the TextBoxTextAndCoordTable
+GetTextBoxIDText:
+ ld a,[hli]
+ ld e,a
+ ld a,[hli]
+ ld d,a ; de = address of text
+ push de ; save text address
+ ld a,[hli]
+ ld e,a ; column of upper left corner of text
+ ld a,[hl]
+ ld d,a ; row of upper left corner of text
+ call GetAddressOfScreenCoords
+ pop de ; restore text address
+ ret
+
+; function to point hl to the screen coordinates
+; INPUT:
+; d = row
+; e = column
+; OUTPUT:
+; hl = address of upper left corner of text box
+GetAddressOfScreenCoords:
+ push bc
+ coord hl, 0, 0
+ ld bc,20
+.loop ; loop to add d rows to the base address
+ ld a,d
+ and a
+ jr z,.addedRows
+ add hl,bc
+ dec d
+ jr .loop
+.addedRows
+ pop bc
+ add hl,de
+ ret
+
+; Format:
+; 00: text box ID
+; 01-02: function address
+TextBoxFunctionTable:
+ dbw MONEY_BOX, DisplayMoneyBox
+ dbw BUY_SELL_QUIT_MENU, DoBuySellQuitMenu
+ dbw FIELD_MOVE_MON_MENU, DisplayFieldMoveMonMenu
+ db $ff ; terminator
+
+; Format:
+; 00: text box ID
+; 01: column of upper left corner
+; 02: row of upper left corner
+; 03: column of lower right corner
+; 04: row of lower right corner
+TextBoxCoordTable:
+ db MESSAGE_BOX, 0, 12, 19, 17
+ db $03, 0, 0, 19, 14
+ db $07, 0, 0, 11, 6
+ db LIST_MENU_BOX, 4, 2, 19, 12
+ db $10, 7, 0, 19, 17
+ db MON_SPRITE_POPUP, 6, 4, 14, 13
+ db $ff ; terminator
+
+; Format:
+; 00: text box ID
+; 01: column of upper left corner
+; 02: row of upper left corner
+; 03: column of lower right corner
+; 04: row of lower right corner
+; 05-06: address of text
+; 07: column of beginning of text
+; 08: row of beginning of text
+; table of window positions and corresponding text [key, start column, start row, end column, end row, text pointer [2 bytes], text column, text row]
+TextBoxTextAndCoordTable:
+ db JP_MOCHIMONO_MENU_TEMPLATE
+ db 0,0,14,17 ; text box coordinates
+ dw JapaneseMochimonoText
+ db 3,0 ; text coordinates
+
+ db USE_TOSS_MENU_TEMPLATE
+ db 13,10,19,14 ; text box coordinates
+ dw UseTossText
+ db 15,11 ; text coordinates
+
+ db JP_SAVE_MESSAGE_MENU_TEMPLATE
+ db 0,0,7,5 ; text box coordinates
+ dw JapaneseSaveMessageText
+ db 2,2 ; text coordinates
+
+ db JP_SPEED_OPTIONS_MENU_TEMPLATE
+ db 0,6,5,10 ; text box coordinates
+ dw JapaneseSpeedOptionsText
+ db 2,7 ; text coordinates
+
+ db BATTLE_MENU_TEMPLATE
+ db 8,12,19,17 ; text box coordinates
+ dw BattleMenuText
+ db 10,14 ; text coordinates
+
+ db SAFARI_BATTLE_MENU_TEMPLATE
+ db 0,12,19,17 ; text box coordinates
+ dw SafariZoneBattleMenuText
+ db 2,14 ; text coordinates
+
+ db SWITCH_STATS_CANCEL_MENU_TEMPLATE
+ db 11,11,19,17 ; text box coordinates
+ dw SwitchStatsCancelText
+ db 13,12 ; text coordinates
+
+ db BUY_SELL_QUIT_MENU_TEMPLATE
+ db 0,0,10,6 ; text box coordinates
+ dw BuySellQuitText
+ db 2,1 ; text coordinates
+
+ db MONEY_BOX_TEMPLATE
+ db 11,0,19,2 ; text box coordinates
+ dw MoneyText
+ db 13,0 ; text coordinates
+
+ db JP_AH_MENU_TEMPLATE
+ db 7,6,11,10 ; text box coordinates
+ dw JapaneseAhText
+ db 8,8 ; text coordinates
+
+ db JP_POKEDEX_MENU_TEMPLATE
+ db 11,8,19,17 ; text box coordinates
+ dw JapanesePokedexMenu
+ db 12,10 ; text coordinates
+
+; note that there is no terminator
+
+BuySellQuitText:
+ db "BUY"
+ next "SELL"
+ next "QUIT@@"
+
+UseTossText:
+ db "USE"
+ next "TOSS@"
+
+JapaneseSaveMessageText:
+ db "きろく"
+ next "メッセージ@"
+
+JapaneseSpeedOptionsText:
+ db "はやい"
+ next "おそい@"
+
+MoneyText:
+ db "MONEY@"
+
+JapaneseMochimonoText:
+ db "もちもの@"
+
+JapaneseMainMenuText:
+ db "つづきから"
+ next "さいしょから@"
+
+BattleMenuText:
+ db "FIGHT ",$E1,$E2
+ next "ITEM RUN@"
+
+SafariZoneBattleMenuText:
+ db "BALL× BAIT"
+ next "THROW ROCK RUN@"
+
+SwitchStatsCancelText:
+ db "SWITCH"
+ next "STATS"
+ next "CANCEL@"
+
+JapaneseAhText:
+ db "アッ!@"
+
+JapanesePokedexMenu:
+ db "データをみる"
+ next "なきごえ"
+ next "ぶんぷをみる"
+ next "キャンセル@"
+
+DisplayMoneyBox:
+ ld hl, wd730
+ set 6, [hl]
+ ld a, MONEY_BOX_TEMPLATE
+ ld [wTextBoxID], a
+ call DisplayTextBoxID
+ coord hl, 13, 1
+ ld b, 1
+ ld c, 6
+ call ClearScreenArea
+ coord hl, 12, 1
+ ld de, wPlayerMoney
+ ld c, $a3
+ call PrintBCDNumber
+ ld hl, wd730
+ res 6, [hl]
+ ret
+
+CurrencyString:
+ db " ¥@"
+
+DoBuySellQuitMenu:
+ ld a, [wd730]
+ set 6, a ; no printing delay
+ ld [wd730], a
+ xor a
+ ld [wChosenMenuItem], a
+ ld a, BUY_SELL_QUIT_MENU_TEMPLATE
+ ld [wTextBoxID], a
+ call DisplayTextBoxID
+ ld a, A_BUTTON | B_BUTTON
+ ld [wMenuWatchedKeys], a
+ ld a, $2
+ ld [wMaxMenuItem], a
+ ld a, $1
+ ld [wTopMenuItemY], a
+ ld a, $1
+ ld [wTopMenuItemX], a
+ xor a
+ ld [wCurrentMenuItem], a
+ ld [wLastMenuItem], a
+ ld [wMenuWatchMovingOutOfBounds], a
+ ld a, [wd730]
+ res 6, a ; turn on the printing delay
+ ld [wd730], a
+ call HandleMenuInput
+ call PlaceUnfilledArrowMenuCursor
+ bit 0, a ; was A pressed?
+ jr nz, .pressedA
+ bit 1, a ; was B pressed? (always true since only A/B are watched)
+ jr z, .pressedA
+ ld a, CANCELLED_MENU
+ ld [wMenuExitMethod], a
+ jr .quit
+.pressedA
+ ld a, CHOSE_MENU_ITEM
+ ld [wMenuExitMethod], a
+ ld a, [wCurrentMenuItem]
+ ld [wChosenMenuItem], a
+ ld b, a
+ ld a, [wMaxMenuItem]
+ cp b
+ jr z, .quit
+ ret
+.quit
+ ld a, CANCELLED_MENU
+ ld [wMenuExitMethod], a
+ ld a, [wCurrentMenuItem]
+ ld [wChosenMenuItem], a
+ scf
+ ret
+
+; displays a menu with two options to choose from
+; b = Y of upper left corner of text region
+; c = X of upper left corner of text region
+; hl = address where the text box border should be drawn
+DisplayTwoOptionMenu:
+ push hl
+ ld a, [wd730]
+ set 6, a ; no printing delay
+ ld [wd730], a
+
+; pointless because both values are overwritten before they are read
+ xor a
+ ld [wChosenMenuItem], a
+ ld [wMenuExitMethod], a
+
+ ld a, A_BUTTON | B_BUTTON
+ ld [wMenuWatchedKeys], a
+ ld a, $1
+ ld [wMaxMenuItem], a
+ ld a, b
+ ld [wTopMenuItemY], a
+ ld a, c
+ ld [wTopMenuItemX], a
+ xor a
+ ld [wLastMenuItem], a
+ ld [wMenuWatchMovingOutOfBounds], a
+ push hl
+ ld hl, wTwoOptionMenuID
+ bit 7, [hl] ; select second menu item by default?
+ res 7, [hl]
+ jr z, .storeCurrentMenuItem
+ inc a
+.storeCurrentMenuItem
+ ld [wCurrentMenuItem], a
+ pop hl
+ push hl
+ push hl
+ call TwoOptionMenu_SaveScreenTiles
+ ld a, [wTwoOptionMenuID]
+ ld hl, TwoOptionMenuStrings
+ ld e, a
+ ld d, $0
+ ld a, $5
+.menuStringLoop
+ add hl, de
+ dec a
+ jr nz, .menuStringLoop
+ ld a, [hli]
+ ld c, a
+ ld a, [hli]
+ ld b, a
+ ld e, l
+ ld d, h
+ pop hl
+ push de
+ ld a, [wTwoOptionMenuID]
+ cp TRADE_CANCEL_MENU
+ jr nz, .notTradeCancelMenu
+ call CableClub_TextBoxBorder
+ jr .afterTextBoxBorder
+.notTradeCancelMenu
+ call TextBoxBorder
+.afterTextBoxBorder
+ call UpdateSprites
+ pop hl
+ ld a, [hli]
+ and a ; put blank line before first menu item?
+ ld bc, 20 + 2
+ jr z, .noBlankLine
+ ld bc, 2 * 20 + 2
+.noBlankLine
+ ld a, [hli]
+ ld e, a
+ ld a, [hli]
+ ld d, a
+ pop hl
+ add hl, bc
+ call PlaceString
+ ld hl, wd730
+ res 6, [hl] ; turn on the printing delay
+ ld a, [wTwoOptionMenuID]
+ cp NO_YES_MENU
+ jr nz, .notNoYesMenu
+; No/Yes menu
+; this menu type ignores the B button
+; it only seems to be used when confirming the deletion of a save file
+ xor a
+ ld [wTwoOptionMenuID], a
+ ld a, [wFlags_0xcd60]
+ push af
+ push hl
+ ld hl, wFlags_0xcd60
+ bit 5, [hl]
+ set 5, [hl] ; don't play sound when A or B is pressed in menu
+ pop hl
+.noYesMenuInputLoop
+ call HandleMenuInput
+ bit 1, a ; A button pressed?
+ jr nz, .noYesMenuInputLoop ; try again if A was not pressed
+ pop af
+ pop hl
+ ld [wFlags_0xcd60], a
+ ld a, SFX_PRESS_AB
+ call PlaySound
+ jr .pressedAButton
+.notNoYesMenu
+ xor a
+ ld [wTwoOptionMenuID], a
+ call HandleMenuInput
+ pop hl
+ bit 1, a ; A button pressed?
+ jr nz, .choseSecondMenuItem ; automatically choose the second option if B is pressed
+.pressedAButton
+ ld a, [wCurrentMenuItem]
+ ld [wChosenMenuItem], a
+ and a
+ jr nz, .choseSecondMenuItem
+; chose first menu item
+ ld a, CHOSE_FIRST_ITEM
+ ld [wMenuExitMethod], a
+ ld c, 15
+ call DelayFrames
+ call TwoOptionMenu_RestoreScreenTiles
+ and a
+ ret
+.choseSecondMenuItem
+ ld a, 1
+ ld [wCurrentMenuItem], a
+ ld [wChosenMenuItem], a
+ ld a, CHOSE_SECOND_ITEM
+ ld [wMenuExitMethod], a
+ ld c, 15
+ call DelayFrames
+ call TwoOptionMenu_RestoreScreenTiles
+ scf
+ ret
+
+; Some of the wider/taller two option menus will not have the screen areas
+; they cover be fully saved/restored by the two functions below.
+; The bottom and right edges of the menu may remain after the function returns.
+
+TwoOptionMenu_SaveScreenTiles:
+ ld de, wBuffer
+ lb bc, 5, 6
+.loop
+ ld a, [hli]
+ ld [de], a
+ inc de
+ dec c
+ jr nz, .loop
+ push bc
+ ld bc, SCREEN_WIDTH - 6
+ add hl, bc
+ pop bc
+ ld c, $6
+ dec b
+ jr nz, .loop
+ ret
+
+TwoOptionMenu_RestoreScreenTiles:
+ ld de, wBuffer
+ lb bc, 5, 6
+.loop
+ ld a, [de]
+ inc de
+ ld [hli], a
+ dec c
+ jr nz, .loop
+ push bc
+ ld bc, SCREEN_WIDTH - 6
+ add hl, bc
+ pop bc
+ ld c, 6
+ dec b
+ jr nz, .loop
+ call UpdateSprites
+ ret
+
+; Format:
+; 00: byte width
+; 01: byte height
+; 02: byte put blank line before first menu item
+; 03: word text pointer
+TwoOptionMenuStrings:
+ db 4,3,0
+ dw .YesNoMenu
+ db 6,3,0
+ dw .NorthWestMenu
+ db 6,3,0
+ dw .SouthEastMenu
+ db 6,3,0
+ dw .YesNoMenu
+ db 6,3,0
+ dw .NorthEastMenu
+ db 7,3,0
+ dw .TradeCancelMenu
+ db 7,4,1
+ dw .HealCancelMenu
+ db 4,3,0
+ dw .NoYesMenu
+
+.NoYesMenu
+ db "NO"
+ next "YES@"
+.YesNoMenu
+ db "YES"
+ next "NO@"
+.NorthWestMenu
+ db "NORTH"
+ next "WEST@"
+.SouthEastMenu
+ db "SOUTH"
+ next "EAST@"
+.NorthEastMenu
+ db "NORTH"
+ next "EAST@"
+.TradeCancelMenu
+ db "TRADE"
+ next "CANCEL@"
+.HealCancelMenu
+ db "HEAL"
+ next "CANCEL@"
+
+DisplayFieldMoveMonMenu:
+ xor a
+ ld hl, wFieldMoves
+ ld [hli], a ; wFieldMoves
+ ld [hli], a ; wFieldMoves + 1
+ ld [hli], a ; wFieldMoves + 2
+ ld [hli], a ; wFieldMoves + 3
+ ld [hli], a ; wNumFieldMoves
+ ld [hl], 12 ; wFieldMovesLeftmostXCoord
+ call GetMonFieldMoves
+ ld a, [wNumFieldMoves]
+ and a
+ jr nz, .fieldMovesExist
+
+; no field moves
+ coord hl, 11, 11
+ ld b, 5
+ ld c, 7
+ call TextBoxBorder
+ call UpdateSprites
+ ld a, 12
+ ld [hFieldMoveMonMenuTopMenuItemX], a
+ coord hl, 13, 12
+ ld de, PokemonMenuEntries
+ jp PlaceString
+
+.fieldMovesExist
+ push af
+
+; Calculate the text box position and dimensions based on the leftmost X coord
+; of the field move names before adjusting for the number of field moves.
+ coord hl, 0, 11
+ ld a, [wFieldMovesLeftmostXCoord]
+ dec a
+ ld e, a
+ ld d, 0
+ add hl, de
+ ld b, 5
+ ld a, 18
+ sub e
+ ld c, a
+ pop af
+
+; For each field move, move the top of the text box up 2 rows while the leaving
+; the bottom of the text box at the bottom of the screen.
+ ld de, -SCREEN_WIDTH * 2
+.textBoxHeightLoop
+ add hl, de
+ inc b
+ inc b
+ dec a
+ jr nz, .textBoxHeightLoop
+
+; Make space for an extra blank row above the top field move.
+ ld de, -SCREEN_WIDTH
+ add hl, de
+ inc b
+
+ call TextBoxBorder
+ call UpdateSprites
+
+; Calculate the position of the first field move name to print.
+ coord hl, 0, 12
+ ld a, [wFieldMovesLeftmostXCoord]
+ inc a
+ ld e, a
+ ld d, 0
+ add hl, de
+ ld de, -SCREEN_WIDTH * 2
+ ld a, [wNumFieldMoves]
+.calcFirstFieldMoveYLoop
+ add hl, de
+ dec a
+ jr nz, .calcFirstFieldMoveYLoop
+
+ xor a
+ ld [wNumFieldMoves], a
+ ld de, wFieldMoves
+.printNamesLoop
+ push hl
+ ld hl, FieldMoveNames
+ ld a, [de]
+ and a
+ jr z, .donePrintingNames
+ inc de
+ ld b, a ; index of name
+.skipNamesLoop ; skip past names before the name we want
+ dec b
+ jr z, .reachedName
+.skipNameLoop ; skip past current name
+ ld a, [hli]
+ cp "@"
+ jr nz, .skipNameLoop
+ jr .skipNamesLoop
+.reachedName
+ ld b, h
+ ld c, l
+ pop hl
+ push de
+ ld d, b
+ ld e, c
+ call PlaceString
+ ld bc, SCREEN_WIDTH * 2
+ add hl, bc
+ pop de
+ jr .printNamesLoop
+
+.donePrintingNames
+ pop hl
+ ld a, [wFieldMovesLeftmostXCoord]
+ ld [hFieldMoveMonMenuTopMenuItemX], a
+ coord hl, 0, 12
+ ld a, [wFieldMovesLeftmostXCoord]
+ inc a
+ ld e, a
+ ld d, 0
+ add hl, de
+ ld de, PokemonMenuEntries
+ jp PlaceString
+
+FieldMoveNames:
+ db "CUT@"
+ db "FLY@"
+ db "@"
+ db "SURF@"
+ db "STRENGTH@"
+ db "FLASH@"
+ db "DIG@"
+ db "TELEPORT@"
+ db "SOFTBOILED@"
+
+PokemonMenuEntries:
+ db "STATS"
+ next "SWITCH"
+ next "CANCEL@"
+
+GetMonFieldMoves:
+ ld a, [wWhichPokemon]
+ ld hl, wPartyMon1Moves
+ ld bc, wPartyMon2 - wPartyMon1
+ call AddNTimes
+ ld d, h
+ ld e, l
+ ld c, NUM_MOVES + 1
+ ld hl, wFieldMoves
+.loop
+ push hl
+.nextMove
+ dec c
+ jr z, .done
+ ld a, [de] ; move ID
+ and a
+ jr z, .done
+ ld b, a
+ inc de
+ ld hl, FieldMoveDisplayData
+.fieldMoveLoop
+ ld a, [hli]
+ cp $ff
+ jr z, .nextMove ; if the move is not a field move
+ cp b
+ jr z, .foundFieldMove
+ inc hl
+ inc hl
+ jr .fieldMoveLoop
+.foundFieldMove
+ ld a, b
+ ld [wLastFieldMoveID], a
+ ld a, [hli] ; field move name index
+ ld b, [hl] ; field move leftmost X coordinate
+ pop hl
+ ld [hli], a ; store name index in wFieldMoves
+ ld a, [wNumFieldMoves]
+ inc a
+ ld [wNumFieldMoves], a
+ ld a, [wFieldMovesLeftmostXCoord]
+ cp b
+ jr c, .skipUpdatingLeftmostXCoord
+ ld a, b
+ ld [wFieldMovesLeftmostXCoord], a
+.skipUpdatingLeftmostXCoord
+ ld a, [wLastFieldMoveID]
+ ld b, a
+ jr .loop
+.done
+ pop hl
+ ret
+
+; Format: [Move id], [name index], [leftmost tile]
+; Move id = id of move
+; Name index = index of name in FieldMoveNames
+; Leftmost tile = -1 + tile column in which the first letter of the move's name should be displayed
+; "SOFTBOILED" is $08 because it has 4 more letters than "SURF", for example, whose value is $0C
+FieldMoveDisplayData:
+ db CUT, $01, $0C
+ db FLY, $02, $0C
+ db $B4, $03, $0C ; unused field move
+ db SURF, $04, $0C
+ db STRENGTH, $05, $0A
+ db FLASH, $06, $0C
+ db DIG, $07, $0C
+ db TELEPORT, $08, $0A
+ db SOFTBOILED, $09, $08
+ db $ff ; list terminator
diff --git a/engine/overworld/elevator.asm b/engine/overworld/elevator.asm
index 929e4f22..7ff4ff71 100755
--- a/engine/overworld/elevator.asm
+++ b/engine/overworld/elevator.asm
@@ -33,7 +33,7 @@ ShakeElevator:
ld a, SFX_SAFARI_ZONE_PA
call PlayMusic
.musicLoop
- ld a, [wChannelSoundIDs + CH4]
+ ld a, [wChannelSoundIDs + Ch4]
cp SFX_SAFARI_ZONE_PA
jr z, .musicLoop
call UpdateSprites
diff --git a/engine/overworld/map_sprite_functions1.asm b/engine/overworld/map_sprite_functions1.asm
new file mode 100644
index 00000000..d1a411fa
--- /dev/null
+++ b/engine/overworld/map_sprite_functions1.asm
@@ -0,0 +1,356 @@
+_UpdateSprites:
+ ld h, $c1
+ inc h
+ ld a, $e ; wSpriteStateData2 + $0e
+.spriteLoop
+ ld l, a
+ sub $e
+ ld c, a
+ ld [H_CURRENTSPRITEOFFSET], a
+ ld a, [hl]
+ and a
+ jr z, .skipSprite ; tests $c2Xe
+ push hl
+ push de
+ push bc
+ call .updateCurrentSprite
+ pop bc
+ pop de
+ pop hl
+.skipSprite
+ ld a, l
+ add $10 ; move to next sprite
+ cp $e ; test for overflow (back at $0e)
+ jr nz, .spriteLoop
+ ret
+.updateCurrentSprite
+ cp $1
+ jp nz, UpdateNonPlayerSprite
+ jp UpdatePlayerSprite
+
+UpdateNonPlayerSprite:
+ dec a
+ swap a
+ ld [$ff93], a ; $10 * sprite#
+ ld a, [wNPCMovementScriptSpriteOffset] ; some sprite offset?
+ ld b, a
+ ld a, [H_CURRENTSPRITEOFFSET]
+ cp b
+ jr nz, .unequal
+ jp DoScriptedNPCMovement
+.unequal
+ jp UpdateNPCSprite
+
+; This detects if the current sprite (whose offset is at H_CURRENTSPRITEOFFSET)
+; is going to collide with another sprite by looping over the other sprites.
+; The current sprite's offset will be labelled with i (e.g. $c1i0).
+; The loop sprite's offset will labelled with j (e.g. $c1j0).
+;
+; Note that the Y coordinate of the sprite (in [$c1k4]) is one of the following
+; 9 values when the sprite is aligned with the grid: $fc, $0c, $1c, $2c, ..., $7c.
+; The reason that 4 is added below to the coordinate is to make it align with a
+; multiple of $10 to make comparisons easier.
+DetectCollisionBetweenSprites:
+ nop
+
+ ld h, wSpriteStateData1 / $100
+ ld a, [H_CURRENTSPRITEOFFSET]
+ add wSpriteStateData1 % $100
+ ld l, a
+
+ ld a, [hl] ; a = [$c1i0] (picture) (0 if slot is unused)
+ and a ; is this sprite slot slot used?
+ ret z ; return if not used
+
+ ld a, l
+ add 3
+ ld l, a
+
+ ld a, [hli] ; a = [$c1i3] (delta Y) (-1, 0, or 1)
+ call SetSpriteCollisionValues
+
+ ld a, [hli] ; a = [$C1i4] (Y screen coordinate)
+ add 4 ; align with multiple of $10
+
+; The effect of the following 3 lines is to
+; add 7 to a if moving south or
+; subtract 7 from a if moving north.
+ add b
+ and $f0
+ or c
+
+ ld [$ff90], a ; store Y coordinate adjusted for direction of movement
+
+ ld a, [hli] ; a = [$c1i5] (delta X) (-1, 0, or 1)
+ call SetSpriteCollisionValues
+ ld a, [hl] ; a = [$C1i6] (X screen coordinate)
+
+; The effect of the following 3 lines is to
+; add 7 to a if moving east or
+; subtract 7 from a if moving west.
+ add b
+ and $f0
+ or c
+
+ ld [$ff91], a ; store X coordinate adjusted for direction of movement
+
+ ld a, l
+ add 7
+ ld l, a
+
+ xor a
+ ld [hld], a ; zero [$c1id] XXX what's [$c1id] for?
+ ld [hld], a ; zero [$c1ic] (directions in which collisions occurred)
+
+ ld a, [$ff91]
+ ld [hld], a ; [$c1ib] = adjusted X coordinate
+ ld a, [$ff90]
+ ld [hl], a ; [$c1ia] = adjusted Y coordinate
+
+ xor a ; zero the loop counter
+
+.loop
+ ld [$ff8f], a ; store loop counter
+ swap a
+ ld e, a
+ ld a, [H_CURRENTSPRITEOFFSET]
+ cp e ; does the loop sprite match the current sprite?
+ jp z, .next ; go to the next sprite if they match
+
+ ld d, h
+ ld a, [de] ; a = [$c1j0] (picture) (0 if slot is unused)
+ and a ; is this sprite slot slot used?
+ jp z, .next ; go the next sprite if not used
+
+ inc e
+ inc e
+ ld a, [de] ; a = [$c1j2] ($ff means the sprite is offscreen)
+ inc a
+ jp z, .next ; go the next sprite if offscreen
+
+ ld a, [H_CURRENTSPRITEOFFSET]
+ add 10
+ ld l, a
+
+ inc e
+ ld a, [de] ; a = [$c1j3] (delta Y)
+ call SetSpriteCollisionValues
+
+ inc e
+ ld a, [de] ; a = [$C1j4] (Y screen coordinate)
+ add 4 ; align with multiple of $10
+
+; The effect of the following 3 lines is to
+; add 7 to a if moving south or
+; subtract 7 from a if moving north.
+ add b
+ and $f0
+ or c
+
+ sub [hl] ; subtract the adjusted Y coordinate of sprite i ([$c1ia]) from that of sprite j
+
+; calculate the absolute value of the difference to get the distance
+ jr nc, .noCarry1
+ cpl
+ inc a
+.noCarry1
+ ld [$ff90], a ; store the distance between the two sprites' adjusted Y values
+
+; Use the carry flag set by the above subtraction to determine which sprite's
+; Y coordinate is larger. This information is used later to set [$c1ic],
+; which stores which direction the collision occurred in.
+; The following 5 lines set the lowest 2 bits of c, which are later shifted left by 2.
+; If sprite i's Y is larger, set lowest 2 bits of c to 10.
+; If sprite j's Y is larger or both are equal, set lowest 2 bits of c to 01.
+ push af
+ rl c
+ pop af
+ ccf
+ rl c
+
+; If sprite i's delta Y is 0, then b = 7, else b = 9.
+ ld b, 7
+ ld a, [hl] ; a = [$c1ia] (adjusted Y coordinate)
+ and $f
+ jr z, .next1
+ ld b, 9
+
+.next1
+ ld a, [$ff90] ; a = distance between adjusted Y coordinates
+ sub b
+ ld [$ff92], a ; store distance adjusted using sprite i's direction
+ ld a, b
+ ld [$ff90], a ; store 7 or 9 depending on sprite i's delta Y
+ jr c, .checkXDistance
+
+; If sprite j's delta Y is 0, then b = 7, else b = 9.
+ ld b, 7
+ dec e
+ ld a, [de] ; a = [$c1j3] (delta Y)
+ inc e
+ and a
+ jr z, .next2
+ ld b, 9
+
+.next2
+ ld a, [$ff92] ; a = distance adjusted using sprite i's direction
+ sub b ; adjust distance using sprite j's direction
+ jr z, .checkXDistance
+ jr nc, .next ; go to next sprite if distance is still positive after both adjustments
+
+.checkXDistance
+ inc e
+ inc l
+ ld a, [de] ; a = [$c1j5] (delta X)
+
+ push bc
+
+ call SetSpriteCollisionValues
+ inc e
+ ld a, [de] ; a = [$c1j6] (X screen coordinate)
+
+; The effect of the following 3 lines is to
+; add 7 to a if moving east or
+; subtract 7 from a if moving west.
+ add b
+ and $f0
+ or c
+
+ pop bc
+
+ sub [hl] ; subtract the adjusted X coordinate of sprite i ([$c1ib]) from that of sprite j
+
+; calculate the absolute value of the difference to get the distance
+ jr nc, .noCarry2
+ cpl
+ inc a
+.noCarry2
+ ld [$ff91], a ; store the distance between the two sprites' adjusted X values
+
+; Use the carry flag set by the above subtraction to determine which sprite's
+; X coordinate is larger. This information is used later to set [$c1ic],
+; which stores which direction the collision occurred in.
+; The following 5 lines set the lowest 2 bits of c.
+; If sprite i's X is larger, set lowest 2 bits of c to 10.
+; If sprite j's X is larger or both are equal, set lowest 2 bits of c to 01.
+ push af
+ rl c
+ pop af
+ ccf
+ rl c
+
+; If sprite i's delta X is 0, then b = 7, else b = 9.
+ ld b, 7
+ ld a, [hl] ; a = [$c1ib] (adjusted X coordinate)
+ and $f
+ jr z, .next3
+ ld b, 9
+
+.next3
+ ld a, [$ff91] ; a = distance between adjusted X coordinates
+ sub b
+ ld [$ff92], a ; store distance adjusted using sprite i's direction
+ ld a, b
+ ld [$ff91], a ; store 7 or 9 depending on sprite i's delta X
+ jr c, .collision
+
+; If sprite j's delta X is 0, then b = 7, else b = 9.
+ ld b, 7
+ dec e
+ ld a, [de] ; a = [$c1j5] (delta X)
+ inc e
+ and a
+ jr z, .next4
+ ld b, 9
+
+.next4
+ ld a, [$ff92] ; a = distance adjusted using sprite i's direction
+ sub b ; adjust distance using sprite j's direction
+ jr z, .collision
+ jr nc, .next ; go to next sprite if distance is still positive after both adjustments
+
+.collision
+ ld a, [$ff91] ; a = 7 or 9 depending on sprite i's delta X
+ ld b, a
+ ld a, [$ff90] ; a = 7 or 9 depending on sprite i's delta Y
+ inc l
+
+; If delta X isn't 0 and delta Y is 0, then b = %0011, else b = %1100.
+; (note that normally if delta X isn't 0, then delta Y must be 0 and vice versa)
+ cp b
+ jr c, .next5
+ ld b, %1100
+ jr .next6
+.next5
+ ld b, %0011
+
+.next6
+ ld a, c ; c has 2 bits set (one of bits 0-1 is set for the X axis and one of bits 2-3 for the Y axis)
+ and b ; we select either the bit in bits 0-1 or bits 2-3 based on the calculation immediately above
+ or [hl] ; or with existing collision direction bits in [$c1ic]
+ ld [hl], a ; store new value
+ ld a, c ; useless code because a is overwritten before being used again
+
+; set bit in [$c1ie] or [$c1if] to indicate which sprite the collision occurred with
+ inc l
+ inc l
+ ld a, [$ff8f] ; a = loop counter
+ ld de, SpriteCollisionBitTable
+ add a
+ add e
+ ld e, a
+ jr nc, .noCarry3
+ inc d
+.noCarry3
+ ld a, [de]
+ or [hl]
+ ld [hli], a
+ inc de
+ ld a, [de]
+ or [hl]
+ ld [hl], a
+
+.next
+ ld a, [$ff8f] ; a = loop counter
+ inc a
+ cp $10
+ jp nz, .loop
+ ret
+
+; takes delta X or delta Y in a
+; b = delta X/Y
+; c = 0 if delta X/Y is 0
+; c = 7 if delta X/Y is 1
+; c = 9 if delta X/Y is -1
+SetSpriteCollisionValues:
+ and a
+ ld b, 0
+ ld c, 0
+ jr z, .done
+ ld c, 9
+ cp -1
+ jr z, .ok
+ ld c, 7
+ ld a, 0
+.ok
+ ld b, a
+.done
+ ret
+
+SpriteCollisionBitTable:
+ db %00000000,%00000001
+ db %00000000,%00000010
+ db %00000000,%00000100
+ db %00000000,%00001000
+ db %00000000,%00010000
+ db %00000000,%00100000
+ db %00000000,%01000000
+ db %00000000,%10000000
+ db %00000001,%00000000
+ db %00000010,%00000000
+ db %00000100,%00000000
+ db %00001000,%00000000
+ db %00010000,%00000000
+ db %00100000,%00000000
+ db %01000000,%00000000
+ db %10000000,%00000000
diff --git a/engine/overworld/set_blackout_map.asm b/engine/overworld/set_blackout_map.asm
new file mode 100644
index 00000000..9bfe82bd
--- /dev/null
+++ b/engine/overworld/set_blackout_map.asm
@@ -0,0 +1,29 @@
+SetLastBlackoutMap:
+; Set the map to return to when
+; blacking out or using Teleport or Dig.
+; Safari rest houses don't count.
+
+ push hl
+ ld hl, SafariZoneRestHouses
+ ld a, [wCurMap]
+ ld b, a
+.loop
+ ld a, [hli]
+ cp -1
+ jr z, .notresthouse
+ cp b
+ jr nz, .loop
+ jr .done
+
+.notresthouse
+ ld a, [wLastMap]
+ ld [wLastBlackoutMap], a
+.done
+ pop hl
+ ret
+
+SafariZoneRestHouses:
+ db SAFARI_ZONE_REST_HOUSE_2
+ db SAFARI_ZONE_REST_HOUSE_3
+ db SAFARI_ZONE_REST_HOUSE_4
+ db -1
diff --git a/engine/predefs.asm b/engine/predefs.asm
index 4ab652b9..d689318d 100755
--- a/engine/predefs.asm
+++ b/engine/predefs.asm
@@ -58,9 +58,10 @@ PredefPointers::
add_predef ScaleSpriteByTwo
add_predef LoadMonBackPic
add_predef CopyDownscaledMonTiles
- ; add_predef LoadMissableObjects
+; add_predef LoadMissableObjects
+; For some reason this points to the middle of the "ld de, wMissableObjectList" instruction in LoadMissableObjects
db 3
- dw $714d
+ dw $714D
add_predef HealParty
add_predef MoveAnimation
add_predef DivideBCDPredef
diff --git a/engine/print_waiting_text.asm b/engine/print_waiting_text.asm
new file mode 100644
index 00000000..7a95da2a
--- /dev/null
+++ b/engine/print_waiting_text.asm
@@ -0,0 +1,20 @@
+PrintWaitingText:
+ coord hl, 3, 10
+ ld b, $1
+ ld c, $b
+ ld a, [wIsInBattle]
+ and a
+ jr z, .asm_4c17
+ call TextBoxBorder
+ jr .asm_4c1a
+.asm_4c17
+ call CableClub_TextBoxBorder
+.asm_4c1a
+ coord hl, 4, 11
+ ld de, WaitingText
+ call PlaceString
+ ld c, 50
+ jp DelayFrames
+
+WaitingText:
+ db "Waiting...!@"
diff --git a/engine/remove_pokemon.asm b/engine/remove_pokemon.asm
new file mode 100644
index 00000000..1fcb5d09
--- /dev/null
+++ b/engine/remove_pokemon.asm
@@ -0,0 +1,95 @@
+_RemovePokemon:
+ ld hl, wPartyCount
+ ld a, [wRemoveMonFromBox]
+ and a
+ jr z, .asm_7b74
+ ld hl, wNumInBox
+.asm_7b74
+ ld a, [hl]
+ dec a
+ ld [hli], a
+ ld a, [wWhichPokemon]
+ ld c, a
+ ld b, $0
+ add hl, bc
+ ld e, l
+ ld d, h
+ inc de
+.asm_7b81
+ ld a, [de]
+ inc de
+ ld [hli], a
+ inc a
+ jr nz, .asm_7b81
+ ld hl, wPartyMonOT
+ ld d, $5
+ ld a, [wRemoveMonFromBox]
+ and a
+ jr z, .asm_7b97
+ ld hl, wBoxMonOT
+ ld d, $13
+.asm_7b97
+ ld a, [wWhichPokemon]
+ call SkipFixedLengthTextEntries
+ ld a, [wWhichPokemon]
+ cp d
+ jr nz, .asm_7ba6
+ ld [hl], $ff
+ ret
+.asm_7ba6
+ ld d, h
+ ld e, l
+ ld bc, NAME_LENGTH
+ add hl, bc
+ ld bc, wPartyMonNicks
+ ld a, [wRemoveMonFromBox]
+ and a
+ jr z, .asm_7bb8
+ ld bc, wBoxMonNicks
+.asm_7bb8
+ call CopyDataUntil
+ ld hl, wPartyMons
+ ld bc, wPartyMon2 - wPartyMon1
+ ld a, [wRemoveMonFromBox]
+ and a
+ jr z, .asm_7bcd
+ ld hl, wBoxMons
+ ld bc, wBoxMon2 - wBoxMon1
+.asm_7bcd
+ ld a, [wWhichPokemon]
+ call AddNTimes
+ ld d, h
+ ld e, l
+ ld a, [wRemoveMonFromBox]
+ and a
+ jr z, .asm_7be4
+ ld bc, wBoxMon2 - wBoxMon1
+ add hl, bc
+ ld bc, wBoxMonOT
+ jr .asm_7beb
+.asm_7be4
+ ld bc, wPartyMon2 - wPartyMon1
+ add hl, bc
+ ld bc, wPartyMonOT
+.asm_7beb
+ call CopyDataUntil
+ ld hl, wPartyMonNicks
+ ld a, [wRemoveMonFromBox]
+ and a
+ jr z, .asm_7bfa
+ ld hl, wBoxMonNicks
+.asm_7bfa
+ ld bc, NAME_LENGTH
+ ld a, [wWhichPokemon]
+ call AddNTimes
+ ld d, h
+ ld e, l
+ ld bc, NAME_LENGTH
+ add hl, bc
+ ld bc, wPokedexOwned
+ ld a, [wRemoveMonFromBox]
+ and a
+ jr z, .asm_7c15
+ ld bc, wBoxMonNicksEnd
+.asm_7c15
+ jp CopyDataUntil
diff --git a/engine/special_warps.asm b/engine/special_warps.asm
new file mode 100644
index 00000000..de00a817
--- /dev/null
+++ b/engine/special_warps.asm
@@ -0,0 +1,149 @@
+SpecialWarpIn:
+ call LoadSpecialWarpData
+ predef LoadTilesetHeader
+ ld hl,wd732
+ bit 2,[hl] ; dungeon warp or fly warp?
+ res 2,[hl]
+ jr z,.next
+; if dungeon warp or fly warp
+ ld a,[wDestinationMap]
+ jr .next2
+.next
+ bit 1,[hl]
+ jr z,.next3
+ call EmptyFunc
+.next3
+ ld a,0
+.next2
+ ld b,a
+ ld a,[wd72d]
+ and a
+ jr nz,.next4
+ ld a,b
+.next4
+ ld hl,wd732
+ bit 4,[hl] ; dungeon warp?
+ ret nz
+; if not dungeon warp
+ ld [wLastMap],a
+ ret
+
+; gets the map ID, tile block map view pointer, tileset, and coordinates
+LoadSpecialWarpData:
+ ld a, [wd72d]
+ cp TRADE_CENTER
+ jr nz, .notTradeCenter
+ ld hl, TradeCenterSpec1
+ ld a, [hSerialConnectionStatus]
+ cp USING_INTERNAL_CLOCK ; which gameboy is clocking determines who is on the left and who is on the right
+ jr z, .copyWarpData
+ ld hl, TradeCenterSpec2
+ jr .copyWarpData
+.notTradeCenter
+ cp COLOSSEUM
+ jr nz, .notColosseum
+ ld hl, ColosseumSpec1
+ ld a, [hSerialConnectionStatus]
+ cp USING_INTERNAL_CLOCK
+ jr z, .copyWarpData
+ ld hl, ColosseumSpec2
+ jr .copyWarpData
+.notColosseum
+ ld a, [wd732]
+ bit 1, a
+ jr nz, .notFirstMap
+ bit 2, a
+ jr nz, .notFirstMap
+ ld hl, FirstMapSpec
+.copyWarpData
+ ld de, wCurMap
+ ld c, $7
+.copyWarpDataLoop
+ ld a, [hli]
+ ld [de], a
+ inc de
+ dec c
+ jr nz, .copyWarpDataLoop
+ ld a, [hli]
+ ld [wCurMapTileset], a
+ xor a
+ jr .done
+.notFirstMap
+ ld a, [wLastMap] ; this value is overwritten before it's ever read
+ ld hl, wd732
+ bit 4, [hl] ; used dungeon warp (jumped down hole/waterfall)?
+ jr nz, .usedDunegonWarp
+ bit 6, [hl] ; return to last pokemon center (or player's house)?
+ res 6, [hl]
+ jr z, .otherDestination
+; return to last pokemon center or player's house
+ ld a, [wLastBlackoutMap]
+ jr .usedFlyWarp
+.usedDunegonWarp
+ ld hl, wd72d
+ res 4, [hl]
+ ld a, [wDungeonWarpDestinationMap]
+ ld b, a
+ ld [wCurMap], a
+ ld a, [wWhichDungeonWarp]
+ ld c, a
+ ld hl, DungeonWarpList
+ ld de, 0
+ ld a, 6
+ ld [wDungeonWarpDataEntrySize], a
+.dungeonWarpListLoop
+ ld a, [hli]
+ cp b
+ jr z, .matchedDungeonWarpDestinationMap
+ inc hl
+ jr .nextDungeonWarp
+.matchedDungeonWarpDestinationMap
+ ld a, [hli]
+ cp c
+ jr z, .matchedDungeonWarpID
+.nextDungeonWarp
+ ld a, [wDungeonWarpDataEntrySize]
+ add e
+ ld e, a
+ jr .dungeonWarpListLoop
+.matchedDungeonWarpID
+ ld hl, DungeonWarpData
+ add hl, de
+ jr .copyWarpData2
+.otherDestination
+ ld a, [wDestinationMap]
+.usedFlyWarp
+ ld b, a
+ ld [wCurMap], a
+ ld hl, FlyWarpDataPtr
+.flyWarpDataPtrLoop
+ ld a, [hli]
+ inc hl
+ cp b
+ jr z, .foundFlyWarpMatch
+ inc hl
+ inc hl
+ jr .flyWarpDataPtrLoop
+.foundFlyWarpMatch
+ ld a, [hli]
+ ld h, [hl]
+ ld l, a
+.copyWarpData2
+ ld de, wCurrentTileBlockMapViewPointer
+ ld c, $6
+.copyWarpDataLoop2
+ ld a, [hli]
+ ld [de], a
+ inc de
+ dec c
+ jr nz, .copyWarpDataLoop2
+ xor a ; OVERWORLD
+ ld [wCurMapTileset], a
+.done
+ ld [wYOffsetSinceLastSpecialWarp], a
+ ld [wXOffsetSinceLastSpecialWarp], a
+ ld a, $ff ; the player's coordinates have already been updated using a special warp, so don't use any of the normal warps
+ ld [wDestinationWarpID], a
+ ret
+
+INCLUDE "data/special_warps.asm"
diff --git a/engine/subtract_paid_money.asm b/engine/subtract_paid_money.asm
new file mode 100644
index 00000000..2888c3fb
--- /dev/null
+++ b/engine/subtract_paid_money.asm
@@ -0,0 +1,17 @@
+; subtracts the amount the player paid from their money
+; sets carry flag if there is enough money and unsets carry flag if not
+SubtractAmountPaidFromMoney_:
+ ld de,wPlayerMoney
+ ld hl,hMoney ; total price of items
+ ld c,3 ; length of money in bytes
+ call StringCmp
+ ret c
+ ld de,wPlayerMoney + 2
+ ld hl,hMoney + 2 ; total price of items
+ ld c,3 ; length of money in bytes
+ predef SubBCDPredef ; subtract total price from money
+ ld a,MONEY_BOX
+ ld [wTextBoxID],a
+ call DisplayTextBoxID ; redraw money text box
+ and a
+ ret
diff --git a/engine/test_battle.asm b/engine/test_battle.asm
new file mode 100644
index 00000000..d9dcf1fa
--- /dev/null
+++ b/engine/test_battle.asm
@@ -0,0 +1,45 @@
+TestBattle:
+ ret
+
+.loop
+ call GBPalNormal
+
+ ; Don't mess around
+ ; with obedience.
+ ld a, %10000000 ; EARTHBADGE
+ ld [wObtainedBadges], a
+
+ ld hl, wFlags_D733
+ set BIT_TEST_BATTLE, [hl]
+
+ ; Reset the party.
+ ld hl, wPartyCount
+ xor a
+ ld [hli], a
+ dec a
+ ld [hl], a
+
+ ; Give the player a
+ ; level 20 Rhydon.
+ ld a, RHYDON
+ ld [wcf91], a
+ ld a, 20
+ ld [wCurEnemyLVL], a
+ xor a
+ ld [wMonDataLocation], a
+ ld [wCurMap], a
+ call AddPartyMon
+
+ ; Fight against a
+ ; level 20 Rhydon.
+ ld a, RHYDON
+ ld [wCurOpponent], a
+
+ predef InitOpponent
+
+ ; When the battle ends,
+ ; do it all again.
+ ld a, 1
+ ld [wUpdateSpritesEnabled], a
+ ld [H_AUTOBGTRANSFERENABLED], a
+ jr .loop