Skip to content
FrenchOrange edited this page Feb 11, 2024 · 25 revisions

This page is for small hints about how to edit pokecrystal that don't need an entire tutorial of their own. Any sort of tips, tricks, hints, pointers to obscure but useful code routines, engine trivia—it all goes here.

Contents

Change the odds of encountering a shiny Pokémon

The odds of encountering a shiny Pokémon in Gen 2 are 1 in 8,192. That value isn't literally defined anywhere: it's a consequence of making shininess based on DVs. A shiny Pokémon has:

  • Attack DV = 2, 3, 6, 7, 10, 11, 14, or 15
  • Defense, Speed, and Special DVs = 10

Since all four DVs are random from 0 to 15, some multiplication shows that 1 in 8,192 Pokémon will be shiny.

The routine that checks those specific values is CheckShininess in engine/gfx/color.asm. If you want different odds, just change which DVs count as shiny. (The Unused_CheckShininess routine right below it would just make a Pokémon shiny if all its DVs are 10 or higher, which gives odds of about 1 in 50.)

Also, the red Gyarados has hard-coded values for its DVs of 14/10/10/10; they're defined as ATKDEFDV_SHINY and SPDSPCDV_SHINY in constants/battle_constants.asm.

Change the speed of healing at a Pokémon Center

When healing at a Pokémon Center (or at Prof. Elm's healing machine), the following process occurs when the placement of Poké Balls is called:

  1. The amount of Pokémon in the player's party is first loaded into the a register and then into the b register (this is where a loop begins).
  2. A Poké Ball tile is placed.
  3. A sound effect plays between each placement.
  4. A delay is called.
  5. The b register decreases by 1.
  6. If the b register is not yet 0, then loop again. Otherwise return and the placement concludes.

It takes roughly three seconds to place six Poké Balls on the machine. The delay can be decreased by modifying the value of the c register (which is what the DelayFrame label uses to determine the length of the delay).

Edit engine/events/heal_machine_anim.asm:

.LoadBallsOntoMachine:
	ld a, [wPartyCount]
	ld b, a
.party_loop
	call .PlaceHealingMachineTile
	push de
	ld de, SFX_SECOND_PART_OF_ITEMFINDER
	call PlaySFX
	pop de
-	ld c, 30
+	ld c, 15
	call DelayFrames
	dec b
	jr nz, .party_loop
	ret

In this example, the time placing the Poké Balls is reduced by half.

It is also possible to make the placement of the Poké Balls instant by removing the delay entirely:

.LoadBallsOntoMachine:
	ld a, [wPartyCount]
	ld b, a
+	push de
+	ld de, SFX_SECOND_PART_OF_ITEMFINDER
+	call PlaySFX
+	pop de
.party_loop
	call .PlaceHealingMachineTile
-	push de
-	ld de, SFX_SECOND_PART_OF_ITEMFINDER
-	call PlaySFX
-	pop de
-	ld c, 30
-	call DelayFrames
	dec b
	jr nz, .party_loop
	ret

Notice that not only has the delay been removed, but the location of the sound effect has been moved to before the .party_loop local jump. This is to make the code more efficient as it only needs to be executed once, not each time the .party_loop local jump is executed, which can be six times if the player has six Pokémon.

Note: Along with the healing machine at the Pokémon Center and Prof. Elm's lab, this modification will also affect the speed that Poké Balls are placed in the hall of fame's Pokémon registration machine. So that needs to be taken into consideration.

Stop the blue palette from animating its white hue

There are eight palettes used for tilesets: GRAY, RED, GREEN, WATER, YELLOW, BROWN, ROOF, and TEXT. It's called WATER instead of BLUE because its lightest hue gets animated for some tilesets, cycling between three colors. This makes water (and buoys, whirlpools, and waterfalls) look more dynamic, but it prevents you from making static blue tiles (like Mart roofs).

To stop it, edit engine/tilesets/tileset_anims.asm and replace all the instances of dw NULL, AnimateWaterPalette with dw NULL, WaitTileAnimation.

Animate tiles even when textboxes are open

Some tiles in tilesets are animated: water, flowers, whirpools, waterfalls, etc. In Gold and Silver they animated even when textboxes were open, but not in Crystal. To re-enable this, edit AnimateTileset in home/video.asm:

 AnimateTileset::
 ; Only call during the first fifth of VBlank

 	ldh a, [hMapAnims]
 	and a
 	ret z

-; Back out if we're too far into VBlank
-	ldh a, [rLY]
-	cp LY_VBLANK
-	ret c
-	cp LY_VBLANK + 7
-	ret nc

(via Crystal_'s #OldGenFactOfTheDay)

Make overworld sprites darker at night

Edit gfx/overworld/npc_sprites.pal:

 ; nite
-	RGB 15,14,24, 31,19,10, 31,07,01, 00,00,00 ; red
-	RGB 15,14,24, 31,19,10, 10,09,31, 00,00,00 ; blue
-	RGB 15,14,24, 31,19,10, 07,23,03, 00,00,00 ; green
-	RGB 15,14,24, 31,19,10, 15,10,03, 00,00,00 ; brown
-	RGB 15,14,24, 31,19,10, 30,10,06, 00,00,00 ; pink
+	RGB 15,14,24, 16,10,09, 17,03,00, 00,00,00 ; red
+	RGB 15,14,24, 16,10,09, 05,04,27, 00,00,00 ; blue
+	RGB 15,14,24, 16,10,09, 03,10,02, 00,00,00 ; green
+	RGB 15,14,24, 16,10,09, 08,04,02, 00,00,00 ; brown
+	RGB 15,14,24, 16,10,09, 18,05,04, 00,00,00 ; pink
 	RGB 31,31,31, 31,31,31, 13,13,13, 00,00,00 ; silver
 	RGB 15,14,24, 08,13,19, 00,11,13, 00,00,00 ; tree
 	RGB 15,14,24, 12,09,15, 08,04,05, 00,00,00 ; rock

This change will also affect Pokégear icons, cursor and character sprites will become darker at night.

Enemy trainers have maximum happiness for a powerful Return

Edit engine/battle/core.asm:

 LoadEnemyMon:
 ; Initialize enemy monster parameters
 ; To do this we pull the species from wTempEnemyMonSpecies

 ...

 .Happiness:
 ; Set happiness
+	ld a, [wBattleMode]
+	dec a
+	ld a, $ff ; Give the enemy mon max happiness...
+	jr nz, .load_happiness ; ...if it's a Trainer battle.
	ld a, BASE_HAPPINESS
+.load_happiness
	ld [wEnemyMonHappiness], a
 	...

Lowercasing Pokémon names cuts off their first letter

If you simply lowercase all the names in data/pokemon/names.asm and rebuild the ROM, the first letter of their names may be missing. The cause is changing "FARFETCH'D" to "Farfetch'd", because 'd is actually a single character. The fix is to pad its name to the same length as all the rest with an @, so that it becomes "Farfetch'd@".

Pan in cutscenes by making the player invisible

The player's object is always at the center of the screen. So if you want to pan the camera during a cutscene, you have to get around that. The key is the show_object and hide_object movement commands, which make an object visible or invisible.

Let's say you have a map with two placeholder sprites: YOURMAP_CHRIS_PLACEHOLDER, an object_event which uses SPRITE_CHRIS, and YOURMAP_KRIS_PLACEHOLDER, which uses SPRITE_KRIS. Then define these movement scripts:

ShowObjectMovement:
	show_object
	step_end

HideObjectMovement:
	hide_object
	step_end

In your cutscene script, to show the placeholder and make the player invisible:

	checkflag ENGINE_PLAYER_IS_FEMALE
	iftrue .ShowGirlPlaceholder
	appear YOURMAP_CHRIS_PLACEHOLDER
	sjump .HidePlayer
.ShowGirlPlaceholder
	appear YOURMAP_KRIS_PLACEHOLDER
.HidePlayer
	applymovement PLAYER, HideObjectMovement

And to make the player visible again and disappear the placeholder:

	applymovement PLAYER, ShowObjectMovement
	checkflag ENGINE_PLAYER_IS_FEMALE
	iftrue .HideGirlPlaceholder
	disappear YOURMAP_CHRIS_PLACEHOLDER
	sjump .HidPlaceholder
.HideGirlPlaceholder
	disappear YOURMAP_KRIS_PLACEHOLDER
.HidPlaceholder

In between those script snippets, you can move the player around to look like the camera is panning around the map.

Align sprites to the tile grid

Sprites such as NPCs, itemballs, Berry trees, etc are shifted 4 pixels higher than the background tile grid. To align them exactly on the tile grid, as they were in the Space World 1997 demo, edit engine/overworld/map_objects.asm:

 	ld hl, OBJECT_SPRITE_X
 	add hl, bc
 	ld a, [hl]
 	ld hl, OBJECT_SPRITE_X_OFFSET
 	add hl, bc
 	add [hl]
 	add 8
 	ld e, a
 	ld a, [wPlayerBGMapOffsetX]
 	add e
 	ldh [hCurSpriteXPixel], a
 	ld hl, OBJECT_SPRITE_Y
 	add hl, bc
 	ld a, [hl]
 	ld hl, OBJECT_SPRITE_Y_OFFSET
 	add hl, bc
 	add [hl]
-	add 12
+	add 16
 	ld e, a
 	ld a, [wPlayerBGMapOffsetY]
 	add e
 	ldh [hCurSpriteYPixel], a

Prevent NPCs' heads from flipping when they walk down

When NPCs walk up or down, they flip horizontally every other frame to create the alternating left-right step animation. But for NPCs with asymmetrical faces, this can look awkward when they walk down.

To disable this flipping for all overworld sprites, edit data/sprites/facings.asm:

 FacingStepDown3: ; walking down 2
 	db 4 ; #
-	db  0,  8, X_FLIP, $80
-	db  0,  0, X_FLIP, $81
+	db  0,  0, 0, $80
+	db  0,  8, 0, $81
 	db  8,  8, RELATIVE_ATTRIBUTES | X_FLIP, $82
 	db  8,  0, RELATIVE_ATTRIBUTES | X_FLIP, $83

Note that the player sprite in the naming screen and the Town Map are unaffected by this. Also, this prevents the bicycling player sprite from moving side to side.

Use original Cycling Road behavior

The Cycling Road works a little differently in Generation II when compared to RB/FRLG. It's no longer possible to stop riding downhill by holding down the A or B button (or only the latter in FRLG).

To restore the "brake" behavior, edit engine/overworld/player_movement.asm:

; Standing downhill instead moves down.

    ld hl, wBikeFlags
    bit BIKEFLAGS_DOWNHILL_F, [hl]
    ret z

    ld c, a
    and D_PAD
    ret nz

+    ld a, c
+    and A_BUTTON | B_BUTTON
+    ret nz

    ld a, c
    or D_DOWN
    ld [wCurInput], a
    ret

Use HG/SS behavior for fainting without saving on the S.S. Aqua

You may have noticed how when you get on the S.S. Aqua or go to either port without saving, then white out, that you're taken to either your cabin or the Vermilion/Olivine Pokémon Center instead of the Pokémon Center you last used.

To go to the last Pokémon Center, like HG/SS does, remove the blackoutmod script commands from maps/OlivinePort.asm, maps/VermilionPort.asm, and maps/FastShip1F.asm.

Use more space in the home bank

The home bank, ROM0, is important because its code is accessible no matter which ROMX bank is currently switched to. It's only $4000 (16,384) bytes, and some of those are required for the rst and interrupt jump vectors and the cartridge header.

All the home code in pokecrystal is INCLUDEd in the "Home" SECTION in home.asm, starting at address $0150. However, if you really need more space, you can put code in home/header.asm, as long as you're careful not to interfere with the interrupts or the cartridge header. In particular, between the joypad interrupt at $0060 and the cartridge header at $0150, there are 157 unused bytes ($0100 - $0060 = $00A0 = 160, three bytes of which are taken by jp Joypad).

For very short pieces of code or data, you can even put them in the unused space of the rst and interrupt vectors. For example, jp VBlank only uses three of the eight bytes in the "vblank" SECTION.

Clone this wiki locally