From f54846f735e2c22f195348cb3ae9f347d7c5cfe3 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 1 Sep 2024 21:26:20 -0700 Subject: [PATCH] [Move] Implement Safeguard (#3447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implemented safeguard and tests * Update tests * Add i18n placeholders * Implement Safeguard for non-volatile statuses * Implement protection from confusion and Yawn * Replace `target instanceof EnemyPokemon` with `target.isPlayer()` * Minor capitalization change * First batch of i18n Adds fr, pt_BR, zh_CN, zh_TW Co-authored-by: Lugiad' Co-authored-by: José Ricardo Fleury Oliveira Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> * Add more translations + de, es, ko Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Asdar Co-authored-by: sodam <66295123+sodaMelon@users.noreply.github.com> * Fix broken character in es translation * Update test with new function definition * Add Italian translation Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * Add move category check for message display Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Update phase imports in Safeguard test * Fix test imports * Update tests --------- Co-authored-by: Joshua Keegan Co-authored-by: Lugiad' Co-authored-by: José Ricardo Fleury Oliveira Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Asdar Co-authored-by: sodam <66295123+sodaMelon@users.noreply.github.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/data/ability.ts | 2 +- src/data/arena-tag.ts | 17 ++++ src/data/move.ts | 22 +++- src/enums/arena-tag-type.ts | 1 + src/field/pokemon.ts | 5 + src/locales/de/arena-tag.json | 8 +- src/locales/de/move-trigger.json | 3 +- src/locales/es/arena-tag.json | 9 +- src/locales/es/move-trigger.json | 3 +- src/locales/fr/arena-tag.json | 8 +- src/locales/fr/move-trigger.json | 3 +- src/locales/it/arena-tag.json | 9 +- src/locales/it/move-trigger.json | 3 +- src/locales/ko/arena-tag.json | 8 +- src/locales/ko/move-trigger.json | 3 +- src/locales/pt_BR/arena-tag.json | 8 +- src/locales/pt_BR/move-trigger.json | 3 +- src/locales/zh_CN/arena-tag.json | 8 +- src/locales/zh_CN/move-trigger.json | 3 +- src/locales/zh_TW/arena-tag.json | 8 +- src/locales/zh_TW/move-trigger.json | 3 +- src/test/moves/safeguard.test.ts | 150 ++++++++++++++++++++++++++++ 22 files changed, 268 insertions(+), 19 deletions(-) create mode 100644 src/test/moves/safeguard.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 818ab07637f..40312eaa8be 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -850,7 +850,7 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr { } export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { - private chance: integer; + public chance: integer; private effects: StatusEffect[]; constructor(chance: integer, ...effects: StatusEffect[]) { diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index a60ea5c2981..09cc7a5b97c 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -905,6 +905,21 @@ class HappyHourTag extends ArenaTag { } } +class SafeguardTag extends ArenaTag { + constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + super(ArenaTagType.SAFEGUARD, turnCount, Moves.SAFEGUARD, sourceId, side); + } + + onAdd(arena: Arena): void { + arena.scene.queueMessage(i18next.t(`arenaTag:safeguardOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`)); + } + + onRemove(arena: Arena): void { + arena.scene.queueMessage(i18next.t(`arenaTag:safeguardOnRemove${this.side === ArenaTagSide.PLAYER ? "Player" : this.side === ArenaTagSide.ENEMY ? "Enemy" : ""}`)); + } +} + + export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { switch (tagType) { case ArenaTagType.MIST: @@ -950,6 +965,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov return new TailwindTag(turnCount, sourceId, side); case ArenaTagType.HAPPY_HOUR: return new HappyHourTag(turnCount, sourceId, side); + case ArenaTagType.SAFEGUARD: + return new SafeguardTag(turnCount, sourceId, side); default: return null; } diff --git a/src/data/move.ts b/src/data/move.ts index 3f87fc68b89..1dc715f264a 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1953,6 +1953,13 @@ export class StatusEffectAttr extends MoveEffectAttr { return false; } } + + if (user !== target && target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)) { + if (move.category === MoveCategory.STATUS) { + user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target)})); + } + return false; + } if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) && pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) { applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); @@ -4659,6 +4666,17 @@ export class ConfuseAttr extends AddBattlerTagAttr { constructor(selfTarget?: boolean) { super(BattlerTagType.CONFUSED, selfTarget, false, 2, 5); } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!this.selfTarget && target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)) { + if (move.category === MoveCategory.STATUS) { + user.scene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target)})); + } + return false; + } + + return super.apply(user, target, move, args); + } } export class RechargeAttr extends AddBattlerTagAttr { @@ -7014,7 +7032,7 @@ export function initMoves() { .attr(FriendshipPowerAttr, true), new StatusMove(Moves.SAFEGUARD, Type.NORMAL, -1, 25, -1, 0, 2) .target(MoveTarget.USER_SIDE) - .unimplemented(), + .attr(AddArenaTagAttr, ArenaTagType.SAFEGUARD, 5, true, true), new StatusMove(Moves.PAIN_SPLIT, Type.NORMAL, -1, 20, -1, 0, 2) .attr(HpSplitAttr) .condition(failOnBossCondition), @@ -7203,7 +7221,7 @@ export function initMoves() { .attr(RemoveScreensAttr), new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) - .condition((user, target, move) => !target.status), + .condition((user, target, move) => !target.status && !target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)), new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferrable).length > 0 ? 1.5 : 1) .attr(RemoveHeldItemAttr, false), diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 1265b815bf4..1c79750c91a 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -22,5 +22,6 @@ export enum ArenaTagType { CRAFTY_SHIELD = "CRAFTY_SHIELD", TAILWIND = "TAILWIND", HAPPY_HOUR = "HAPPY_HOUR", + SAFEGUARD = "SAFEGUARD", NO_CRIT = "NO_CRIT" } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 16ad96f61cc..c970c99e7d3 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2787,6 +2787,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const types = this.getTypes(true, true); + const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + if (sourcePokemon && sourcePokemon !== this && this.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) { + return false; + } + switch (effect) { case StatusEffect.POISON: case StatusEffect.TOXIC: diff --git a/src/locales/de/arena-tag.json b/src/locales/de/arena-tag.json index 454effae60c..3bed4fefbd0 100644 --- a/src/locales/de/arena-tag.json +++ b/src/locales/de/arena-tag.json @@ -47,5 +47,11 @@ "tailwindOnRemovePlayer": "Der Rückenwind auf deiner Seite hat sich gelegt!", "tailwindOnRemoveEnemy": "Der Rückenwind auf gegnerischer Seite hat sich gelegt!", "happyHourOnAdd": "Goldene Zeiten sind angebrochen!", - "happyHourOnRemove": "Die goldenen Zeiten sind vorbei!" + "happyHourOnRemove": "Die goldenen Zeiten sind vorbei!", + "safeguardOnAdd": "Das ganze Feld wird von einem Schleier umhüllt!", + "safeguardOnAddPlayer": "Das Team des Anwenders wird von einem Schleier umhüllt!", + "safeguardOnAddEnemy": "Das gegnerische Team wird von einem Schleier umhüllt!", + "safeguardOnRemove": "Der mystische Schleier, der das ganze Feld umgab, hat sich gelüftet!", + "safeguardOnRemovePlayer": "Der mystische Schleier, der dein Team umgab, hat sich gelüftet!", + "safeguardOnRemoveEnemy": "Der mystische Schleier, der das gegnerische Team umgab, hat sich gelüftet!" } \ No newline at end of file diff --git a/src/locales/de/move-trigger.json b/src/locales/de/move-trigger.json index 5b2b2471df9..163e8014d8b 100644 --- a/src/locales/de/move-trigger.json +++ b/src/locales/de/move-trigger.json @@ -61,5 +61,6 @@ "suppressAbilities": "Die Fähigkeit von {{pokemonName}} wirkt nicht mehr!", "revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!", "swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!", - "exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!" + "exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!", + "safeguard": "{{targetName}} wird durch Bodyguard geschützt!" } \ No newline at end of file diff --git a/src/locales/es/arena-tag.json b/src/locales/es/arena-tag.json index 9e26dfeeb6e..913876ddf87 100644 --- a/src/locales/es/arena-tag.json +++ b/src/locales/es/arena-tag.json @@ -1 +1,8 @@ -{} \ No newline at end of file +{ + "safeguardOnAdd": "¡Todos los Pokémon están protegidos por Velo Sagrado!", + "safeguardOnAddPlayer": "¡Tu equipo se ha protegido con Velo Sagrado!", + "safeguardOnAddEnemy": "¡El equipo enemigo se ha protegido con Velo Sagrado!", + "safeguardOnRemove": "¡Velo Sagrado dejó de hacer efecto!", + "safeguardOnRemovePlayer": "El efecto de Velo Sagrado en tu equipo se ha disipado.", + "safeguardOnRemoveEnemy": "El efecto de Velo Sagrado en el equipo enemigo se ha disipado." +} \ No newline at end of file diff --git a/src/locales/es/move-trigger.json b/src/locales/es/move-trigger.json index b570f029377..52a6f86d930 100644 --- a/src/locales/es/move-trigger.json +++ b/src/locales/es/move-trigger.json @@ -7,5 +7,6 @@ "usedUpAllElectricity": "¡{{pokemonName}} ha descargado toda su electricidad!", "stoleItem": "¡{{pokemonName}} robó el objeto\n{{itemName}} de {{targetName}}!", "statEliminated": "¡Los cambios en estadísticas fueron eliminados!", - "revivalBlessing": "¡{{pokemonName}} ha revivido!" + "revivalBlessing": "¡{{pokemonName}} ha revivido!", + "safeguard": "¡{{targetName}} está protegido por Velo Sagrado!" } \ No newline at end of file diff --git a/src/locales/fr/arena-tag.json b/src/locales/fr/arena-tag.json index 16355816ae4..c3c705290fa 100644 --- a/src/locales/fr/arena-tag.json +++ b/src/locales/fr/arena-tag.json @@ -47,5 +47,11 @@ "tailwindOnRemovePlayer": "Le vent arrière soufflant\nsur votre équipe s’arrête !", "tailwindOnRemoveEnemy": "Le vent arrière soufflant\nsur l’équipe ennemie s’arrête !", "happyHourOnAdd": "L’ambiance est euphorique !", - "happyHourOnRemove": "L’ambiance se calme !" + "happyHourOnRemove": "L’ambiance se calme !", + "safeguardOnAdd": "Un voile mystérieux recouvre\ntout le terrain !", + "safeguardOnAddPlayer": "Un voile mystérieux recouvre\nvotre équipe !", + "safeguardOnAddEnemy": "Un voile mystérieux recouvre\nl’équipe ennemie !", + "safeguardOnRemove": "Le terrain n’est plus protégé\npar le voile mystérieux !", + "safeguardOnRemovePlayer": "Votre équipe n’est plus protégée\npar le voile mystérieux !", + "safeguardOnRemoveEnemy": "L’équipe ennemie n’est plus protégée\npar le voile mystérieux !" } \ No newline at end of file diff --git a/src/locales/fr/move-trigger.json b/src/locales/fr/move-trigger.json index 43cf09d5bf6..5c814745a8e 100644 --- a/src/locales/fr/move-trigger.json +++ b/src/locales/fr/move-trigger.json @@ -61,5 +61,6 @@ "suppressAbilities": "Le talent de {{pokemonName}}\na été rendu inactif !", "revivalBlessing": "{{pokemonName}} a repris connaissance\net est prêt à se battre de nouveau !", "swapArenaTags": "Les effets affectant chaque côté du terrain\nont été échangés par {{pokemonName}} !", - "exposedMove": "{{targetPokemonName}} est identifié\npar {{pokemonName}} !" + "exposedMove": "{{targetPokemonName}} est identifié\npar {{pokemonName}} !", + "safeguard": "{{targetName}} est protégé\npar la capacité Rune Protect !" } \ No newline at end of file diff --git a/src/locales/it/arena-tag.json b/src/locales/it/arena-tag.json index 9e26dfeeb6e..a1c5ee5b3c9 100644 --- a/src/locales/it/arena-tag.json +++ b/src/locales/it/arena-tag.json @@ -1 +1,8 @@ -{} \ No newline at end of file +{ + "safeguardOnAdd": "Un velo mistico ricopre il campo!", + "safeguardOnAddPlayer": "Un velo mistico ricopre la tua squadra!", + "safeguardOnAddEnemy": "Un velo mistico ricopre la squadra avversaria!", + "safeguardOnRemove": "Il campo non è più protetto da Salvaguardia!", + "safeguardOnRemovePlayer": "La tua squadra non è più protetta da Salvaguardia!", + "safeguardOnRemoveEnemy": "La squadra avversaria non è più protetta da Salvaguardia!" +} \ No newline at end of file diff --git a/src/locales/it/move-trigger.json b/src/locales/it/move-trigger.json index e852c2fb52a..58b7b1a4c5b 100644 --- a/src/locales/it/move-trigger.json +++ b/src/locales/it/move-trigger.json @@ -61,5 +61,6 @@ "suppressAbilities": "L’abilità di {{pokemonName}}\nperde ogni efficacia!", "revivalBlessing": "{{pokemonName}} torna in forze!", "swapArenaTags": "{{pokemonName}} ha invertito gli effetti attivi\nnelle due metà del campo!", - "exposedMove": "{{pokemonName}} ha identificato\n{{targetPokemonName}}!" + "exposedMove": "{{pokemonName}} ha identificato\n{{targetPokemonName}}!", + "safeguard": "Salvaguardia protegge {{targetName}}!" } \ No newline at end of file diff --git a/src/locales/ko/arena-tag.json b/src/locales/ko/arena-tag.json index 61586508a94..ce9922ab3bf 100644 --- a/src/locales/ko/arena-tag.json +++ b/src/locales/ko/arena-tag.json @@ -47,5 +47,11 @@ "tailwindOnRemovePlayer": "우리 편의\n순풍이 멈췄다!", "tailwindOnRemoveEnemy": "상대의\n순풍이 멈췄다!", "happyHourOnAdd": "모두 행복한 기분에\n휩싸였다!", - "happyHourOnRemove": "기분이 원래대로 돌아왔다." + "happyHourOnRemove": "기분이 원래대로 돌아왔다.", + "safeguardOnAdd": "필드 전체가 신비의 베일에 둘러싸였다!", + "safeguardOnAddPlayer": "우리 편은 신비의 베일에 둘러싸였다!", + "safeguardOnAddEnemy": "상대 편은 신비의 베일에 둘러싸였다!", + "safeguardOnRemove": "필드를 감싸던 신비의 베일이 없어졌다!", + "safeguardOnRemovePlayer": "우리 편을 감싸던 신비의 베일이 없어졌다!", + "safeguardOnRemoveEnemy": "상대 편을 감싸던 신비의 베일이 없어졌다!" } \ No newline at end of file diff --git a/src/locales/ko/move-trigger.json b/src/locales/ko/move-trigger.json index 61dffa122a3..f0e0fbd6a56 100644 --- a/src/locales/ko/move-trigger.json +++ b/src/locales/ko/move-trigger.json @@ -61,5 +61,6 @@ "suppressAbilities": "{{pokemonName}}의\n특성이 효과를 발휘하지 못하게 되었다!", "revivalBlessing": "{{pokemonName}}[[는]]\n정신을 차려 싸울 수 있게 되었다!", "swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!", - "exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!" + "exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!", + "safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!" } \ No newline at end of file diff --git a/src/locales/pt_BR/arena-tag.json b/src/locales/pt_BR/arena-tag.json index 20ef208c8fc..7ab1ecea721 100644 --- a/src/locales/pt_BR/arena-tag.json +++ b/src/locales/pt_BR/arena-tag.json @@ -47,5 +47,11 @@ "tailwindOnRemovePlayer": "O Tailwind de sua equipe acabou!", "tailwindOnRemoveEnemy": "O Tailwind da equipe adversária acabou!", "happyHourOnAdd": "Todos foram envolvidos por uma atmosfera alegre!", - "happyHourOnRemove": "A atmosfera retornou ao normal." + "happyHourOnRemove": "A atmosfera retornou ao normal.", + "safeguardOnAdd": "O campo de batalha está envolto num véu místico!", + "safeguardOnAddPlayer": "Sua equipe se envolveu num véu místico!", + "safeguardOnAddEnemy": "A equipe adversária se envolveu num véu místico!", + "safeguardOnRemove": "O campo não está mais protegido por Safeguard!", + "safeguardOnRemovePlayer": "Sua equipe não está mais protegido por Safeguard!", + "safeguardOnRemoveEnemy": "A equipe adversária não está mais protegido por Safeguard!" } \ No newline at end of file diff --git a/src/locales/pt_BR/move-trigger.json b/src/locales/pt_BR/move-trigger.json index 416740dba0d..ea320412a24 100644 --- a/src/locales/pt_BR/move-trigger.json +++ b/src/locales/pt_BR/move-trigger.json @@ -61,5 +61,6 @@ "suppressAbilities": "A habilidade de {{pokemonName}}\nfoi suprimida!", "revivalBlessing": "{{pokemonName}} foi reanimado!", "swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!", - "exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!" + "exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!", + "safeguard": "{{targetName}} está protegido por Safeguard!" } \ No newline at end of file diff --git a/src/locales/zh_CN/arena-tag.json b/src/locales/zh_CN/arena-tag.json index 5a36b3ae1f7..74ad38ba9bf 100644 --- a/src/locales/zh_CN/arena-tag.json +++ b/src/locales/zh_CN/arena-tag.json @@ -47,5 +47,11 @@ "tailwindOnRemovePlayer": "我方的顺风停止了!", "tailwindOnRemoveEnemy": "敌方的顺风停止了!", "happyHourOnAdd": "大家被欢乐的\n气氛包围了!", - "happyHourOnRemove": "气氛回复到平常了。" + "happyHourOnRemove": "气氛回复到平常了。", + "safeguardOnAdd": "整个场地被\n神秘之幕包围了!", + "safeguardOnAddPlayer": "我方被\n神秘之幕包围了!", + "safeguardOnAddEnemy": "对手被\n神秘之幕包围了!", + "safeguardOnRemove": "包围整个场地的\n神秘之幕消失了!", + "safeguardOnRemovePlayer": "包围我方的\n神秘之幕消失了!", + "safeguardOnRemoveEnemy": "包围对手的\n神秘之幕消失了!" } \ No newline at end of file diff --git a/src/locales/zh_CN/move-trigger.json b/src/locales/zh_CN/move-trigger.json index 5a76f402783..44705d54e76 100644 --- a/src/locales/zh_CN/move-trigger.json +++ b/src/locales/zh_CN/move-trigger.json @@ -61,5 +61,6 @@ "suppressAbilities": "{{pokemonName}}的特性\n变得无效了!", "revivalBlessing": "{{pokemonName}}复活了!", "swapArenaTags": "{{pokemonName}}\n交换了双方的场地效果!", - "exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!" + "exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!", + "safeguard": "{{targetName}}\n正受到神秘之幕的保护!" } \ No newline at end of file diff --git a/src/locales/zh_TW/arena-tag.json b/src/locales/zh_TW/arena-tag.json index b60946a3b77..78246d9c44f 100644 --- a/src/locales/zh_TW/arena-tag.json +++ b/src/locales/zh_TW/arena-tag.json @@ -1,5 +1,11 @@ { "noCritOnAddPlayer": "{{moveName}}保護了你的\n隊伍不被擊中要害!", "noCritOnAddEnemy": "{{moveName}}保護了對方的\n隊伍不被擊中要害!", - "noCritOnRemove": "{{pokemonNameWithAffix}}的{{moveName}}\n效果消失了!" + "noCritOnRemove": "{{pokemonNameWithAffix}}的{{moveName}}\n效果消失了!", + "safeguardOnAdd": "整個場地被\n神秘之幕包圍了!", + "safeguardOnAddPlayer": "我方被\n神秘之幕包圍了!", + "safeguardOnAddEnemy": "對手被\n神秘之幕包圍了!", + "safeguardOnRemove": "包圍整個場地的\n神秘之幕消失了!", + "safeguardOnRemovePlayer": "包圍我方的\n神秘之幕消失了!", + "safeguardOnRemoveEnemy": "包圍對手的\n神秘之幕消失了!" } \ No newline at end of file diff --git a/src/locales/zh_TW/move-trigger.json b/src/locales/zh_TW/move-trigger.json index 03ca6841a7f..60dcc1eab7a 100644 --- a/src/locales/zh_TW/move-trigger.json +++ b/src/locales/zh_TW/move-trigger.json @@ -61,5 +61,6 @@ "suppressAbilities": "{{pokemonName}}的特性\n變得無效了!", "revivalBlessing": "{{pokemonName}}復活了!", "swapArenaTags": "{{pokemonName}}\n交換了雙方的場地效果!", - "exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!" + "exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!", + "safeguard": "{{targetName}}\n正受到神秘之幕的保護!" } \ No newline at end of file diff --git a/src/test/moves/safeguard.test.ts b/src/test/moves/safeguard.test.ts new file mode 100644 index 00000000000..94a7aa6031e --- /dev/null +++ b/src/test/moves/safeguard.test.ts @@ -0,0 +1,150 @@ +import { BattlerIndex } from "#app/battle"; +import { allAbilities, PostDefendContactApplyStatusEffectAbAttr } from "#app/data/ability"; +import { Abilities } from "#app/enums/abilities"; +import { StatusEffect } from "#app/enums/status-effect"; +import GameManager from "#app/test/utils/gameManager"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Safeguard", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemySpecies(Species.DRATINI) + .enemyMoveset(Array(4).fill(Moves.SAFEGUARD)) + .enemyAbility(Abilities.BALL_FETCH) + .enemyLevel(5) + .starterSpecies(Species.DRATINI) + .moveset([Moves.NUZZLE, Moves.SPORE, Moves.YAWN, Moves.SPLASH]) + .ability(Abilities.BALL_FETCH); + }); + + it("protects from damaging moves with additional effects", async () => { + await game.startBattle(); + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.NUZZLE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(enemy.status).toBeUndefined(); + }, TIMEOUT); + + it("protects from status moves", async () => { + await game.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SPORE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(enemyPokemon.status).toBeUndefined(); + }, TIMEOUT); + + it("protects from confusion", async () => { + game.override.moveset([Moves.CONFUSE_RAY]); + await game.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.CONFUSE_RAY); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(enemyPokemon.summonData.tags).toEqual([]); + }, TIMEOUT); + + it("protects ally from status", async () => { + game.override.battleType("double"); + + await game.startBattle(); + + game.move.select(Moves.SPORE, 0, BattlerIndex.ENEMY_2); + game.move.select(Moves.NUZZLE, 1, BattlerIndex.ENEMY_2); + + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2]); + + await game.phaseInterceptor.to("BerryPhase"); + + const enemyPokemon = game.scene.getEnemyField(); + + expect(enemyPokemon[0].status).toBeUndefined(); + expect(enemyPokemon[1].status).toBeUndefined(); + }, TIMEOUT); + + it("protects from Yawn", async () => { + await game.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.YAWN); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(enemyPokemon.summonData.tags).toEqual([]); + }, TIMEOUT); + + it("doesn't protect from already existing Yawn", async () => { + await game.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.YAWN); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(enemyPokemon.status?.effect).toEqual(StatusEffect.SLEEP); + }, TIMEOUT); + + it("doesn't protect from self-inflicted via Rest or Flame Orb", async () => { + game.override.enemyHeldItems([{name: "FLAME_ORB"}]); + await game.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SPLASH); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + + expect(enemyPokemon.status?.effect).toEqual(StatusEffect.BURN); + + game.override.enemyMoveset(Array(4).fill(Moves.REST)); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(enemyPokemon.status?.effect).toEqual(StatusEffect.SLEEP); + }, TIMEOUT); + + it("protects from ability-inflicted status", async () => { + game.override.ability(Abilities.STATIC); + vi.spyOn(allAbilities[Abilities.STATIC].getAttrs(PostDefendContactApplyStatusEffectAbAttr)[0], "chance", "get").mockReturnValue(100); + await game.startBattle(); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SPLASH); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(enemyPokemon.status).toBeUndefined(); + }, TIMEOUT); +});