-
Notifications
You must be signed in to change notification settings - Fork 817
Add Hail as a new weather condition
This tutorial is for how to add Hail as a new weather condition. Though it focuses on a specific weather condition, this can be used as a guideline to add any other crazy and wacky weather you wish. Since Hail has a lot of similarities to Sandstorm, we can use it as a template in which we base our modifications on.
- Define some new weather constants
- Add new text related to the weather condition
- Add damage-inflicting effect
- Add Blizzard accuracy bypass effect
- Halve Solar Beam's power during Hail
- Add move that triggers the weather condition
- Teach the AI how to use this weather effectively
- Make it look flashy!
Edit constants/battle_constants.asm:
; values in wBattleWeather
const_def
const WEATHER_NONE
const WEATHER_RAIN
const WEATHER_SUN
const WEATHER_SANDSTORM
+ const WEATHER_HAIL
const WEATHER_RAIN_END
const WEATHER_SUN_END
const WEATHER_SANDSTORM_END
+ const WEATHER_HAIL_END
We need to create some new battle text to go along with the most relevant effects that our weather condition shall have (such as when it is started/ended, etc).
Edit data/text/battle.asm:
BattleText_TheSandstormRages:
text "The sandstorm"
line "rages."
prompt
+BattleText_HailContinuesToFall:
+ text "Hail continues to"
+ line "fall."
+ prompt
+
BattleText_TheRainStopped:
text "The rain stopped."
prompt
...
BattleText_TheSandstormSubsided:
text "The sandstorm"
line "subsided."
prompt
+BattleText_TheHailStopped:
+ text "The hail stopped."
+ prompt
+
BattleText_EnemyMonFainted:
text "Enemy @"
text_ram wEnemyMonNick
...
BattleText_NoTimeLeftToday: ; unreferenced
text "There is no time"
line "left today!"
done
+ItStartedToHailText:
+ text "It started"
+ line "to hail!"
+ prompt
+
+PeltedByHailText:
+ text "<USER>"
+ line "is pelted by HAIL!"
+ prompt
+
Using Sandstorm as a guideline, we can create the actual effects of this new weather condition. This means inflicting some damage to all Pokémon, possibly excluding Ice-types. For Sandstorm, this is done core.asm.
Edit HandleWeather
in engine/battle/core.asm:
HandleWeather:
...
ld hl, wWeatherCount
dec [hl]
- jr z, .ended
+ jr nz, .continues
+; ended
+ ld hl, .WeatherEndedMessages
+ call .PrintWeatherMessage
+ xor a
+ ld [wBattleWeather], a
+ ret
+
+.continues
ld hl, .WeatherMessages
call .PrintWeatherMessage
ld a, [wBattleWeather]
cp WEATHER_SANDSTORM
- ret nz
+ jr nz, .check_hail
ldh a, [hSerialConnectionStatus]
cp USING_EXTERNAL_CLOCK
jr z, .enemy_first
...
ld hl, SandstormHitsText
jp StdBattleTextbox
-.ended
- ld hl, .WeatherEndedMessages
- call .PrintWeatherMessage
- xor a
- ld [wBattleWeather], a
- ret
+.check_hail
+ ld a, [wBattleWeather]
+ cp WEATHER_HAIL
+ ret nz
+
+ ldh a, [hSerialConnectionStatus]
+ cp USING_EXTERNAL_CLOCK
+ jr z, .enemy_first_hail
+
+; player first
+ call SetPlayerTurn
+ call .HailDamage
+ call SetEnemyTurn
+ jr .HailDamage
+
+.enemy_first_hail
+ call SetEnemyTurn
+ call .HailDamage
+ call SetPlayerTurn
+
+.HailDamage:
+ ld a, BATTLE_VARS_SUBSTATUS3
+ call GetBattleVar
+ bit SUBSTATUS_UNDERGROUND, a
+ ret nz
+
+ ld hl, wBattleMonType1
+ ldh a, [hBattleTurn]
+ and a
+ jr z, .ok1
+ ld hl, wEnemyMonType1
+.ok1
+ ld a, [hli]
+ cp ICE
+ ret z
+
+ ld a, [hl]
+ cp ICE
+ ret z
+
+ call GetSixteenthMaxHP
+ call SubtractHPFromUser
+
+ ld hl, PeltedByHailText
+ jp StdBattleTextbox
.PrintWeatherMessage:
ld a, [wBattleWeather]
...
.WeatherMessages:
; entries correspond to WEATHER_* constants
dw BattleText_RainContinuesToFall
dw BattleText_TheSunlightIsStrong
dw BattleText_TheSandstormRages
+ dw BattleText_HailContinuesToFall
.WeatherEndedMessages:
; entries correspond to WEATHER_* constants
dw BattleText_TheRainStopped
dw BattleText_TheSunlightFaded
dw BattleText_TheSandstormSubsided
+ dw BattleText_TheHailStopped
...
Some things to note. The .ended
branch was moved up so as to avoid an unnecessary jp
instruction after adding the Hail handling, and moves it closer to where the actual weather count check is made. If the weather effect is otherwise still continuing, then the code jumps to the .continues
branch to execute the right weather effect.
Most of the code for Hail is copied from the Sandstorm handling, with exception of the text to print and the types that are unaffected. Additionally, the animation is removed because we have yet to add any fancy visuals to the new effects (this will optionally be added later). Lastly, the actual battle text data we added in the previous step are referenced for use here.
Another effect of Hail is making it so that Blizzard does not miss when it is active. We can handle this the same way that the game handles Thunder accuracy under the effects of rain.
First we should associate Blizzard with a new specific move effect, which will be subject to the Hail accuracy handling we will implement. (More detail on this process in this tutorial).
Edit constants/move_effect_constants.asm:
; MoveEffectsPointers indexes (see data/moves/effects_pointers.asm)
const_def
...
const EFFECT_BEAT_UP
const EFFECT_FLY
const EFFECT_DEFENSE_CURL
+ const EFFECT_BLIZZARD
DEF NUM_MOVE_EFFECTS EQU const_value
Edit data/moves/moves.asm:
Moves:
; entries correspond to constants/move_constants.asm
table_width MOVE_LENGTH, Moves
...
move ICE_BEAM, EFFECT_FREEZE_HIT, 95, ICE, 100, 10, 10
- move BLIZZARD, EFFECT_FREEZE_HIT, 120, ICE, 70, 5, 10
+ move BLIZZARD, EFFECT_BLIZZARD, 120, ICE, 70, 5, 10
move PSYBEAM, EFFECT_CONFUSE_HIT, 65, PSYCHIC_TYPE, 100, 20, 10
...
assert_table_length NUM_ATTACKS
For the effect pointer, we will reuse FreezeHit
, since for the purposes of what commands the move will call, it is functionally identical to that effect. We don't need to copy it for a new redundant effect.
Edit data/moves/effects_pointers.asm:
MoveEffectsPointers:
; entries correspond to EFFECT_* constants
table_width 2, MoveEffectsPointers
...
dw BeatUp
dw Fly
dw DefenseCurl
+ dw FreezeHit ; for Blizzard, purposefully with different EFFECT_* constant
assert_table_length NUM_MOVE_EFFECTS
Now we can check for this move effect when performing the accuracy checks, similar to how Thunder works in rain.
Edit BattleCommand_CheckHit
in engine/battle/effect_commands.asm:
BattleCommand_CheckHit:
...
call .ThunderRain
ret z
+ call .BlizzardHail
+ ret z
+
call .XAccuracy
ret nz
...
cp WEATHER_RAIN
ret
+.BlizzardHail:
+; Return z if the current move always hits in hail, and it is hailing.
+ ld a, BATTLE_VARS_MOVE_EFFECT
+ call GetBattleVar
+ cp EFFECT_BLIZZARD
+ ret nz
+
+ ld a, [wBattleWeather]
+ cp WEATHER_HAIL
+ ret
+
.XAccuracy:
ld a, BATTLE_VARS_SUBSTATUS4
call GetBattleVar
...
BattleCommand_CheckHit
is the routine which checks/decides whether any move (with some exceptions) will connect or not. There are special cases, such as Thunder in rain or having the "Locked-On" substatus. Here we add a subroutine which checks whether it's Blizzard (or more specifically, a move that has the effect EFFECT_BLIZZARD
) and it's hailing, in which case it completely bypasses the accuracy check.
During hail, Solar Beam's power is reduced by 50%, so we need to implement this. Go to data/battle/weather_modifiers.asm:
WeatherTypeModifiers:
db WEATHER_RAIN, WATER, MORE_EFFECTIVE
db WEATHER_RAIN, FIRE, NOT_VERY_EFFECTIVE
db WEATHER_SUN, FIRE, MORE_EFFECTIVE
db WEATHER_SUN, WATER, NOT_VERY_EFFECTIVE
db -1 ; end
WeatherMoveModifiers:
db WEATHER_RAIN, EFFECT_SOLARBEAM, NOT_VERY_EFFECTIVE
+ db WEATHER_HAIL, EFFECT_SOLARBEAM, NOT_VERY_EFFECTIVE
db -1 ; end
We have successfully added a weather condition that we can never trigger. Let's fix that by introducing a new move, Hail. You can follow this tutorial to learn how to add a new move, with the caveat that it should have a new move effect which we will implement, EFFECT_HAIL
. Its battle properties are HAIL, EFFECT_HAIL, 0, ICE, 100, 10, 0
. Let's define what this effect does.
Modify constants/move_effect_constants.asm again:
; MoveEffectsPointers indexes (see data/moves/effects_pointers.asm)
const_def
...
const EFFECT_BEAT_UP
const EFFECT_FLY
const EFFECT_DEFENSE_CURL
const EFFECT_BLIZZARD
+ const EFFECT_HAIL
DEF NUM_MOVE_EFFECTS EQU const_value
Edit data/moves/effects_pointers.asm again:
MoveEffectsPointers:
; entries correspond to EFFECT_* constants
table_width 2, MoveEffectsPointers
...
dw BeatUp
dw Fly
dw DefenseCurl
dw FreezeHit ; for Blizzard, purposefully with different EFFECT_* constant
+ dw Hail
assert_table_length NUM_MOVE_EFFECTS
Edit data/moves/effects.asm:
MoveEffects: ; used only for BANK(MoveEffects)
...
DefenseCurl:
checkobedience
usedmovetext
doturn
defenseup
curl
lowersub
statupanim
raisesub
statupmessage
statupfailtext
endmove
+Hail:
+ checkobedience
+ usedmovetext
+ doturn
+ starthail
+ endmove
+
Edit macros/scripts/battle_commands.asm:
; BattleCommandPointers indexes (see data/battle/effect_command_pointers.asm)
...
command supereffectivelooptext ; ad
command startloop ; ae
command curl ; af
+ command starthail ; b0
DEF NUM_EFFECT_COMMANDS EQU const_value - 1
const_def -1, -1
command endmove ; ff
command endturn ; fe
Edit data/battle/effect_command_pointers.asm:
BattleCommandPointers:
; entries correspond to macros/scripts/battle_commands.asm
table_width 2, BattleCommandPointers
...
dw BattleCommand_SuperEffectiveLoopText
dw BattleCommand_StartLoop
dw BattleCommand_Curl
+ dw BattleCommand_StartHail
assert_table_length NUM_EFFECT_COMMANDS
Here we introduced a new effect for starting the hail weather condition, and associated it to a battle command routine which will be called when the move is used successfully. Again we can draw inspiration from the Sandstorm related routine, BattleCommand_StartSandstorm
.
Create engine/battle/move_effects/hail.asm:
+BattleCommand_StartHail:
+; starthail
+
+ ld a, [wBattleWeather]
+ cp WEATHER_HAIL
+ jr z, .failed
+
+ ld a, WEATHER_HAIL
+ ld [wBattleWeather], a
+ ld a, 5
+ ld [wWeatherCount], a
+ ld hl, ItStartedToHailText
+ jp StdBattleTextbox
+
+.failed
+ call AnimateFailedMove
+ jp PrintButItFailed
+
Edit engine/battle/effect_commands.asm again:
INCLUDE "engine/battle/move_effects/future_sight.asm"
INCLUDE "engine/battle/move_effects/thunder.asm"
+INCLUDE "engine/battle/move_effects/hail.asm"
CheckHiddenOpponent:
...
This routine simply loads the weather with the hail constant we defined, and sets its remaining turns to 5; in case hail is already set up, it'll fail. The function also prints text depending on what happens. For all practical purposes, our new weather is now implemented.
If you run this at this stage, the new move is functional and the effects work as intended, but we can still do some extra steps in order to polish it. The rest of the sections should be considered optional.
There is already some rudimentary logic on how the AI uses the other weather effects. Let's implement some basic checking to have the AI also use Hail in a (minimally) effective manner.
Modify engine/battle/ai/redundant.asm:
.Moves:
...
dbw EFFECT_MOONLIGHT, .Moonlight
dbw EFFECT_SWAGGER, .Swagger
dbw EFFECT_FUTURE_SIGHT, .FutureSight
+ dbw EFFECT_HAIL, .Hail
db -1
...
+.Hail:
+ ld a, [wBattleWeather]
+ cp WEATHER_HAIL
+ jr z, .Redundant
+ jr .NotRedundant
+
.Heal:
.MorningSun:
.Synthesis:
.Moonlight:
...
This file lists some moves that the AI can quickly check for redundancy. Hail has a quick redundancy check in which, if it's hailing, then the move should be dismissed.
Edit engine/battle/ai/scoring.asm:
AI_Smart_EffectHandlers:
...
dbw EFFECT_SOLARBEAM, AI_Smart_Solarbeam
dbw EFFECT_THUNDER, AI_Smart_Thunder
dbw EFFECT_FLY, AI_Smart_Fly
+ dbw EFFECT_HAIL, AI_Smart_Hail
db -1 ; end
...
.SandstormImmuneTypes:
db ROCK
db GROUND
db STEEL
db -1 ; end
+AI_Smart_Hail:
+; Greatly discourage this move if the player is immune to Hail damage.
+ ld a, [wBattleMonType1]
+ cp ICE
+ jr z, .greatly_discourage
+
+ ld a, [wBattleMonType2]
+ cp ICE
+ jr z, .greatly_discourage
+
+; Discourage this move if player's HP is below 50%.
+ call AICheckPlayerHalfHP
+ jr nc, .discourage
+
+; Encourage move if AI has good Hail moves
+ push hl
+ ld hl, .GoodHailMoves
+ call AIHasMoveInArray
+ pop hl
+ jr c, .encourage
+
+; 50% chance to encourage this move otherwise.
+ call AI_50_50
+ ret c
+
+.encourage
+ dec [hl]
+ ret
+
+.greatly_discourage
+ inc [hl]
+.discourage
+ inc [hl]
+ ret
+
+.GoodHailMoves
+ db BLIZZARD
+ db -1 ; end
+
AI_Smart_Endure:
ld a, [wEnemyProtectCount]
and a
...
We base this behavior on the Sunny Day/Rain Dance and Sandstorm AI-related routines. The AI will avoid using the move if the player is immune to Hail or has low HP, and will encourage the move if it has any "Good Hail moves", which right now is only composed of the move Blizzard. This list is fully extensible with other moves.
The move and weather have no special animations. It's completely possible to reuse some other move animation (such as Powder Snow), but we can quickly whip something up with the resources we already have at our disposal. The Sandstorm animation already works well as a "weather hazard," and there are some sprites representing ice, so let's make use of them.
Edit constants/battle_anim_constants.asm:
; BattleAnimObjects indexes (see data/battle_anims/objects.asm)
const_def
...
const ANIM_OBJ_PLAYERHEAD_1ROW
const ANIM_OBJ_ENEMYFEET_2ROW
const ANIM_OBJ_PLAYERHEAD_2ROW
+ const ANIM_OBJ_HAIL
DEF NUM_ANIM_OBJS EQU const_value
; BattleAnimFrameData indexes (see data/battle_anims/framesets.asm)
const_def
...
const BATTLEANIMFRAMESET_B6
const BATTLEANIMFRAMESET_B7
const BATTLEANIMFRAMESET_B8
+ const BATTLEANIMFRAMESET_HAIL
DEF NUM_BATTLEANIMFRAMESETS EQU const_value
; BattleAnimOAMData indexes (see data/battle_anims/oam.asm)
const_def
...
const BATTLEANIMOAMSET_D5
const BATTLEANIMOAMSET_D6
const BATTLEANIMOAMSET_D7
+ const BATTLEANIMOAMSET_HAIL
DEF NUM_BATTLEANIMOAMSETS EQU const_value
Edit data/battle_anims/framesets.asm:
BattleAnimFrameData:
; entries correspond to BATTLEANIMFRAMESET_* constants
table_width 2, BattleAnimFrameData
...
dw .Frameset_b6 ; BATTLEANIMFRAMESET_B6
dw .Frameset_b7 ; BATTLEANIMFRAMESET_B7
dw .Frameset_b8 ; BATTLEANIMFRAMESET_B8
+ dw .Frameset_Hail ; BATTLEANIMFRAMESET_HAIL
assert_table_length NUM_BATTLEANIMFRAMESETS
...
.Frameset_b8:
oamframe BATTLEANIMOAMSET_D7, 8
oamend
+
+.Frameset_Hail:
+ oamframe BATTLEANIMOAMSET_HAIL, 32
+ oamend
Edit data/battle_anims/oam.asm:
BattleAnimOAMData:
; entries correspond to BATTLEANIMOAMSET_* constants
table_width 4, BattleAnimOAMData
...
battleanimoam $00, 6, .OAMData_d5 ; BATTLEANIMOAMSET_D5
battleanimoam $00, 14, .OAMData_d6 ; BATTLEANIMOAMSET_D6
battleanimoam $00, 12, .OAMData_d7 ; BATTLEANIMOAMSET_D7
+ battleanimoam $00, 13, .OAMData_Hail ; BATTLEANIMOAMSET_HAIL
assert_table_length NUM_BATTLEANIMOAMSETS
...
dbsprite 6, -2, 4, 0, $00, $0
dbsprite 8, -4, 4, 0, $00, $0
dbsprite 10, -2, 4, 0, $00, $0
+.OAMData_Hail:
+ dbsprite -13, -2, 4, 0, $04, $0
+ dbsprite -11, -4, 4, 0, $04, $0
+ dbsprite -9, -1, 4, 0, $04, $0
+ dbsprite -7, -5, 4, 0, $04, $0
+ dbsprite -5, -3, 4, 0, $04, $0
+ dbsprite -3, -5, 4, 0, $04, $0
+ dbsprite -1, -3, 4, 0, $04, $0
+ dbsprite 0, -3, 4, 0, $04, $0
+ dbsprite 2, -5, 4, 0, $04, $0
+ dbsprite 4, 0, 4, 0, $04, $0
+ dbsprite 6, -2, 4, 0, $04, $0
+ dbsprite 8, -4, 4, 0, $04, $0
+ dbsprite 10, -2, 4, 0, $04, $0
+
These are simply some frame and OAM data that we'll use to get the proper tile from the tileset we'll load along with the new animation. The structure of the OAMData_Hail
in particular is the same as Sandstorm's, but with a different tile stored in VRAM. Now we should create an object that will take the form of this little "hail crystal".
Modify data/battle_anims/objects.asm:
BattleAnimObjects:
; entries correspond to ANIM_OBJ_* constants
table_width BATTLEANIMOBJ_LENGTH, BattleAnimObjects
...
; ANIM_OBJ_ENEMYFEET_2ROW
battleanimobj ABSOLUTE_X, $00, BATTLEANIMFRAMESET_B7, BATTLEANIMFUNC_NULL, PAL_BATTLE_OB_ENEMY, ANIM_GFX_PLAYERHEAD
; ANIM_OBJ_PLAYERHEAD_2ROW
battleanimobj ABSOLUTE_X, $00, BATTLEANIMFRAMESET_B8, BATTLEANIMFUNC_NULL, PAL_BATTLE_OB_PLAYER, ANIM_GFX_ENEMYFEET
+; ANIM_OBJ_HAIL
+ battleanimobj RELATIVE_X | X_FLIP, $00, BATTLEANIMFRAMESET_HAIL, BATTLEANIMFUNC_RAIN_SANDSTORM, PAL_BATTLE_OB_BLUE, ANIM_GFX_ICE
assert_table_length NUM_ANIM_OBJS
Objects are the data structures that serve as the "particles" in the game's animation engine. So having the object created, we need to use them in some form of animation sequence. We're going to create two different animations, which will use the same sequence. Since the animation constant for a move corresponds to it the move's constant already, we only need to define one extra animation constant for the situation where the hail should play between turns (such as the case with Sandstorm).
Edit constants/move_constants.asm:
; battle anims
...
const ANIM_WOBBLE ; 113
const ANIM_SHAKE ; 114
const ANIM_HIT_CONFUSION ; 115
+ const ANIM_IN_HAIL ; 116
DEF NUM_BATTLE_ANIMS EQU const_value - 1
Modify data/moves/animations.asm:
BattleAnimations::
; entries correspond to constants/move_constants.asm
table_width 2, BattleAnimations
...
dw BattleAnim_Whirlpool
dw BattleAnim_BeatUp
+ dw BattleAnim_Hail
assert_table_length NUM_ATTACKS + 1
- dw BattleAnim_252
dw BattleAnim_253
dw BattleAnim_254
dw BattleAnim_SweetScent2
...
dw BattleAnim_Wobble
dw BattleAnim_Shake
dw BattleAnim_HitConfusion
+ dw BattleAnim_InHail
assert_table_length NUM_BATTLE_ANIMS + 1
BattleAnim_0:
-BattleAnim_252:
BattleAnim_253:
BattleAnim_254:
BattleAnim_MirrorMove:
anim_ret
...
anim_obj ANIM_OBJ_HIT_BIG_YFIX, 136, 48, $0
anim_wait 8
anim_call BattleAnim_ShowMon_0
anim_ret
+BattleAnim_Hail:
+BattleAnim_InHail:
+ anim_1gfx ANIM_GFX_ICE
+ anim_bgeffect ANIM_BG_WHITE_HUES, $0, $8, $0
+ anim_obj ANIM_OBJ_HAIL, 88, 0, $0
+ anim_wait 8
+ anim_obj ANIM_OBJ_HAIL, 72, 0, $1
+ anim_wait 8
+ anim_obj ANIM_OBJ_HAIL, 56, 0, $2
+.loop
+ anim_sound 0, 1, SFX_SHINE
+ anim_wait 8
+ anim_loop 8, .loop
+ anim_wait 8
+ anim_ret
+
BattleAnimSub_Drain:
anim_obj ANIM_OBJ_DRAIN, 132, 44, $0
anim_obj ANIM_OBJ_DRAIN, 132, 44, $8
anim_obj ANIM_OBJ_DRAIN, 132, 44, $10
...
The animation is now complete, so now we can call it whenever we need to show some hailing in the battle screen.
Edit HandleWeather.HailDamage
in engine/battle/core.asm:
.HailDamage:
...
ld a, [hl]
cp ICE
ret z
+ call SwitchTurnCore
+ xor a
+ ld [wNumHits], a
+ ld de, ANIM_IN_HAIL
+ call Call_PlayBattleAnim
+ call SwitchTurnCore
+
call GetSixteenthMaxHP
call SubtractHPFromUser
ld hl, PeltedByHailText
jp StdBattleTextbox
Modify the earlier file we created, BattleCommand_StartHail
in engine/battle/move_effects/hail.asm:
BattleCommand_StartHail:
; starthail
...
ld a, WEATHER_HAIL
ld [wBattleWeather], a
ld a, 5
ld [wWeatherCount], a
+ call AnimateCurrentMove
ld hl, ItStartedToHailText
jp StdBattleTextbox
...
And with that, everything is in place, and now we have a brand new weather type to use in the game! The main point of this tutorial was to show how one can simply use the resources already available to create something new. In a lot of cases, it helps to check similar systems already implemented in the codebase.