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

  1. Define a move constant
  2. Give it a name and description
  3. Define its battle properties
  4. Define its animation
  5. Let Pokémon learn the move
  6. Adding a 255th move
    1. Prepare move $FF
    2. Swap move $FF with Struggle
    3. Remove Struggle from the MetronomeExcepts list
    4. Get rid of NUM_ATTACKS + 1 checks
    5. Use $00 instead of $FF for Pursuit's effect

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:

Screenshot

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).

6.3. Remove Struggle from the MetronomeExcepts list

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

6.4. Get rid of NUM_ATTACKS + 1 checks

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!