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
- Define new constants
- Update moves with their categories
- Mask out the category in
PrintMoveType
- Mask out the category in nine places in the battle engine
- Make Hidden Power special
- Update the AI to understand categories
- Support printing category names
- Display categories in battle
- 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:
- 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.
- 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 keepingPHYSICAL
<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; everySPECIAL
+ type combination will be greater than everyPHYSICAL
+ 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.
PrintMoveType
3. Mask out the category in 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
BattleCommand_Stab
(actually, this usage callsGetBattleVarAddr
instead ofGetBattleVar
)BattleCommand_Stab
again (although this one callsGetBattleVar
)CheckTypeMatchup
BattleCommand_DamageCalc
CheckMoveTypeMatchesTarget
Be careful not to add this code to DoMove
. Doing so will cause certain moves to have the wrong effect.
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!