diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index fd72ab21026..9c846d2f3cb 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -303,6 +303,39 @@ class CraftyShieldTag extends ConditionalProtectTag { } } +/** + * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Lucky_Chant_(move) Lucky Chant}. + * Prevents critical hits against the tag's side. + */ +export class NoCritTag extends ArenaTag { + /** + * Constructor method for the NoCritTag class + * @param turnCount `integer` the number of turns this effect lasts + * @param sourceMove {@linkcode Moves} the move that created this effect + * @param sourceId `integer` the ID of the {@linkcode Pokemon} that created this effect + * @param side {@linkcode ArenaTagSide} the side to which this effect belongs + */ + constructor(turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide) { + super(ArenaTagType.NO_CRIT, turnCount, sourceMove, sourceId, side); + } + + /** Queues a message upon adding this effect to the field */ + onAdd(arena: Arena): void { + arena.scene.queueMessage(i18next.t(`arenaTag:noCritOnAdd${this.side === ArenaTagSide.PLAYER ? "Player" : "Enemy"}`, { + moveName: this.getMoveName() + })); + } + + /** Queues a message upon removing this effect from the field */ + onRemove(arena: Arena): void { + const source = arena.scene.getPokemonById(this.sourceId); + arena.scene.queueMessage(i18next.t("arenaTag:noCritOnRemove", { + pokemonNameWithAffix: getPokemonNameWithAffix(source), + moveName: this.getMoveName() + })); + } +} + /** * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) Wish}. * Heals the Pokémon in the user's position the turn after Wish is used. @@ -803,6 +836,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov return new MatBlockTag(sourceId, side); case ArenaTagType.CRAFTY_SHIELD: return new CraftyShieldTag(sourceId, side); + case ArenaTagType.NO_CRIT: + return new NoCritTag(turnCount, sourceMove, sourceId, side); case ArenaTagType.MUD_SPORT: return new MudSportTag(turnCount, sourceId); case ArenaTagType.WATER_SPORT: diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index d2f462d5a44..4fad0ddbd08 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1774,8 +1774,6 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.ALWAYS_CRIT: case BattlerTagType.IGNORE_ACCURACY: return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, 2, sourceMove); - case BattlerTagType.NO_CRIT: - return new BattlerTag(tagType, BattlerTagLapseType.AFTER_MOVE, turnCount, sourceMove); case BattlerTagType.ALWAYS_GET_HIT: case BattlerTagType.RECEIVE_DOUBLE_DAMAGE: return new BattlerTag(tagType, BattlerTagLapseType.PRE_MOVE, 1, sourceMove); diff --git a/src/data/move.ts b/src/data/move.ts index d93869dedb9..dbb29d4c9d1 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4258,7 +4258,6 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.INFATUATED: case BattlerTagType.NIGHTMARE: case BattlerTagType.DROWSY: - case BattlerTagType.NO_CRIT: return -5; case BattlerTagType.SEEDED: case BattlerTagType.SALT_CURED: @@ -7120,9 +7119,8 @@ export function initMoves() { new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4) .attr(SuppressAbilitiesAttr), new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4) - .attr(AddBattlerTagAttr, BattlerTagType.NO_CRIT, false, false, 5) - .target(MoveTarget.USER_SIDE) - .unimplemented(), + .attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true) + .target(MoveTarget.USER_SIDE), new StatusMove(Moves.ME_FIRST, Type.NORMAL, -1, 20, -1, 0, 4) .ignoresVirtual() .target(MoveTarget.NEAR_ENEMY) diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 722096c42cd..1265b815bf4 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -21,5 +21,6 @@ export enum ArenaTagType { MAT_BLOCK = "MAT_BLOCK", CRAFTY_SHIELD = "CRAFTY_SHIELD", TAILWIND = "TAILWIND", - HAPPY_HOUR = "HAPPY_HOUR" + HAPPY_HOUR = "HAPPY_HOUR", + NO_CRIT = "NO_CRIT" } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 7ef73bef281..eeba56d6532 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -48,7 +48,6 @@ export enum BattlerTagType { FIRE_BOOST = "FIRE_BOOST", CRIT_BOOST = "CRIT_BOOST", ALWAYS_CRIT = "ALWAYS_CRIT", - NO_CRIT = "NO_CRIT", IGNORE_ACCURACY = "IGNORE_ACCURACY", BYPASS_SLEEP = "BYPASS_SLEEP", IGNORE_FLYING = "IGNORE_FLYING", diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 7fc5f14d0e1..ea7acc12588 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -22,7 +22,7 @@ import { BattleStat } from "../data/battle-stat"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; -import { ArenaTagSide, WeakenMoveScreenTag } from "../data/arena-tag"; +import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; @@ -1885,6 +1885,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { apply(source: Pokemon, move: Move): HitResult { let result: HitResult; const damage = new Utils.NumberHolder(0); + const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const defendingSidePlayField = this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField(); const variableCategory = new Utils.IntegerHolder(move.category); @@ -1911,7 +1912,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Apply arena tags for conditional protection if (!move.checkFlag(MoveFlags.IGNORE_PROTECT, source, this) && !move.isAllyTarget()) { - const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority); this.scene.arena.applyTagsForSide(ArenaTagType.WIDE_GUARD, defendingSide, cancelled, this, move.moveTarget); this.scene.arena.applyTagsForSide(ArenaTagType.MAT_BLOCK, defendingSide, cancelled, this, move.category); @@ -1978,15 +1978,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } console.log(`crit stage: +${critLevel.value}`); const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))]; - isCritical = !source.getTag(BattlerTagType.NO_CRIT) && (critChance === 1 || !this.scene.randBattleSeedInt(critChance)); + isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); if (Overrides.NEVER_CRIT_OVERRIDE) { isCritical = false; } } if (isCritical) { + const noCritTag = this.scene.arena.getTagOnSide(NoCritTag, defendingSide); const blockCrit = new Utils.BooleanHolder(false); applyAbAttrs(BlockCritAbAttr, this, null, blockCrit); - if (blockCrit.value) { + if (noCritTag || blockCrit.value) { isCritical = false; } } @@ -1996,7 +1997,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyAbAttrs(MultCritAbAttr, source, null, criticalMultiplier); const screenMultiplier = new Utils.NumberHolder(1); if (!isCritical) { - this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, move.category, this.scene.currentBattle.double, screenMultiplier); + this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); } const isTypeImmune = (typeMultiplier.value * arenaAttackTypeMultiplier.value) === 0; const sourceTypes = source.getTypes(); diff --git a/src/locales/de/arena-tag.ts b/src/locales/de/arena-tag.ts index cc0a821aade..65699742331 100644 --- a/src/locales/de/arena-tag.ts +++ b/src/locales/de/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "Die Pokémon auf der gegnerischen Seite werden von {{moveName}} behütet!", "conditionalProtectApply": "{{pokemonNameWithAffix}} wird durch {{moveName}} geschützt!", "matBlockOnAdd": "{{pokemonNameWithAffix}} bringt seinen Tatami-Schild in Position!", + "noCritOnAddPlayer": "{{moveName}} schützt dein Team vor Volltreffern!", + "noCritOnAddEnemy": "{{moveName}} schützt das gegnerische Team vor Volltreffern!", + "noCritOnRemove": "{{moveName}} von {{pokemonNameWithAffix}} hört auf zu wirken!", "wishTagOnAdd": "Der Wunschtraum von {{pokemonNameWithAffix}} erfüllt sich!", "mudSportOnAdd": "Die Stärke aller Elektro-Attacken wurde reduziert!", "mudSportOnRemove": "Lehmsuhler hört auf zu wirken!", diff --git a/src/locales/en/arena-tag.ts b/src/locales/en/arena-tag.ts index 8bc2302368a..22612795308 100644 --- a/src/locales/en/arena-tag.ts +++ b/src/locales/en/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "{{moveName}} protected the\nopposing team!", "conditionalProtectApply": "{{moveName}} protected {{pokemonNameWithAffix}}!", "matBlockOnAdd": "{{pokemonNameWithAffix}} intends to flip up a mat\nand block incoming attacks!", + "noCritOnAddPlayer": "The {{moveName}} shielded your\nteam from critical hits!", + "noCritOnAddEnemy": "The {{moveName}} shielded the opposing\nteam from critical hits!", + "noCritOnRemove": "{{pokemonNameWithAffix}}'s {{moveName}}\nwore off!", "wishTagOnAdd": "{{pokemonNameWithAffix}}'s wish\ncame true!", "mudSportOnAdd": "Electricity's power was weakened!", "mudSportOnRemove": "The effects of Mud Sport\nhave faded.", diff --git a/src/locales/es/arena-tag.ts b/src/locales/es/arena-tag.ts index 8bc2302368a..22612795308 100644 --- a/src/locales/es/arena-tag.ts +++ b/src/locales/es/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "{{moveName}} protected the\nopposing team!", "conditionalProtectApply": "{{moveName}} protected {{pokemonNameWithAffix}}!", "matBlockOnAdd": "{{pokemonNameWithAffix}} intends to flip up a mat\nand block incoming attacks!", + "noCritOnAddPlayer": "The {{moveName}} shielded your\nteam from critical hits!", + "noCritOnAddEnemy": "The {{moveName}} shielded the opposing\nteam from critical hits!", + "noCritOnRemove": "{{pokemonNameWithAffix}}'s {{moveName}}\nwore off!", "wishTagOnAdd": "{{pokemonNameWithAffix}}'s wish\ncame true!", "mudSportOnAdd": "Electricity's power was weakened!", "mudSportOnRemove": "The effects of Mud Sport\nhave faded.", diff --git a/src/locales/fr/arena-tag.ts b/src/locales/fr/arena-tag.ts index cc97cb4e34f..62ef203cf68 100644 --- a/src/locales/fr/arena-tag.ts +++ b/src/locales/fr/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "La capacité {{moveName}}\nprotège l’équipe ennemie !", "conditionalProtectApply": "{{pokemonNameWithAffix}} est protégé\npar {{moveName}} !", "matBlockOnAdd": "{{pokemonNameWithAffix}} se prépare\nà utiliser un tatami pour bloquer les attaques !", + "noCritOnAddPlayer": "{{moveName}} immunise votre équipe\ncontre les coups critiques !", + "noCritOnAddEnemy": "{{moveName}} immunise l’équipe ennemie\ncontre les coups critiques !", + "noCritOnRemove": "Les effets d’{{moveName}}\nsur {{pokemonNameWithAffix}} prennent fin !", "wishTagOnAdd": "Le vœu de{{pokemonNameWithAffix}}\nse réalise !", "mudSportOnAdd": "La puissance des capacités\nde type Électrik diminue !", "mudSportOnRemove": "L’effet de Lance-Boue se dissipe !", diff --git a/src/locales/it/arena-tag.ts b/src/locales/it/arena-tag.ts index 8bc2302368a..22612795308 100644 --- a/src/locales/it/arena-tag.ts +++ b/src/locales/it/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "{{moveName}} protected the\nopposing team!", "conditionalProtectApply": "{{moveName}} protected {{pokemonNameWithAffix}}!", "matBlockOnAdd": "{{pokemonNameWithAffix}} intends to flip up a mat\nand block incoming attacks!", + "noCritOnAddPlayer": "The {{moveName}} shielded your\nteam from critical hits!", + "noCritOnAddEnemy": "The {{moveName}} shielded the opposing\nteam from critical hits!", + "noCritOnRemove": "{{pokemonNameWithAffix}}'s {{moveName}}\nwore off!", "wishTagOnAdd": "{{pokemonNameWithAffix}}'s wish\ncame true!", "mudSportOnAdd": "Electricity's power was weakened!", "mudSportOnRemove": "The effects of Mud Sport\nhave faded.", diff --git a/src/locales/ja/arena-tag.ts b/src/locales/ja/arena-tag.ts index 8bc2302368a..22612795308 100644 --- a/src/locales/ja/arena-tag.ts +++ b/src/locales/ja/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "{{moveName}} protected the\nopposing team!", "conditionalProtectApply": "{{moveName}} protected {{pokemonNameWithAffix}}!", "matBlockOnAdd": "{{pokemonNameWithAffix}} intends to flip up a mat\nand block incoming attacks!", + "noCritOnAddPlayer": "The {{moveName}} shielded your\nteam from critical hits!", + "noCritOnAddEnemy": "The {{moveName}} shielded the opposing\nteam from critical hits!", + "noCritOnRemove": "{{pokemonNameWithAffix}}'s {{moveName}}\nwore off!", "wishTagOnAdd": "{{pokemonNameWithAffix}}'s wish\ncame true!", "mudSportOnAdd": "Electricity's power was weakened!", "mudSportOnRemove": "The effects of Mud Sport\nhave faded.", diff --git a/src/locales/ko/arena-tag.ts b/src/locales/ko/arena-tag.ts index ca1039e2bc0..2211ced6c4c 100644 --- a/src/locales/ko/arena-tag.ts +++ b/src/locales/ko/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "상대 주변을\n{{moveName}}[[가]] 보호하고 있다!", "conditionalProtectApply": "{{pokemonNameWithAffix}}[[를]]\n{{moveName}}[[가]] 지켜주고 있다!", "matBlockOnAdd": "{{pokemonNameWithAffix}}[[는]]\n마룻바닥세워막기를 노리고 있다!", + "noCritOnAddPlayer": "{{moveName}}의 힘으로\n우리 편의 급소가 숨겨졌다!", + "noCritOnAddEnemy": "{{moveName}}의 힘으로\n상대의 급소가 숨겨졌다!", + "noCritOnRemove": "{{pokemonNameWithAffix}}의 {{moveName}}[[가]] 풀렸다!", "wishTagOnAdd": "{{pokemonNameWithAffix}}의\n희망사항이 이루어졌다!", "mudSportOnAdd": "전기의 위력이 약해졌다!", "mudSportOnRemove": "흙놀이의 효과가\n없어졌다!", diff --git a/src/locales/pt_BR/arena-tag.ts b/src/locales/pt_BR/arena-tag.ts index 0d3b8ff587f..ebdf886f9a6 100644 --- a/src/locales/pt_BR/arena-tag.ts +++ b/src/locales/pt_BR/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "{{moveName}} protegeu a\nequipe adversária!", "conditionalProtectApply": "{{moveName}} protegeu {{pokemonNameWithAffix}}!", "matBlockOnAdd": "{{pokemonNameWithAffix}} pretende levantar um tapete\npara bloquear ataques!", + "noCritOnAddPlayer": "{{moveName}} protegeu sua\equipe de acertos críticos!", + "noCritOnAddEnemy": "{{moveName}} protegeu a\equipe adversária de acertos críticos", + "noCritOnRemove": "{{moveName}} de {{pokemonNameWithAffix}}\nacabou!", "wishTagOnAdd": "O desejo de {{pokemonNameWithAffix}}\nfoi concedido!", "mudSportOnAdd": "O poder de movimentos elétricos foi enfraquecido!", "mudSportOnRemove": "Os efeitos de Mud Sport\nsumiram.", diff --git a/src/locales/zh_CN/arena-tag.ts b/src/locales/zh_CN/arena-tag.ts index 027a5667415..974ef36d7af 100644 --- a/src/locales/zh_CN/arena-tag.ts +++ b/src/locales/zh_CN/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "{{moveName}}\n保护了敌方!", "conditionalProtectApply": "{{moveName}}\n保护了{{pokemonNameWithAffix}}!", "matBlockOnAdd": "{{pokemonNameWithAffix}}正在\n伺机使出掀榻榻米!", + "noCritOnAddPlayer": "{{moveName}}保护了你的\n队伍不被击中要害!", + "noCritOnAddEnemy": "{{moveName}}保护了对方的\n队伍不被击中要害!", + "noCritOnRemove": "{{pokemonNameWithAffix}}的{{moveName}}\n效果消失了!", "wishTagOnAdd": "{{pokemonNameWithAffix}}的\n祈愿实现了!", "mudSportOnAdd": "电气的威力减弱了!", "mudSportOnRemove": "玩泥巴的效果消失了!", diff --git a/src/locales/zh_TW/arena-tag.ts b/src/locales/zh_TW/arena-tag.ts index 8bc2302368a..ee7d2eb7bc5 100644 --- a/src/locales/zh_TW/arena-tag.ts +++ b/src/locales/zh_TW/arena-tag.ts @@ -22,6 +22,9 @@ export const arenaTag: SimpleTranslationEntries = { "conditionalProtectOnAddEnemy": "{{moveName}} protected the\nopposing team!", "conditionalProtectApply": "{{moveName}} protected {{pokemonNameWithAffix}}!", "matBlockOnAdd": "{{pokemonNameWithAffix}} intends to flip up a mat\nand block incoming attacks!", + "noCritOnAddPlayer": "{{moveName}}保護了你的\n隊伍不被擊中要害!", + "noCritOnAddEnemy": "{{moveName}}保護了對方的\n隊伍不被擊中要害!", + "noCritOnRemove": "{{pokemonNameWithAffix}}的{{moveName}}\n效果消失了!", "wishTagOnAdd": "{{pokemonNameWithAffix}}'s wish\ncame true!", "mudSportOnAdd": "Electricity's power was weakened!", "mudSportOnRemove": "The effects of Mud Sport\nhave faded.", diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index 2facff3428f..3c6e2b83baa 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -35,7 +35,6 @@ describe("Moves - Dragon Rage", () => { game = new GameManager(phaserGame); game.override.battleType("single"); - game.override.disableCrits(); game.override.starterSpecies(Species.SNORLAX); game.override.moveset([Moves.DRAGON_RAGE]); @@ -60,6 +59,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores weaknesses", async () => { + game.override.disableCrits(); vi.spyOn(enemyPokemon, "getTypes").mockReturnValue([Type.DRAGON]); game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE)); @@ -70,6 +70,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores resistances", async () => { + game.override.disableCrits(); vi.spyOn(enemyPokemon, "getTypes").mockReturnValue([Type.STEEL]); game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE)); @@ -80,6 +81,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores stat changes", async () => { + game.override.disableCrits(); partyPokemon.summonData.battleStats[BattleStat.SPATK] = 2; game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE)); @@ -90,6 +92,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores stab", async () => { + game.override.disableCrits(); vi.spyOn(partyPokemon, "getTypes").mockReturnValue([Type.DRAGON]); game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE)); @@ -100,7 +103,6 @@ describe("Moves - Dragon Rage", () => { }); it("ignores criticals", async () => { - partyPokemon.removeTag(BattlerTagType.NO_CRIT); partyPokemon.addTag(BattlerTagType.ALWAYS_CRIT, 99); game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE)); @@ -111,6 +113,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores damage modification from abilities such as ice scales", async () => { + game.override.disableCrits(); game.override.enemyAbility(Abilities.ICE_SCALES); game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE)); @@ -121,6 +124,7 @@ describe("Moves - Dragon Rage", () => { }); it("ignores multi hit", async () => { + game.override.disableCrits(); game.scene.addModifier(modifierTypes.MULTI_LENS().newModifier(partyPokemon), false); game.doAttack(getMovePosition(game.scene, 0, Moves.DRAGON_RAGE)); diff --git a/src/test/moves/lucky_chant.test.ts b/src/test/moves/lucky_chant.test.ts new file mode 100644 index 00000000000..1a29edb8052 --- /dev/null +++ b/src/test/moves/lucky_chant.test.ts @@ -0,0 +1,113 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import GameManager from "../utils/gameManager"; +import { getMovePosition } from "../utils/gameManagerUtils"; +import { Moves } from "#app/enums/moves.js"; +import { Species } from "#app/enums/species.js"; +import { Abilities } from "#app/enums/abilities.js"; +import { BerryPhase, TurnEndPhase } from "#app/phases.js"; +import { BattlerTagType } from "#app/enums/battler-tag-type.js"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Lucky Chant", () => { + 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") + .moveset([Moves.LUCKY_CHANT, Moves.SPLASH, Moves.FOLLOW_ME]) + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.INSOMNIA) + .enemyMoveset(Array(4).fill(Moves.FLOWER_TRICK)) + .startingLevel(100) + .enemyLevel(100); + }); + + it( + "should prevent critical hits from moves", + async () => { + await game.startBattle([Species.CHARIZARD]); + + const playerPokemon = game.scene.getPlayerPokemon(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const firstTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp; + + game.doAttack(getMovePosition(game.scene, 0, Moves.LUCKY_CHANT)); + + await game.phaseInterceptor.to(BerryPhase, false); + + const secondTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp - firstTurnDamage; + expect(secondTurnDamage).toBeLessThan(firstTurnDamage); + }, TIMEOUT + ); + + it( + "should prevent critical hits against the user's ally", + async () => { + game.override.battleType("double"); + + await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); + + const playerPokemon = game.scene.getPlayerField(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.FOLLOW_ME)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const firstTurnDamage = playerPokemon[0].getMaxHp() - playerPokemon[0].hp; + + game.doAttack(getMovePosition(game.scene, 0, Moves.FOLLOW_ME)); + game.doAttack(getMovePosition(game.scene, 1, Moves.LUCKY_CHANT)); + + await game.phaseInterceptor.to(BerryPhase, false); + + const secondTurnDamage = playerPokemon[0].getMaxHp() - playerPokemon[0].hp - firstTurnDamage; + expect(secondTurnDamage).toBeLessThan(firstTurnDamage); + }, TIMEOUT + ); + + it( + "should prevent critical hits from field effects", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); + + await game.startBattle([Species.CHARIZARD]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + + enemyPokemon.addTag(BattlerTagType.ALWAYS_CRIT, 2, Moves.NONE, 0); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const firstTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp; + + game.doAttack(getMovePosition(game.scene, 0, Moves.LUCKY_CHANT)); + + await game.phaseInterceptor.to(BerryPhase, false); + + const secondTurnDamage = playerPokemon.getMaxHp() - playerPokemon.hp - firstTurnDamage; + expect(secondTurnDamage).toBeLessThan(firstTurnDamage); + }, TIMEOUT + ); +});