Physical Special split - pret/pokecrystal GitHub Wiki

In generations 2 and 3 some types were always physical and some were always special. Here's how to implement the physical/special split from Gen 4 and up.

(The code for this feature was adapted from Pokémon Fractal (the Axyllia region) and Polished Crystal.)

Contents

  1. Define new constants
  2. Update moves with their categories
  3. Mask out the category in PrintMoveType
  4. Mask out the category in nine places in the battle engine
  5. Make Hidden Power special
  6. Update the AI to understand categories
  7. Support printing category names
  8. Display categories in battle
  9. Display categories in the Move screen

1. Define new constants

Edit constants/type_constants.asm:

 	const_def
-
-DEF PHYSICAL EQU const_value
 	const NORMAL
 	const FIGHTING
 	const FLYING
 	const POISON
 	const GROUND
 	const ROCK
 	const BIRD
 	const BUG
 	const GHOST
 	const STEEL

 DEF UNUSED_TYPES EQU const_value
 	const_next 19
 	const CURSE_TYPE
 DEF UNUSED_TYPES_END EQU const_value

-DEF SPECIAL EQU const_value
 	const FIRE
 	const WATER
 	const GRASS
 	const ELECTRIC
 	const PSYCHIC_TYPE
 	const ICE
 	const DRAGON
 	const DARK
 DEF TYPES_END EQU const_value
+
+DEF TYPE_MASK EQU %00111111
+DEF PHYSICAL  EQU %01000000
+DEF SPECIAL   EQU %10000000
+DEF STATUS    EQU %11000000

We're going to store each move's type and category in the same byte. This works well for two reasons:

  1. A byte has eight bits. Two bits can store four values, which is enough for the three categories; and the remaining six can store 64 values, which is more than enough for all the types, even with those unused middle ones. We'll just have to be careful to mask out the category bits when dealing with types alone.
  2. Throughout the code, moves' categories are checked by comparing their type value with the SPECIAL constant; values less than it are physical, otherwise they're special. We're keeping PHYSICAL < SPECIAL, so those checks will all still work. The category bits are higher than the type bits, so the type won't interfere with this relation; every SPECIAL + type combination will be greater than every PHYSICAL + type one.

2. Update moves with their categories

Edit data/moves/moves.asm:

 MACRO move
	db \1 ; animation
	db \2 ; effect
	db \3 ; power
-	db \4 ; type
-	db \5 percent ; accuracy
-	db \6 ; pp
-	db \7 percent ; effect chance
-	assert \6 <= 40, "PP must be 40 or less"
+	db \4 | \5 ; type
+	db \6 percent ; accuracy
+	db \7 ; pp
+	db \8 percent ; effect chance
+	assert \7 <= 40, "PP must be 40 or less"
 ENDM

 Moves:
 ; entries correspond to move ids (see constants/move_constants.asm)
 	table_width MOVE_LENGTH, Moves
-	move POUND,        EFFECT_NORMAL_HIT,         40, NORMAL,   100, 35,   0
-	...
-	move BEAT_UP,      EFFECT_BEAT_UP,            10, DARK,     100, 10,   0
+	move POUND,        EFFECT_NORMAL_HIT,         40, NORMAL,   PHYSICAL, 100, 35,   0
+	...
+	move BEAT_UP,      EFFECT_BEAT_UP,            10, DARK,     PHYSICAL, 100, 10,   0
 	assert_table_length NUM_ATTACKS

You'll have to assign the right category—PHYSICAL, SPECIAL, or STATUS—to all 251 moves, right after their types. There's a file which already does this with the default pokecrystal moves here.

3. Mask out the category in PrintMoveType

Edit engine/pokemon/types.asm:

 PrintMoveType:
 ; Print the type of move b at hl.

 	push hl
 	ld a, b
 	dec a
 	ld bc, MOVE_LENGTH
 	ld hl, Moves
 	call AddNTimes
 	ld de, wStringBuffer1
 	ld a, BANK(Moves)
 	call FarCopyBytes
 	ld a, [wStringBuffer1 + MOVE_TYPE]
+	and TYPE_MASK
 	pop hl

This is the first of many times we'll have to add and TYPE_MASK somewhere.

4. Mask out the category in nine places in the battle engine

First, edit engine/battle/effect_commands.asm. There are five places in the code where we need to do this:

 	ld a, BATTLE_VARS_MOVE_TYPE
 	call GetBattleVar
+	and TYPE_MASK
  1. BattleCommand_Stab (actually, this usage calls GetBattleVarAddr instead of GetBattleVar)
  2. BattleCommand_Stab again (although this one calls GetBattleVar)
  3. CheckTypeMatchup
  4. BattleCommand_DamageCalc
  5. CheckMoveTypeMatchesTarget

Just use your text editor to find all five occurrences of "BATTLE_VARS_MOVE_TYPE" in the file, and make that one-line change to all of them.

Next, edit engine/battle/move_effects/conversion.asm:

 	ld hl, Moves + MOVE_TYPE
 	call GetMoveAttr
+	and TYPE_MASK

Edit engine/battle/move_effects/conversion2.asm:

 	ld hl, Moves + MOVE_TYPE
 	call GetMoveAttr
+	and TYPE_MASK
 	...
 	ld a, BATTLE_VARS_MOVE_TYPE
 	call GetBattleVarAddr
+	and TYPE_MASK

And edit engine/battle/move_effects/thunder.asm:

 	ld a, BATTLE_VARS_MOVE_TYPE
 	call GetBattleVarAddr
+	and TYPE_MASK

That's nine additions of and TYPE_MASK to mask out the category bits and leave only the type.

5. Make Hidden Power special

Edit engine/battle/hidden_power.asm:

 ; Overwrite the current move type.
 	push af
 	ld a, BATTLE_VARS_MOVE_TYPE
 	call GetBattleVarAddr
 	pop af
+	or SPECIAL
 	ld [hl], a

If you're using an older version of pokecrystal, you may also have to edit another line to make sure Hidden Power's type is calculated correctly:

 ; Skip unused types
 	cp UNUSED_TYPES
 	jr c, .done
-	add SPECIAL - UNUSED_TYPES
+	add UNUSED_TYPES_END - UNUSED_TYPES

6. Update the AI to understand categories

At this point the Physical/Special split works, technically, but for two things: the AI doesn't fully understand it, and the user interface doesn't show it. We'll take care of the AI first.

Edit engine/battle/ai/scoring.asm. There are a few unrelated changes to make here, so let's go over them one at a time.

 AI_Types:
 	...
 ; Discourage this move if there are any moves
 ; that do damage of a different type.
 	push hl
 	push de
 	push bc
 	ld a, [wEnemyMoveStruct + MOVE_TYPE]
+	and TYPE_MASK
 	ld d, a
 	...
 	call AIGetEnemyMove
 	ld a, [wEnemyMoveStruct + MOVE_TYPE]
+	and TYPE_MASK
 	cp d

Here we're just masking out categories again.

 AI_Smart_SpDefenseUp2:

 	...

 ; 80% chance to greatly encourage this move if
-; enemy's Special Defense level is lower than +2, and the player is of a special type.
+; enemy's Special Defense level is lower than +2,
+; and the player's Pokémon is Special-oriented.
 	cp BASE_STAT_LEVEL + 2
 	ret nc

-	ld a, [wBattleMonType1]
-	cp SPECIAL
-	jr nc, .encourage
-	ld a, [wBattleMonType2]
-	cp SPECIAL
-	ret c
+	push hl
+; Get the pointer for the player's Pokémon's base Attack
+	ld a, [wBattleMonSpecies]
+	ld hl, BaseData + BASE_ATK
+	ld bc, BASE_DATA_SIZE
+	call AddNTimes
+; Get the Pokémon's base Attack
+	ld a, BANK(BaseData)
+	call GetFarByte
+	ld d, a
+; Get the pointer for the player's Pokémon's base Special Attack
+	ld bc, BASE_SAT - BASE_ATK
+	add hl, bc
+; Get the Pokémon's base Special Attack
+	ld a, BANK(BaseData)
+	call GetFarByte
+	pop hl
+; If its base Attack is greater than its base Special Attack,
+; don't encourage this move.
+	cp d
+	ret c

 .encourage
 	call AI_80_20
 	ret c
 	dec [hl]
 	dec [hl]
 	ret

This routine used to encourage the AI to use moves that raise its Special Defense if the player's Pokémon was of a Special type. Since physical/special categories are now independent of types, we've changed it to check whether the player's base Special Attack is at least as high as its base Attack.

 AI_Smart_Encore:
 	...

 	push hl
 	ld a, [wEnemyMoveStruct + MOVE_TYPE]
+	and TYPE_MASK
 	ld hl, wEnemyMonType1
 	predef CheckTypeMatchup

Just masking out the category again.

 AI_Smart_Curse:
 	...

 	ld a, [wBattleMonType1]
 	cp GHOST
 	jr z, .greatly_encourage
-	cp SPECIAL
-	ret nc
-	ld a, [wBattleMonType2]
-	cp SPECIAL
-	ret nc
 	call AI_80_20
 	ret c
 	dec [hl]
 	dec [hl]
 	ret

This routine used to discourage the AI from using Curse if the player's Pokémon was of a Special type (since Curse raises the user's Defense, which is useless against special attacks). That's no longer meaningful, but it's not worth checking the player's base stats again.

7. Support printing category names

Create data/types/category_names.asm:

+CategoryNames:
+	dw .Physical
+	dw .Special
+	dw .Status
+
+.Physical: db "PHYSICAL@"
+.Special:  db "SPECIAL@"
+.Status:   db "STATUS@"

Create engine/pokemon/categories.asm:

+GetMoveCategoryName:
+; Copy the category name of move b to wStringBuffer1.
+
+	ld a, b
+	dec a
+	ld bc, MOVE_LENGTH
+	ld hl, Moves + MOVE_TYPE
+	call AddNTimes
+	ld a, BANK(Moves)
+	call GetFarByte
+
+; Mask out the type
+	and ~TYPE_MASK
+; Shift the category bits into the range 0-2
+	rlc a
+	rlc a
+	dec a
+
+	ld hl, CategoryNames
+	ld e, a
+	ld d, 0
+	add hl, de
+	add hl, de
+	ld a, [hli]
+	ld h, [hl]
+	ld l, a
+	ld de, wStringBuffer1
+	ld bc, MOVE_NAME_LENGTH
+	jp CopyBytes
+
+INCLUDE "data/types/category_names.asm"

And edit main.asm:

 INCLUDE "engine/pokemon/types.asm"
+INCLUDE "engine/pokemon/categories.asm"

This is based on the routines in engine/pokemon/types.asm.

8. Display categories in battle

Edit engine/battle/core.asm:

+	farcall UpdateMoveData
+	ld a, [wPlayerMoveStruct + MOVE_ANIM]
+	ld b, a
+	farcall GetMoveCategoryName
 	hlcoord 1, 9
-	ld de, .Type
+	ld de, wStringBuffer1
 	call PlaceString

-	hlcoord 7, 11
+	ld h, b
+	ld l, c
 	ld [hl], "/"

-	callfar UpdateMoveData
 	ld a, [wPlayerMoveStruct + MOVE_ANIM]
 	ld b, a
 	hlcoord 2, 10
 	predef PrintMoveType

 .done
 	ret

 .Disabled:
 	db "Disabled!@"
-.Type:
-	db "TYPE/@"

Instead of printing "TYPE/" in the move property box, we print the move's category.

9. Display categories in the Move screen

Edit engine/pokemon/mon_menu.asm (or engine/menus/start_menu.asm in older versions of pokecrystal):

 PlaceMoveData:
 	xor a
 	ldh [hBGMapMode], a
 	hlcoord 0, 10
 	ld de, String_MoveType_Top
 	call PlaceString
 	hlcoord 0, 11
 	ld de, String_MoveType_Bottom
 	call PlaceString
 	hlcoord 12, 12
 	ld de, String_MoveAtk
 	call PlaceString
+	ld a, [wCurSpecies]
+	ld b, a
+	farcall GetMoveCategoryName
+	hlcoord 1, 11
+	ld de, wStringBuffer1
+	call PlaceString
 	ld a, [wCurSpecies]
 	ld b, a
-	hlcoord 2, 12
+	hlcoord 1, 12
+	ld [hl], "/"
+	inc hl
 	predef PrintMoveType
 	...

 String_MoveType_Top:
-	db "┌─────┐@
+	db "┌────────┐@"
 String_MoveType_Bottom:
-	db "│TYPE/└@"
+	db "│        └@"
.moving_move
	ld a, " "
	hlcoord 1, 11
-	ld bc, 5
+	ld bc, 8
	call ByteFill
	hlcoord 1, 12
	lb bc, 5, SCREEN_WIDTH - 2
	call ClearBox

Again, instead of printing "TYPE/" in the move property box, we print the move's category. There's no room for the "/" after the category, so here it goes before the type.

Now we're done!

Screenshot