[Move] Implement Safeguard (#3447)

* 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' <adrien.grivel@hotmail.fr>
Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br>
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 <asdargmng@gmail.com>
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 <keeganjosh@vuw.ac.nz>
Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr>
Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br>
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 <asdargmng@gmail.com>
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>
This commit is contained in:
NightKev 2024-09-01 21:26:20 -07:00 committed by GitHub
parent 2d5bd57c44
commit f54846f735
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 268 additions and 19 deletions

View File

@ -850,7 +850,7 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr {
} }
export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
private chance: integer; public chance: integer;
private effects: StatusEffect[]; private effects: StatusEffect[];
constructor(chance: integer, ...effects: StatusEffect[]) { constructor(chance: integer, ...effects: StatusEffect[]) {

View File

@ -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 { export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null {
switch (tagType) { switch (tagType) {
case ArenaTagType.MIST: case ArenaTagType.MIST:
@ -950,6 +965,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov
return new TailwindTag(turnCount, sourceId, side); return new TailwindTag(turnCount, sourceId, side);
case ArenaTagType.HAPPY_HOUR: case ArenaTagType.HAPPY_HOUR:
return new HappyHourTag(turnCount, sourceId, side); return new HappyHourTag(turnCount, sourceId, side);
case ArenaTagType.SAFEGUARD:
return new SafeguardTag(turnCount, sourceId, side);
default: default:
return null; return null;
} }

View File

@ -1953,6 +1953,13 @@ export class StatusEffectAttr extends MoveEffectAttr {
return false; 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)) if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0))
&& pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) { && pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) {
applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect);
@ -4659,6 +4666,17 @@ export class ConfuseAttr extends AddBattlerTagAttr {
constructor(selfTarget?: boolean) { constructor(selfTarget?: boolean) {
super(BattlerTagType.CONFUSED, selfTarget, false, 2, 5); 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 { export class RechargeAttr extends AddBattlerTagAttr {
@ -7014,7 +7032,7 @@ export function initMoves() {
.attr(FriendshipPowerAttr, true), .attr(FriendshipPowerAttr, true),
new StatusMove(Moves.SAFEGUARD, Type.NORMAL, -1, 25, -1, 0, 2) new StatusMove(Moves.SAFEGUARD, Type.NORMAL, -1, 25, -1, 0, 2)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.unimplemented(), .attr(AddArenaTagAttr, ArenaTagType.SAFEGUARD, 5, true, true),
new StatusMove(Moves.PAIN_SPLIT, Type.NORMAL, -1, 20, -1, 0, 2) new StatusMove(Moves.PAIN_SPLIT, Type.NORMAL, -1, 20, -1, 0, 2)
.attr(HpSplitAttr) .attr(HpSplitAttr)
.condition(failOnBossCondition), .condition(failOnBossCondition),
@ -7203,7 +7221,7 @@ export function initMoves() {
.attr(RemoveScreensAttr), .attr(RemoveScreensAttr),
new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3) new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3)
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) .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) 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(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferrable).length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false), .attr(RemoveHeldItemAttr, false),

View File

@ -22,5 +22,6 @@ export enum ArenaTagType {
CRAFTY_SHIELD = "CRAFTY_SHIELD", CRAFTY_SHIELD = "CRAFTY_SHIELD",
TAILWIND = "TAILWIND", TAILWIND = "TAILWIND",
HAPPY_HOUR = "HAPPY_HOUR", HAPPY_HOUR = "HAPPY_HOUR",
SAFEGUARD = "SAFEGUARD",
NO_CRIT = "NO_CRIT" NO_CRIT = "NO_CRIT"
} }

View File

@ -2787,6 +2787,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const types = this.getTypes(true, true); 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) { switch (effect) {
case StatusEffect.POISON: case StatusEffect.POISON:
case StatusEffect.TOXIC: case StatusEffect.TOXIC:

View File

@ -47,5 +47,11 @@
"tailwindOnRemovePlayer": "Der Rückenwind auf deiner Seite hat sich gelegt!", "tailwindOnRemovePlayer": "Der Rückenwind auf deiner Seite hat sich gelegt!",
"tailwindOnRemoveEnemy": "Der Rückenwind auf gegnerischer Seite hat sich gelegt!", "tailwindOnRemoveEnemy": "Der Rückenwind auf gegnerischer Seite hat sich gelegt!",
"happyHourOnAdd": "Goldene Zeiten sind angebrochen!", "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!"
} }

View File

@ -61,5 +61,6 @@
"suppressAbilities": "Die Fähigkeit von {{pokemonName}} wirkt nicht mehr!", "suppressAbilities": "Die Fähigkeit von {{pokemonName}} wirkt nicht mehr!",
"revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!", "revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!",
"swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!", "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!"
} }

View File

@ -1 +1,8 @@
{} {
"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."
}

View File

@ -7,5 +7,6 @@
"usedUpAllElectricity": "¡{{pokemonName}} ha descargado toda su electricidad!", "usedUpAllElectricity": "¡{{pokemonName}} ha descargado toda su electricidad!",
"stoleItem": "¡{{pokemonName}} robó el objeto\n{{itemName}} de {{targetName}}!", "stoleItem": "¡{{pokemonName}} robó el objeto\n{{itemName}} de {{targetName}}!",
"statEliminated": "¡Los cambios en estadísticas fueron eliminados!", "statEliminated": "¡Los cambios en estadísticas fueron eliminados!",
"revivalBlessing": "¡{{pokemonName}} ha revivido!" "revivalBlessing": "¡{{pokemonName}} ha revivido!",
"safeguard": "¡{{targetName}} está protegido por Velo Sagrado!"
} }

View File

@ -47,5 +47,11 @@
"tailwindOnRemovePlayer": "Le vent arrière soufflant\nsur votre équipe sarrête !", "tailwindOnRemovePlayer": "Le vent arrière soufflant\nsur votre équipe sarrête !",
"tailwindOnRemoveEnemy": "Le vent arrière soufflant\nsur léquipe ennemie sarrête !", "tailwindOnRemoveEnemy": "Le vent arrière soufflant\nsur léquipe ennemie sarrête !",
"happyHourOnAdd": "Lambiance est euphorique !", "happyHourOnAdd": "Lambiance est euphorique !",
"happyHourOnRemove": "Lambiance se calme !" "happyHourOnRemove": "Lambiance 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 nest plus protégé\npar le voile mystérieux !",
"safeguardOnRemovePlayer": "Votre équipe nest plus protégée\npar le voile mystérieux !",
"safeguardOnRemoveEnemy": "Léquipe ennemie nest plus protégée\npar le voile mystérieux !"
} }

View File

@ -61,5 +61,6 @@
"suppressAbilities": "Le talent de {{pokemonName}}\na été rendu inactif !", "suppressAbilities": "Le talent de {{pokemonName}}\na été rendu inactif !",
"revivalBlessing": "{{pokemonName}} a repris connaissance\net est prêt à se battre de nouveau !", "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}} !", "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 !"
} }

View File

@ -1 +1,8 @@
{} {
"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!"
}

View File

@ -61,5 +61,6 @@
"suppressAbilities": "Labilità di {{pokemonName}}\nperde ogni efficacia!", "suppressAbilities": "Labilità di {{pokemonName}}\nperde ogni efficacia!",
"revivalBlessing": "{{pokemonName}} torna in forze!", "revivalBlessing": "{{pokemonName}} torna in forze!",
"swapArenaTags": "{{pokemonName}} ha invertito gli effetti attivi\nnelle due metà del campo!", "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}}!"
} }

View File

@ -47,5 +47,11 @@
"tailwindOnRemovePlayer": "우리 편의\n순풍이 멈췄다!", "tailwindOnRemovePlayer": "우리 편의\n순풍이 멈췄다!",
"tailwindOnRemoveEnemy": "상대의\n순풍이 멈췄다!", "tailwindOnRemoveEnemy": "상대의\n순풍이 멈췄다!",
"happyHourOnAdd": "모두 행복한 기분에\n휩싸였다!", "happyHourOnAdd": "모두 행복한 기분에\n휩싸였다!",
"happyHourOnRemove": "기분이 원래대로 돌아왔다." "happyHourOnRemove": "기분이 원래대로 돌아왔다.",
"safeguardOnAdd": "필드 전체가 신비의 베일에 둘러싸였다!",
"safeguardOnAddPlayer": "우리 편은 신비의 베일에 둘러싸였다!",
"safeguardOnAddEnemy": "상대 편은 신비의 베일에 둘러싸였다!",
"safeguardOnRemove": "필드를 감싸던 신비의 베일이 없어졌다!",
"safeguardOnRemovePlayer": "우리 편을 감싸던 신비의 베일이 없어졌다!",
"safeguardOnRemoveEnemy": "상대 편을 감싸던 신비의 베일이 없어졌다!"
} }

View File

@ -61,5 +61,6 @@
"suppressAbilities": "{{pokemonName}}의\n특성이 효과를 발휘하지 못하게 되었다!", "suppressAbilities": "{{pokemonName}}의\n특성이 효과를 발휘하지 못하게 되었다!",
"revivalBlessing": "{{pokemonName}}[[는]]\n정신을 차려 싸울 수 있게 되었다!", "revivalBlessing": "{{pokemonName}}[[는]]\n정신을 차려 싸울 수 있게 되었다!",
"swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!", "swapArenaTags": "{{pokemonName}}[[는]]\n서로의 필드 효과를 교체했다!",
"exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!" "exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!",
"safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!"
} }

View File

@ -47,5 +47,11 @@
"tailwindOnRemovePlayer": "O Tailwind de sua equipe acabou!", "tailwindOnRemovePlayer": "O Tailwind de sua equipe acabou!",
"tailwindOnRemoveEnemy": "O Tailwind da equipe adversária acabou!", "tailwindOnRemoveEnemy": "O Tailwind da equipe adversária acabou!",
"happyHourOnAdd": "Todos foram envolvidos por uma atmosfera alegre!", "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!"
} }

View File

@ -61,5 +61,6 @@
"suppressAbilities": "A habilidade de {{pokemonName}}\nfoi suprimida!", "suppressAbilities": "A habilidade de {{pokemonName}}\nfoi suprimida!",
"revivalBlessing": "{{pokemonName}} foi reanimado!", "revivalBlessing": "{{pokemonName}} foi reanimado!",
"swapArenaTags": "{{pokemonName}} trocou os efeitos de batalha que afetam cada lado do campo!", "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!"
} }

View File

@ -47,5 +47,11 @@
"tailwindOnRemovePlayer": "我方的顺风停止了!", "tailwindOnRemovePlayer": "我方的顺风停止了!",
"tailwindOnRemoveEnemy": "敌方的顺风停止了!", "tailwindOnRemoveEnemy": "敌方的顺风停止了!",
"happyHourOnAdd": "大家被欢乐的\n气氛包围了", "happyHourOnAdd": "大家被欢乐的\n气氛包围了",
"happyHourOnRemove": "气氛回复到平常了。" "happyHourOnRemove": "气氛回复到平常了。",
"safeguardOnAdd": "整个场地被\n神秘之幕包围了",
"safeguardOnAddPlayer": "我方被\n神秘之幕包围了",
"safeguardOnAddEnemy": "对手被\n神秘之幕包围了",
"safeguardOnRemove": "包围整个场地的\n神秘之幕消失了",
"safeguardOnRemovePlayer": "包围我方的\n神秘之幕消失了",
"safeguardOnRemoveEnemy": "包围对手的\n神秘之幕消失了"
} }

View File

@ -61,5 +61,6 @@
"suppressAbilities": "{{pokemonName}}的特性\n变得无效了", "suppressAbilities": "{{pokemonName}}的特性\n变得无效了",
"revivalBlessing": "{{pokemonName}}复活了!", "revivalBlessing": "{{pokemonName}}复活了!",
"swapArenaTags": "{{pokemonName}}\n交换了双方的场地效果", "swapArenaTags": "{{pokemonName}}\n交换了双方的场地效果",
"exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!" "exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!",
"safeguard": "{{targetName}}\n正受到神秘之幕的保护"
} }

View File

@ -1,5 +1,11 @@
{ {
"noCritOnAddPlayer": "{{moveName}}保護了你的\n隊伍不被擊中要害", "noCritOnAddPlayer": "{{moveName}}保護了你的\n隊伍不被擊中要害",
"noCritOnAddEnemy": "{{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神秘之幕消失了"
} }

View File

@ -61,5 +61,6 @@
"suppressAbilities": "{{pokemonName}}的特性\n變得無效了", "suppressAbilities": "{{pokemonName}}的特性\n變得無效了",
"revivalBlessing": "{{pokemonName}}復活了!", "revivalBlessing": "{{pokemonName}}復活了!",
"swapArenaTags": "{{pokemonName}}\n交換了雙方的場地效果", "swapArenaTags": "{{pokemonName}}\n交換了雙方的場地效果",
"exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!" "exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!",
"safeguard": "{{targetName}}\n正受到神秘之幕的保護"
} }

View File

@ -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);
});