Add a new move - pret/pokecrystal GitHub Wiki
This tutorial is for how to add a new move, allowing up to 255 moves. As an example, we'll add Nasty Plot.
Contents
- Define a move constant
- Give it a name and description
- Define its battle properties
- Define its animation
- Let Pokémon learn the move
- Adding a 255th move
1. Define a move constant
Edit constants/move_constants.asm:
; move ids
; indexes for:
; - Moves (see data/moves/moves.asm)
; - MoveNames (see data/moves/names.asm)
; - MoveDescriptions (see data/moves/descriptions.asm)
; - BattleAnimations (see data/moves/animations.asm)
const_def
const NO_MOVE ; 00
const POUND ; 01
...
const BEAT_UP ; fb
+ const NASTY_PLOT ; fc
DEF NUM_ATTACKS EQU const_value - 1
; Battle animations use the same constants as the moves up to this point
const_next $ff
const ANIM_SWEET_SCENT_2 ; ff
...
Move constants are actually a subset of battle animation constants. $01 to $FB are the 251 constants from POUND
to BEAT_UP
; then $FC, $FD, and $FE are unused; then starting at $FF, ANIM_SWEET_SCENT_2
and above correspond to animations beyond the ones played for moves (throwing Poké Balls, showing confusion, etc). Anyway, those three unused values can all be used for new moves.
2. Give it a name and description
Edit data/moves/names.asm:
MoveNames::
list_start MoveNames
li "POUND"
...
li "BEAT UP"
+ li "NASTY PLOT"
assert_list_length NUM_ATTACKS
A name can be up to 12 printed characters long (for example, "'d" counts as one character when printed on a textbox).
Now edit data/moves/descriptions.asm:
MoveDescriptions::
; entries correspond to move ids (see constants/move_constants.asm)
table_width 2, MoveDescriptions
dw PoundDescription
...
dw BeatUpDescription
+ dw NastyPlotDescription
assert_table_length NUM_ATTACKS
- dw MoveFCDescription
dw MoveFDDescription
dw MoveFEDescription
dw MoveFFDescription
dw Move00Description
assert_table_length $100
-MoveFCDescription:
MoveFDDescription:
MoveFEDescription:
MoveFFDescription:
Move00Description:
db "?@"
...
BeatUpDescription:
db "Party #MON join"
next "in the attack.@"
+
+NastyPlotDescription:
+ db "Sharply increases"
+ next "user's SPCL.ATK.@"
A description has two lines, each with up to 18 characters, plus a "@" at the end.
3. Define its battle properties
Edit data/moves/moves.asm:
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 NASTY_PLOT, EFFECT_SP_ATK_UP_2, 0, DARK, 100, 20, 0
assert_table_length NUM_ATTACKS
The move
defines these properties:
- animation: Which animation to play when using the move. Remember, constants like
POUND
correspond to moves but also to battle animations, as we'll see later. - effect: What effect the move has. Valid effects are in constants/move_effect_constants.asm. Some exist that aren't used for any moves yet, like
EFFECT_SP_ATK_UP_2
. - power: The base power. 0 for non-damaging moves; 1 for moves that do damage but not with the standard formula, like Seismic Toss, Counter, or Magnitude. (The AI uses this property to distinguish damaging and non-damaging moves.)
- type: The type.
- accuracy: The accuracy, from 1 to 100.
- PP: The PP, from 5 to 40. Sketch has 1 PP but it requires special-case code in some places; and 40 is the maximum because any more and PP Up could boost it out of bounds. (PP is stored in 6 bits, not a full byte, so cannot exceed 63.)
- effect chance: The chances of an effect triggering. Not applicable for all effects.
4. Define its animation
Edit data/moves/animations.asm:
BattleAnimations::
; entries correspond to constants/move_constants.asm
table_width 2, BattleAnimations
dw BattleAnim_Dummy
dw BattleAnim_Pound
...
dw BattleAnim_BeatUp
+ dw BattleAnim_NastyPlot
assert_table_length NUM_ATTACKS + 1
- dw BattleAnim_Dummy
dw BattleAnim_Dummy
dw BattleAnim_Dummy
dw BattleAnim_SweetScent2
assert_table_length $100
; $100
dw BattleAnim_ThrowPokeBall
...
BattleAnim_Dummy:
BattleAnim_MirrorMove:
anim_ret
...
+BattleAnim_NastyPlot:
BattleAnim_PsychUp:
anim_1gfx ANIM_GFX_STATUS
anim_call BattleAnim_TargetObj_1Row
anim_bgeffect ANIM_BG_CYCLE_MON_LIGHT_DARK_REPEATING, $0, BG_EFFECT_USER, $20
anim_sound 0, 0, SFX_PSYBEAM
anim_obj ANIM_OBJ_PSYCH_UP, 44, 88, $0
anim_obj ANIM_OBJ_PSYCH_UP, 44, 88, $10
anim_obj ANIM_OBJ_PSYCH_UP, 44, 88, $20
anim_obj ANIM_OBJ_PSYCH_UP, 44, 88, $30
anim_wait 64
anim_incbgeffect ANIM_BG_CYCLE_MON_LIGHT_DARK_REPEATING
anim_call BattleAnim_ShowMon_0
anim_wait 16
anim_ret
Designing a new animation is beyond the scope of this tutorial. They require careful placement and timing of different elements, and the scripting system used to do this is poorly understood. Here we're just reusing Psych Up's animation for Nasty Plot, since it looks appropriate.
5. Let Pokémon learn the move
By now the move fully exists—it might show up with Metronome—but no Pokémon can use it. So add it to level-up learnsets in data/pokemon/evos_attacks.asm, egg move sets in data/pokemon/egg_moves.asm, or NPC trainers' parties in data/trainers/parties.asm (see the new Pokémon and new trainer tutorials for help with that). Or add a new TM for it, following the tutorial.
I added NASTY_PLOT
to these sets, based on their canon ones in later generations:
- data/pokemon/evos_attacks.asm: Pikachu, Raichu, Ninetales, Meowth, Persian, Drowzee, Hypno, Mew, Pichu, Aipom, Slowking, Girafarig, Houndour, Houndoom
- data/pokemon/egg_moves.asm: Zubat, Drowzee, Mr. Mime, Togepi, Misdreavus, Houndour, Smoochum
- data/trainers/parties.asm: Karen's Houndoom
6. Adding a 255th move
It's pretty easy to replace the unused moves 252, 253, and 254 ($FC, $FD, and $FE) using the steps above. But move 255 ($FF) is trickier. The value $FF is often used as a special case in the code: it's the maximum value a single byte can have, it marks the end of lists, and using it like any other value can be difficult to impossible.
(If you're wondering why so many lists end with db -1
, not db $ff
, that's because the byte $FF can be 255 or −1 depending on context, due to two's complement arithmetic.)
6.1. Prepare move $FF
Let's say the 255th move will be Fake Out. First, follow the steps as usual. Define FAKE_OUT
after your move $FE; give it a name, description, and battle properties (FAKE_OUT, EFFECT_FAKE_OUT, 40, NORMAL, 100, 10, 0
); give it an animation (it can share BattleAnim_Tackle
); and add it to Pokémon learnsets.
Remember, this time we're introducing a new constant, not replacing an old one. So you also need to change const_next $ff
to const_next $100
for the battle animation constants, or just remove the const_next
since there's no longer a gap between moves and battle animations. This means ANIM_SWEET_SCENT_2
will be shifted from $FF to $100; ANIM_THROW_POKE_BALL
from $100 to $101; and so on.
6.2. Swap move $FF with Struggle
Because $FF (aka 255, aka −1) is treated specially, it's not actually suitable as a move ID. Luckily, Struggle is a move that's treated specially itself: no Pokémon can learn it naturally. So go back to all the files you edited in the previous step, and swap the lines for Struggle ($A5) with the lines for Fake Out ($FF).
MetronomeExcepts
list
6.3. Remove Struggle from the Now that Struggle is move $FF, including it in a list would end the list early. Usually moves would show up in various lists, but since Struggle is not a typical learnable move, it only shows up in one: MetronomeExcepts
, the list of moves Metronome cannot copy.
Edit data/moves/metronome_exception_moves.asm:
MetronomeExcepts:
db NO_MOVE
db METRONOME
- db STRUGGLE
db SKETCH
db MIMIC
db COUNTER
db MIRROR_COAT
db PROTECT
db DETECT
db ENDURE
db DESTINY_BOND
db SLEEP_TALK
db THIEF
db -1
We still don't want Metronome to copy Struggle, so edit engine/battle/move_effects/metronome.asm:
; No invalid moves.
cp NUM_ATTACKS + 1
jr nc, .GetMove
+; No Struggle.
+ cp STRUGGLE
+ jr z, .GetMove
+
; None of the moves in MetronomeExcepts.
push af
ld de, 1
ld hl, MetronomeExcepts
call IsInArray
pop bc
jr c, .GetMove
NUM_ATTACKS + 1
checks
6.4. Get rid of Some places in the code do cp NUM_ATTACKS + 1
to check if a move ID is not too high. If we have 255 moves, then NUM_ATTACKS
+ 1 will be 256 ($100), which won't fit in one byte, so these checks will cause a build error. However, the checks will also be redundant since every move ID is valid, so we can remove them.
Edit engine/battle/move_effects/metronome.asm again:
-; No invalid moves.
- cp NUM_ATTACKS + 1
- jr nc, .GetMove
Edit engine/events/battle_tower/battle_tower.asm:
ValidateBTParty:
; Check for and fix errors in party data
...
.dont_load
ld [wCurPartyLevel], a
ld hl, MON_MOVES
add hl, bc
ld d, NUM_MOVES - 1
ld a, [hli]
and a
- jr z, .not_move
- cp NUM_ATTACKS + 1
- jr nc, .not_move
- jr .valid_move
+ jr nz, .valid_move
-.not_move
dec hl
ld a, POUND
ld [hli], a
xor a
ld [hli], a
ld [hli], a
ld [hl], a
jr .done_moves
.valid_move
- ld a, [hl]
- cp NUM_ATTACKS + 1
- jr c, .next
- ld [hl], $0
-
-.next
- inc hl
+ ld a, [hli]
dec d
jr nz, .valid_move
Edit engine/pokemon/correct_party_errors.asm:
ld hl, wPartyMon1Moves
ld a, [wPartyCount]
ld b, a
.loop5
push hl
ld c, NUM_MOVES
ld a, [hl]
and a
- jr z, .invalid_move
- cp NUM_ATTACKS + 1
- jr c, .moves_loop
+ jr nz, .moves_loop
-.invalid_move
ld [hl], POUND
.moves_loop
ld a, [hl]
and a
- jr z, .fill_invalid_moves
- cp NUM_ATTACKS + 1
- jr c, .next_move
+ jr nz, .next_move
.fill_invalid_moves
...
And edit mobile/mobile_5c.asm:
CheckBTMonMovesForErrors:
ld c, BATTLETOWER_PARTY_LENGTH
ld hl, wBT_OTTempMon1Moves
.loop
push hl
- ld a, [hl]
- cp NUM_ATTACKS + 1
- jr c, .okay
- ld a, POUND
- ld [hl], a
-
-.okay
- inc hl
+ ld a, [hli]
ld b, NUM_MOVES - 1
.loop2
ld a, [hl]
and a
- jr z, .loop3
- cp NUM_ATTACKS + 1
- jr c, .next
+ jr nz, .next
.loop3
...
6.5. Use $00 instead of $FF for Pursuit's effect
There's one more way that $FF as a move ID is special. When Pursuit attacks a Pokémon that's switching out, the value $FF is used to end that attack early. Now that $FF is a valid move, we'll need to use a different value; NO_MOVE
($00) works.
Edit engine/battle/core.asm:
PursuitSwitch:
...
ld a, BATTLE_VARS_MOVE
call GetBattleVarAddr
- ld a, $ff
+ xor a ; NO_MOVE
ld [hl], a
...
And edit engine/battle/effect_commands.asm:
CheckTurn:
BattleCommand_CheckTurn:
; checkturn
; Repurposed as hardcoded turn handling. Useless as a command.
-; Move $ff immediately ends the turn.
+; NO_MOVE immediately ends the turn.
ld a, BATTLE_VARS_MOVE
call GetBattleVar
- inc a
+ and a ; NO_MOVE?
jp z, EndTurn
...
Now you can have 255 moves, as long as the last one is Struggle!