From 2c98b438ce963b49c01a88135ac7207e28b5a76a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Jan 2024 10:55:23 +0100 Subject: [PATCH] Adds Move Shed Tail --- data/battle_scripts_1.s | 28 ++++++++-- include/constants/battle_move_effects.h | 1 + include/constants/battle_string_ids.h | 3 +- src/battle_gfx_sfx_util.c | 4 +- src/battle_main.c | 5 ++ src/battle_message.c | 2 + src/battle_script_commands.c | 8 +-- src/data/battle_moves.h | 2 +- test/battle/move_effect/shed_tail.c | 72 +++++++++++++++++++++++++ test/battle/move_effect/substitute.c | 56 +++++++++++++++++++ 10 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 test/battle/move_effect/shed_tail.c create mode 100644 test/battle/move_effect/substitute.c diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index a07c3cc56d6e..d28afa4877ec 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -370,6 +370,27 @@ gBattleScriptsForMoveEffects:: .4byte BattleScript_EffectHit @ EFFECT_FICKLE_BEAM .4byte BattleScript_EffectHit @ EFFECT_BLIZZARD .4byte BattleScript_EffectHit @ EFFECT_RAIN_ALWAYS_HIT + .4byte BattleScript_EffectShedTail @ EFFECT_SHED_TAIL + +BattleScript_EffectShedTail: + attackcanceler + attackstring + ppreduce + waitstate + jumpifstatus2 BS_ATTACKER, STATUS2_SUBSTITUTE, BattleScript_AlreadyHasSubstitute + jumpifbattletype BATTLE_TYPE_ARENA, BattleScript_ButItFailed + jumpifcantswitch SWITCH_IGNORE_ESCAPE_PREVENTION | BS_ATTACKER, BattleScript_ButItFailed + setsubstitute + jumpifbyte CMP_EQUAL, cMULTISTRING_CHOOSER, B_MSG_SUBSTITUTE_FAILED, BattleScript_SubstituteString + attackanimation + waitanimation + healthbarupdate BS_ATTACKER + datahpupdate BS_ATTACKER + printstring STRINGID_SHEDITSTAIL + waitmessage B_WAIT_TIME_LONG + moveendto MOVEEND_ATTACKER_VISIBLE + moveendfrom MOVEEND_TARGET_VISIBLE + goto BattleScript_MoveSwitchOpenPartyScreen BattleScript_EffectPsychicNoise:: printstring STRINGID_PKMNPREVENTEDFROMHEALING @@ -505,6 +526,7 @@ BattleScript_MoveSwitch: jumpifcantswitch SWITCH_IGNORE_ESCAPE_PREVENTION | BS_ATTACKER, BattleScript_MoveSwitchEnd printstring STRINGID_PKMNWENTBACK waitmessage B_WAIT_TIME_SHORT +BattleScript_MoveSwitchOpenPartyScreen: openpartyscreen BS_ATTACKER, BattleScript_MoveSwitchEnd switchoutabilities BS_ATTACKER waitstate @@ -4179,15 +4201,13 @@ BattleScript_EffectSubstitute:: waitstate jumpifstatus2 BS_ATTACKER, STATUS2_SUBSTITUTE, BattleScript_AlreadyHasSubstitute setsubstitute - jumpifbyte CMP_NOT_EQUAL, cMULTISTRING_CHOOSER, B_MSG_SUBSTITUTE_FAILED, BattleScript_SubstituteAnim - pause B_WAIT_TIME_SHORT - goto BattleScript_SubstituteString -BattleScript_SubstituteAnim:: + jumpifbyte CMP_EQUAL, cMULTISTRING_CHOOSER, B_MSG_SUBSTITUTE_FAILED, BattleScript_SubstituteString attackanimation waitanimation healthbarupdate BS_ATTACKER datahpupdate BS_ATTACKER BattleScript_SubstituteString:: + pause B_WAIT_TIME_SHORT printfromtable gSubstituteUsedStringIds waitmessage B_WAIT_TIME_LONG goto BattleScript_MoveEnd diff --git a/include/constants/battle_move_effects.h b/include/constants/battle_move_effects.h index 4d5195629365..8b427bb1189e 100644 --- a/include/constants/battle_move_effects.h +++ b/include/constants/battle_move_effects.h @@ -350,6 +350,7 @@ enum { EFFECT_FICKLE_BEAM, EFFECT_BLIZZARD, EFFECT_RAIN_ALWAYS_HIT, // Unlike EFFECT_THUNDER, it doesn't get its accuracy reduced under sun. + EFFECT_SHED_TAIL, NUM_BATTLE_MOVE_EFFECTS, }; diff --git a/include/constants/battle_string_ids.h b/include/constants/battle_string_ids.h index de6117e822aa..07a4af185401 100644 --- a/include/constants/battle_string_ids.h +++ b/include/constants/battle_string_ids.h @@ -700,8 +700,9 @@ #define STRINGID_ELECTROSHOCKCHARGING 698 #define STRINGID_ITEMWASUSEDUP 699 #define STRINGID_ATTACKERLOSTITSTYPE 700 +#define STRINGID_SHEDITSTAIL 701 -#define BATTLESTRINGS_COUNT 701 +#define BATTLESTRINGS_COUNT 702 // This is the string id that gBattleStringsTable starts with. // String ids before this (e.g. STRINGID_INTROMSG) are not in the table, diff --git a/src/battle_gfx_sfx_util.c b/src/battle_gfx_sfx_util.c index 2f72197bf982..0aba96a46f64 100644 --- a/src/battle_gfx_sfx_util.c +++ b/src/battle_gfx_sfx_util.c @@ -25,6 +25,8 @@ #include "constants/songs.h" #include "constants/rgb.h" #include "constants/battle_palace.h" +#include "constants/battle_move_effects.h" + extern const u8 gBattlePalaceNatureToMoveTarget[]; extern const struct CompressedSpriteSheet gSpriteSheet_EnemyShadow; @@ -996,7 +998,7 @@ void LoadBattleMonGfxAndAnimate(u8 battler, bool8 loadMonSprite, u8 spriteId) void TrySetBehindSubstituteSpriteBit(u8 battler, u16 move) { - if (move == MOVE_SUBSTITUTE) + if (gBattleMoves[move].effect == EFFECT_SUBSTITUTE || gBattleMoves[move].effect == EFFECT_SHED_TAIL) gBattleSpritesDataPtr->battlerData[battler].behindSubstitute = 1; } diff --git a/src/battle_main.c b/src/battle_main.c index b65ad5229385..d36f8ff38652 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3156,6 +3156,11 @@ void SwitchInClearSetData(u32 battler) gDisableStructs[battler].battlerPreventingEscape = disableStructCopy.battlerPreventingEscape; gDisableStructs[battler].embargoTimer = disableStructCopy.embargoTimer; } + else if (gBattleMoves[gCurrentMove].effect == EFFECT_SHED_TAIL) + { + gBattleMons[battler].status2 |= STATUS2_SUBSTITUTE; + gDisableStructs[battler].substituteHP = disableStructCopy.substituteHP; + } gMoveResultFlags = 0; gDisableStructs[battler].isFirstTurn = 2; diff --git a/src/battle_message.c b/src/battle_message.c index 5279aa53373f..f2826c2b9602 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -837,9 +837,11 @@ static const u8 sText_HospitalityRestoration[] = _("The {B_ATK_PARTNER_NAME} dra static const u8 sText_ElectroShockCharging[] = _("{B_ATK_NAME_WITH_PREFIX} absorbed\nelectricity!"); static const u8 sText_ItemWasUsedUp[] = _("The {B_LAST_ITEM}\nwas used up..."); static const u8 sText_AttackerLostItsType[] = _("{B_ATK_NAME_WITH_PREFIX} lost\nits {B_BUFF1} type!"); +static const u8 sText_ShedItsTail[] = _("{B_ATK_NAME_WITH_PREFIX} shed its tail\nto create a decoy!"); const u8 *const gBattleStringsTable[BATTLESTRINGS_COUNT] = { + [STRINGID_SHEDITSTAIL - BATTLESTRINGS_TABLE_START] = sText_ShedItsTail, [STRINGID_ELECTROSHOCKCHARGING - BATTLESTRINGS_TABLE_START] = sText_ElectroShockCharging, [STRINGID_HOSPITALITYRESTORATION - BATTLESTRINGS_TABLE_START] = sText_HospitalityRestoration, [STRINGID_THESWAMPDISAPPEARED - BATTLESTRINGS_TABLE_START] = sText_TheSwampDisappeared, diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index c12ccab908dd..832f6bd2dc8b 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -12413,8 +12413,10 @@ static void Cmd_setsubstitute(void) { CMD_ARGS(); - u32 hp = GetNonDynamaxMaxHP(gBattlerAttacker) / 4; - if (GetNonDynamaxMaxHP(gBattlerAttacker) / 4 == 0) + u32 factor = gBattleMoves[gCurrentMove].effect == EFFECT_SHED_TAIL ? 2 : 4; + u32 hp = GetNonDynamaxMaxHP(gBattlerAttacker) / factor; + + if (GetNonDynamaxMaxHP(gBattlerAttacker) / factor == 0) hp = 1; if (gBattleMons[gBattlerAttacker].hp <= hp) @@ -12424,7 +12426,7 @@ static void Cmd_setsubstitute(void) } else { - gBattleMoveDamage = GetNonDynamaxMaxHP(gBattlerAttacker) / 4; // one bit value will only work for Pokémon which max hp can go to 1020(which is more than possible in games) + gBattleMoveDamage = GetNonDynamaxMaxHP(gBattlerAttacker) / factor; // one bit value will only work for Pokémon which max hp can go to 1020(which is more than possible in games) if (gBattleMoveDamage == 0) gBattleMoveDamage = 1; diff --git a/src/data/battle_moves.h b/src/data/battle_moves.h index d5a7d01c105f..a7d3c1eb16a3 100644 --- a/src/data/battle_moves.h +++ b/src/data/battle_moves.h @@ -12897,7 +12897,7 @@ const struct BattleMove gBattleMoves[MOVES_COUNT_DYNAMAX] = [MOVE_SHED_TAIL] = { - .effect = EFFECT_PLACEHOLDER, // EFFECT_SHED_TAIL + .effect = EFFECT_SHED_TAIL, .power = 0, .type = TYPE_NORMAL, .accuracy = 0, diff --git a/test/battle/move_effect/shed_tail.c b/test/battle/move_effect/shed_tail.c new file mode 100644 index 000000000000..258d52b3daab --- /dev/null +++ b/test/battle/move_effect/shed_tail.c @@ -0,0 +1,72 @@ +#include "global.h" +#include "test/battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_SHED_TAIL].effect == EFFECT_SHED_TAIL); +} + +SINGLE_BATTLE_TEST("Shed Tail creates a Substitute at the cost of 1/2 users maximum HP and switches the user out") +{ + s16 maxHP = 0; + s16 costHP = 0; + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SHED_TAIL); SEND_OUT(player, 1); } + } SCENE { + maxHP = GetMonData(&gPlayerParty[0], MON_DATA_HP); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SHED_TAIL, player); + HP_BAR(player, captureDamage: &costHP); + MESSAGE("Wobbuffet shed its tail to create a decoy!"); + MESSAGE("Go! Wynaut!"); + }THEN { + EXPECT_EQ(maxHP / 2, costHP); + } +} + +SINGLE_BATTLE_TEST("Shed Tail fails if the user doesn't have enough HP") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SHED_TAIL); } + } SCENE { + MESSAGE("It was too weak to make a SUBSTITUTE!"); + } +} + +SINGLE_BATTLE_TEST("Shed Tail's HP cost can trigger a berry before the user switches out") +{ + GIVEN { + ASSUME(gItems[ITEM_SITRUS_BERRY].battleUsage == EFFECT_ITEM_RESTORE_HP); + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_SITRUS_BERRY); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SHED_TAIL); SEND_OUT(player, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SHED_TAIL, player); + MESSAGE("Wobbuffet's Sitrus Berry restored health!"); + MESSAGE("Go! Wynaut!"); + } +} + +SINGLE_BATTLE_TEST("Shed Tail fails if there are no usable pokemon left") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) + PLAYER(SPECIES_WYNAUT) { HP(0); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SHED_TAIL); } + } SCENE { + MESSAGE("Wobbuffet used Shed Tail!"); + MESSAGE("But it failed!"); + } +} diff --git a/test/battle/move_effect/substitute.c b/test/battle/move_effect/substitute.c new file mode 100644 index 000000000000..ae2d4848b4df --- /dev/null +++ b/test/battle/move_effect/substitute.c @@ -0,0 +1,56 @@ +#include "global.h" +#include "test/battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_SUBSTITUTE].effect == EFFECT_SUBSTITUTE); +} + +SINGLE_BATTLE_TEST("Substitute creates a Substitute at the cost of 1/4 users maximum HP") +{ + s16 maxHP = 0; + s16 costHP = 0; + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SUBSTITUTE); } + } SCENE { + maxHP = GetMonData(&gPlayerParty[0], MON_DATA_HP); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SUBSTITUTE, player); + HP_BAR(player, captureDamage: &costHP); + MESSAGE("Wobbuffet made a SUBSTITUTE!"); + }THEN { + EXPECT_EQ(maxHP / 4, costHP); + } +} + +SINGLE_BATTLE_TEST("Substitute fails if the user doesn't have enough HP") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SUBSTITUTE); } + } SCENE { + MESSAGE("It was too weak to make a SUBSTITUTE!"); + } +} + +SINGLE_BATTLE_TEST("Substitute's HP cost can trigger a berry") +{ + GIVEN { + ASSUME(gItems[ITEM_SITRUS_BERRY].battleUsage == EFFECT_ITEM_RESTORE_HP); + PLAYER(SPECIES_WOBBUFFET) { HP(300); Item(ITEM_SITRUS_BERRY); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SUBSTITUTE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SUBSTITUTE, player); + MESSAGE("Wobbuffet's Sitrus Berry restored health!"); + } +}