From 7755f798fd0f88f5fffc2451e558882ecf27cd93 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sun, 1 Sep 2024 18:21:11 -0700 Subject: [PATCH 01/14] [Ability] Implement Tera Shell (#3856) * Implement Tera Shell * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> * Add comments and fixed damage condition to `applyPreDefend` * Fix speed tie breaking things in tera shell test * Change deprecated `startBattle` calls --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> --- src/data/ability.ts | 47 ++++++++++- src/field/pokemon.ts | 7 +- src/locales/en/ability-trigger.json | 1 + src/test/abilities/tera_shell.test.ts | 111 ++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 src/test/abilities/tera_shell.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 3d5b32f4ce8..e40a88a04b2 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -8,7 +8,7 @@ import { Weather, WeatherType } from "./weather"; import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; -import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr } from "./move"; +import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { Stat, getStatName } from "./pokemon-stat"; import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; @@ -475,6 +475,47 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { } } +/** + * Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Tera_Shell_(Ability) | Tera Shell} + * When the source is at full HP, incoming attacks will have a maximum 0.5x type effectiveness multiplier. + * @extends PreDefendAbAttr + */ +export class FullHpResistTypeAbAttr extends PreDefendAbAttr { + /** + * Reduces a type multiplier to 0.5 if the source is at full HP. + * @param pokemon {@linkcode Pokemon} the Pokemon with this ability + * @param passive n/a + * @param simulated n/a (this doesn't change game state) + * @param attacker n/a + * @param move {@linkcode Move} the move being used on the source + * @param cancelled n/a + * @param args `[0]` a container for the move's current type effectiveness multiplier + * @returns `true` if the move's effectiveness is reduced; `false` otherwise + */ + applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move | null, cancelled: Utils.BooleanHolder | null, args: any[]): boolean | Promise { + const typeMultiplier = args[0]; + if (!(typeMultiplier && typeMultiplier instanceof Utils.NumberHolder)) { + return false; + } + + if (move && move.hasAttr(FixedDamageAttr)) { + return false; + } + + if (pokemon.isFullHp() && typeMultiplier.value > 0.5) { + typeMultiplier.value = 0.5; + return true; + } + return false; + } + + getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + return i18next.t("abilityTriggers:fullHpResistType", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) + }); + } +} + export class PostDefendAbAttr extends AbAttr { applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean | Promise { return false; @@ -5761,10 +5802,10 @@ export function initAbilities() { .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr), new Ability(Abilities.TERA_SHELL, 9) + .attr(FullHpResistTypeAbAttr) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .ignorable() - .unimplemented(), + .ignorable(), new Ability(Abilities.TERAFORM_ZERO, 9) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9fcbdd8dc6c..457ff874095 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -22,7 +22,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; 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, MoveTypeChangeAbAttr } from "../data/ability"; +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, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1276,6 +1276,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } + // Apply Tera Shell's effect to attacks after all immunities are accounted for + if (!ignoreAbility && move.category !== MoveCategory.STATUS) { + applyPreDefendAbAttrs(FullHpResistTypeAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier); + } + return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier; } diff --git a/src/locales/en/ability-trigger.json b/src/locales/en/ability-trigger.json index 307ab70b85c..4f1d4dac766 100644 --- a/src/locales/en/ability-trigger.json +++ b/src/locales/en/ability-trigger.json @@ -12,6 +12,7 @@ "blockItemTheft": "{{pokemonNameWithAffix}}'s {{abilityName}}\nprevents item theft!", "typeImmunityHeal": "{{pokemonNameWithAffix}}'s {{abilityName}}\nrestored its HP a little!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} avoided damage\nwith {{abilityName}}!", + "fullHpResistType": "{{pokemonNameWithAffix}} made its shell gleam!\nIt's distorting type matchups!", "moveImmunity": "It doesn't affect {{pokemonNameWithAffix}}!", "reverseDrain": "{{pokemonNameWithAffix}} sucked up the liquid ooze!", "postDefendTypeChange": "{{pokemonNameWithAffix}}'s {{abilityName}}\nmade it the {{typeName}} type!", diff --git a/src/test/abilities/tera_shell.test.ts b/src/test/abilities/tera_shell.test.ts new file mode 100644 index 00000000000..f9cb2935619 --- /dev/null +++ b/src/test/abilities/tera_shell.test.ts @@ -0,0 +1,111 @@ +import { Abilities } from "#app/enums/abilities"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; +import { HitResult } from "#app/field/pokemon.js"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const TIMEOUT = 10 * 1000; // 10 second timeout + +describe("Abilities - Tera Shell", () => { + 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") + .ability(Abilities.TERA_SHELL) + .moveset([Moves.SPLASH]) + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.INSOMNIA) + .enemyMoveset(Array(4).fill(Moves.MACH_PUNCH)) + .startingLevel(100) + .enemyLevel(100); + }); + + it( + "should change the effectiveness of non-resisted attacks when the source is at full HP", + async () => { + await game.classicMode.startBattle([Species.SNORLAX]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + vi.spyOn(playerPokemon, "getMoveEffectiveness"); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0.5); + + await game.toNextTurn(); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(2); + }, TIMEOUT + ); + + it( + "should not override type immunities", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK)); + + await game.classicMode.startBattle([Species.SNORLAX]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + vi.spyOn(playerPokemon, "getMoveEffectiveness"); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0); + }, TIMEOUT + ); + + it( + "should not override type multipliers less than 0.5x", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.QUICK_ATTACK)); + + await game.classicMode.startBattle([Species.AGGRON]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + vi.spyOn(playerPokemon, "getMoveEffectiveness"); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0.25); + }, TIMEOUT + ); + + it( + "should not affect the effectiveness of fixed-damage moves", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.DRAGON_RAGE)); + + await game.classicMode.startBattle([Species.CHARIZARD]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + vi.spyOn(playerPokemon, "apply"); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.EFFECTIVE); + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40); + }, TIMEOUT + ); +}); From 97e3250f62d956268a0076ade875e5bb9cd38af1 Mon Sep 17 00:00:00 2001 From: chaosgrimmon <31082757+chaosgrimmon@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:31:25 -0400 Subject: [PATCH 02/14] [Sprite] Chien-Pao sprite cleanup (#3961) * Delete public/images/pokemon/exp/shiny/1002b.png * Delete public/images/pokemon/exp/shiny/1002s.png * Delete public/images/pokemon/exp/shiny/1002sb.png --- public/images/pokemon/exp/shiny/1002b.png | Bin 3018 -> 0 bytes public/images/pokemon/exp/shiny/1002s.png | Bin 4517 -> 0 bytes public/images/pokemon/exp/shiny/1002sb.png | Bin 2924 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/images/pokemon/exp/shiny/1002b.png delete mode 100644 public/images/pokemon/exp/shiny/1002s.png delete mode 100644 public/images/pokemon/exp/shiny/1002sb.png diff --git a/public/images/pokemon/exp/shiny/1002b.png b/public/images/pokemon/exp/shiny/1002b.png deleted file mode 100644 index 85dfb1c4bd6b6b3fb8312e71372bf37f6758c24e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3018 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 diff --git a/public/images/pokemon/exp/shiny/1002s.png b/public/images/pokemon/exp/shiny/1002s.png deleted file mode 100644 index 835b3dcd73bde83d1eb1492aec1c6bd61a6a9608..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4517 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 diff --git a/public/images/pokemon/exp/shiny/1002sb.png b/public/images/pokemon/exp/shiny/1002sb.png deleted file mode 100644 index f87e2fc42397c82c99e00917b9c5dbd2e973ec67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2924 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 L+Y{<>-q-&N5*a0} From 3bcee779e296be330ba3c6168ff462d8f7f38d23 Mon Sep 17 00:00:00 2001 From: "Adrian T." <68144167+torranx@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:39:12 +0800 Subject: [PATCH 03/14] [Move] Fully implement dragon cheer (#3959) --- src/data/battler-tags.ts | 21 +++++++++++++++++++++ src/data/move.ts | 5 ++--- src/enums/battler-tag-type.ts | 3 ++- src/field/pokemon.ts | 13 ++++++++++--- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 8c05d296e76..38db36e86f6 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1507,6 +1507,25 @@ export class CritBoostTag extends BattlerTag { } } +/** + * Tag for the effects of Dragon Cheer, which boosts the critical hit ratio of the user's allies. + * @extends {CritBoostTag} + */ +export class DragonCheerTag extends CritBoostTag { + /** The types of the user's ally when the tag is added */ + public typesOnAdd: Type[]; + + constructor() { + super(BattlerTagType.CRIT_BOOST, Moves.DRAGON_CHEER); + } + + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + + this.typesOnAdd = pokemon.getTypes(true); + } +} + export class SaltCuredTag extends BattlerTag { private sourceIndex: number; @@ -1923,6 +1942,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new TypeBoostTag(tagType, sourceMove, Type.FIRE, 1.5, false); case BattlerTagType.CRIT_BOOST: return new CritBoostTag(tagType, sourceMove); + case BattlerTagType.DRAGON_CHEER: + return new DragonCheerTag(); case BattlerTagType.ALWAYS_CRIT: case BattlerTagType.IGNORE_ACCURACY: return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, 2, sourceMove); diff --git a/src/data/move.ts b/src/data/move.ts index d50dc7e2074..bbc33241454 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -9143,9 +9143,8 @@ export function initMoves() { new AttackMove(Moves.HARD_PRESS, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 9) .attr(OpponentHighHpPowerAttr, 100), new StatusMove(Moves.DRAGON_CHEER, Type.DRAGON, -1, 15, -1, 0, 9) - .attr(AddBattlerTagAttr, BattlerTagType.CRIT_BOOST, false, true) - .target(MoveTarget.NEAR_ALLY) - .partial(), + .attr(AddBattlerTagAttr, BattlerTagType.DRAGON_CHEER, false, true) + .target(MoveTarget.NEAR_ALLY), new AttackMove(Moves.ALLURING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 9) .soundBased() .partial(), diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index b133b442801..73580a107b2 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -69,5 +69,6 @@ export enum BattlerTagType { GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA", GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU", BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING", - SHELL_TRAP = "SHELL_TRAP" + SHELL_TRAP = "SHELL_TRAP", + DRAGON_CHEER = "DRAGON_CHEER", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 457ff874095..00d5f5d3dd2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -18,7 +18,7 @@ import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { BattleStat } from "../data/battle-stat"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; @@ -2069,9 +2069,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { critLevel.value += 1; } } - if (source.getTag(BattlerTagType.CRIT_BOOST)) { - critLevel.value += 2; + + const critBoostTag = source.getTag(CritBoostTag); + if (critBoostTag) { + if (critBoostTag instanceof DragonCheerTag) { + critLevel.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1; + } else { + critLevel.value += 2; + } } + console.log(`crit stage: +${critLevel.value}`); const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))]; isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); From 64368b62bc983ad6a475a021cfd22b372ab274ee Mon Sep 17 00:00:00 2001 From: "Adrian T." <68144167+torranx@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:39:42 +0800 Subject: [PATCH 04/14] [Ability] Add form change support for Flower Gift (#3941) * add form change support for flower gift * fix nits --- src/data/ability.ts | 34 ++++-- src/data/move.ts | 4 +- src/data/pokemon-forms.ts | 9 +- src/field/arena.ts | 10 +- src/test/abilities/flower_gift.test.ts | 154 +++++++++++++++++++++++++ 5 files changed, 193 insertions(+), 18 deletions(-) create mode 100644 src/test/abilities/flower_gift.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index e40a88a04b2..4d3d32e22fa 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2428,7 +2428,7 @@ export class PostSummonWeatherSuppressedFormChangeAbAttr extends PostSummonAbAtt /** * Triggers weather-based form change when summoned into an active weather. - * Used by Forecast. + * Used by Forecast and Flower Gift. * @extends PostSummonAbAttr */ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr { @@ -2451,7 +2451,10 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr { * @returns whether the form change was triggered */ applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { - if (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST) { + const isCastformWithForecast = (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST); + const isCherrimWithFlowerGift = (pokemon.species.speciesId === Species.CHERRIM && this.ability === Abilities.FLOWER_GIFT); + + if (isCastformWithForecast || isCherrimWithFlowerGift) { if (simulated) { return simulated; } @@ -3124,37 +3127,41 @@ export class PostWeatherChangeAbAttr extends AbAttr { /** * Triggers weather-based form change when weather changes. - * Used by Forecast. + * Used by Forecast and Flower Gift. * @extends PostWeatherChangeAbAttr */ export class PostWeatherChangeFormChangeAbAttr extends PostWeatherChangeAbAttr { private ability: Abilities; + private formRevertingWeathers: WeatherType[]; - constructor(ability: Abilities) { + constructor(ability: Abilities, formRevertingWeathers: WeatherType[]) { super(false); this.ability = ability; + this.formRevertingWeathers = formRevertingWeathers; } /** * Calls {@linkcode Arena.triggerWeatherBasedFormChangesToNormal | triggerWeatherBasedFormChangesToNormal} when the * weather changed to form-reverting weather, otherwise calls {@linkcode Arena.triggerWeatherBasedFormChanges | triggerWeatherBasedFormChanges} - * @param {Pokemon} pokemon the Pokemon that changed the weather + * @param {Pokemon} pokemon the Pokemon with this ability * @param passive n/a * @param weather n/a * @param args n/a * @returns whether the form change was triggered */ applyPostWeatherChange(pokemon: Pokemon, passive: boolean, simulated: boolean, weather: WeatherType, args: any[]): boolean { - if (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST) { + const isCastformWithForecast = (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST); + const isCherrimWithFlowerGift = (pokemon.species.speciesId === Species.CHERRIM && this.ability === Abilities.FLOWER_GIFT); + + if (isCastformWithForecast || isCherrimWithFlowerGift) { if (simulated) { return simulated; } - const formRevertingWeathers: WeatherType[] = [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ]; const weatherType = pokemon.scene.arena.weather?.weatherType; - if (weatherType && formRevertingWeathers.includes(weatherType)) { + if (weatherType && this.formRevertingWeathers.includes(weatherType)) { pokemon.scene.arena.triggerWeatherBasedFormChangesToNormal(); } else { pokemon.scene.arena.triggerWeatherBasedFormChanges(); @@ -4744,7 +4751,8 @@ function setAbilityRevealed(pokemon: Pokemon): void { */ function getPokemonWithWeatherBasedForms(scene: BattleScene) { return scene.getField(true).filter(p => - p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM + (p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM) + || (p.hasAbility(Abilities.FLOWER_GIFT) && p.species.speciesId === Species.CHERRIM) ); } @@ -4943,7 +4951,7 @@ export function initAbilities() { .attr(UncopiableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .attr(PostSummonFormChangeByWeatherAbAttr, Abilities.FORECAST) - .attr(PostWeatherChangeFormChangeAbAttr, Abilities.FORECAST), + .attr(PostWeatherChangeFormChangeAbAttr, Abilities.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ]), new Ability(Abilities.STICKY_HOLD, 3) .attr(BlockItemTheftAbAttr) .bypassFaint() @@ -5136,8 +5144,10 @@ export function initAbilities() { .conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), BattleStatMultiplierAbAttr, BattleStat.SPDEF, 1.5) .attr(UncopiableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) - .ignorable() - .partial(), + .attr(PostSummonFormChangeByWeatherAbAttr, Abilities.FLOWER_GIFT) + .attr(PostWeatherChangeFormChangeAbAttr, Abilities.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]) + .partial() // Should also boosts stats of ally + .ignorable(), new Ability(Abilities.BAD_DREAMS, 4) .attr(PostTurnHurtIfSleepingAbAttr), new Ability(Abilities.PICKPOCKET, 5) diff --git a/src/data/move.ts b/src/data/move.ts index bbc33241454..2ffa8a36de9 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5888,9 +5888,9 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr { target.summonData.ability = tempAbilityId; user.scene.queueMessage(i18next.t("moveTriggers:swappedAbilitiesWithTarget", {pokemonName: getPokemonNameWithAffix(user)})); - // Swaps Forecast from Castform + // Swaps Forecast/Flower Gift from Castform/Cherrim user.scene.arena.triggerWeatherBasedFormChangesToNormal(); - // Swaps Forecast to Castform (edge case) + // Swaps Forecast/Flower Gift to Castform/Cherrim (edge case) user.scene.arena.triggerWeatherBasedFormChanges(); return true; diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index e4417f8e8bb..1420d8800aa 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -359,7 +359,7 @@ export class SpeciesDefaultFormMatchTrigger extends SpeciesFormChangeTrigger { /** * Class used for triggering form changes based on weather. - * Used by Castform. + * Used by Castform and Cherrim. * @extends SpeciesFormChangeTrigger */ export class SpeciesFormChangeWeatherTrigger extends SpeciesFormChangeTrigger { @@ -392,7 +392,7 @@ export class SpeciesFormChangeWeatherTrigger extends SpeciesFormChangeTrigger { /** * Class used for reverting to the original form when the weather runs out * or when the user loses the ability/is suppressed. - * Used by Castform. + * Used by Castform and Cherrim. * @extends SpeciesFormChangeTrigger */ export class SpeciesFormChangeRevertWeatherFormTrigger extends SpeciesFormChangeTrigger { @@ -930,6 +930,11 @@ export const pokemonFormChanges: PokemonFormChanges = { new SpeciesFormChange(Species.CASTFORM, "rainy", "", new SpeciesFormChangeActiveTrigger(), true), new SpeciesFormChange(Species.CASTFORM, "snowy", "", new SpeciesFormChangeActiveTrigger(), true), ], + [Species.CHERRIM]: [ + new SpeciesFormChange(Species.CHERRIM, "overcast", "sunshine", new SpeciesFormChangeWeatherTrigger(Abilities.FLOWER_GIFT, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]), true), + new SpeciesFormChange(Species.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]), true), + new SpeciesFormChange(Species.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeActiveTrigger(), true), + ], }; export function initPokemonForms() { diff --git a/src/field/arena.ts b/src/field/arena.ts index 7622b9a014f..e8defbd1a8e 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -339,7 +339,10 @@ export class Arena { */ triggerWeatherBasedFormChanges(): void { this.scene.getField(true).forEach( p => { - if (p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM) { + const isCastformWithForecast = (p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM); + const isCherrimWithFlowerGift = (p.hasAbility(Abilities.FLOWER_GIFT) && p.species.speciesId === Species.CHERRIM); + + if (isCastformWithForecast || isCherrimWithFlowerGift) { new ShowAbilityPhase(this.scene, p.getBattlerIndex()); this.scene.triggerPokemonFormChange(p, SpeciesFormChangeWeatherTrigger); } @@ -351,7 +354,10 @@ export class Arena { */ triggerWeatherBasedFormChangesToNormal(): void { this.scene.getField(true).forEach( p => { - if (p.hasAbility(Abilities.FORECAST, false, true) && p.species.speciesId === Species.CASTFORM) { + const isCastformWithForecast = (p.hasAbility(Abilities.FORECAST, false, true) && p.species.speciesId === Species.CASTFORM); + const isCherrimWithFlowerGift = (p.hasAbility(Abilities.FLOWER_GIFT, false, true) && p.species.speciesId === Species.CHERRIM); + + if (isCastformWithForecast || isCherrimWithFlowerGift) { new ShowAbilityPhase(this.scene, p.getBattlerIndex()); return this.scene.triggerPokemonFormChange(p, SpeciesFormChangeRevertWeatherFormTrigger); } diff --git a/src/test/abilities/flower_gift.test.ts b/src/test/abilities/flower_gift.test.ts new file mode 100644 index 00000000000..f8c1141386d --- /dev/null +++ b/src/test/abilities/flower_gift.test.ts @@ -0,0 +1,154 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#app/enums/abilities"; +import { Stat } from "#app/enums/stat"; +import { WeatherType } from "#app/enums/weather-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Flower Gift", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const OVERCAST_FORM = 0; + const SUNSHINE_FORM = 1; + + /** + * Tests reverting to normal form when Cloud Nine/Air Lock is active on the field + * @param {GameManager} game The game manager instance + * @param {Abilities} ability The ability that is active on the field + */ + const testRevertFormAgainstAbility = async (game: GameManager, ability: Abilities) => { + game.override.starterForms({ [Species.CASTFORM]: SUNSHINE_FORM }).enemyAbility(ability); + await game.classicMode.startBattle([Species.CASTFORM]); + + game.move.select(Moves.SPLASH); + + expect(game.scene.getPlayerPokemon()?.formIndex).toBe(OVERCAST_FORM); + }; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([Moves.SPLASH, Moves.RAIN_DANCE, Moves.SUNNY_DAY, Moves.SKILL_SWAP]) + .enemySpecies(Species.MAGIKARP) + .enemyMoveset(SPLASH_ONLY) + .enemyAbility(Abilities.BALL_FETCH); + }); + + // TODO: Uncomment expect statements when the ability is implemented - currently does not increase stats of allies + it("increases the Attack and Special Defense stats of the Pokémon with this Ability and its allies by 1.5× during Harsh Sunlight", async () => { + game.override.battleType("double"); + await game.classicMode.startBattle([Species.CHERRIM, Species.MAGIKARP]); + + const [ cherrim ] = game.scene.getPlayerField(); + const cherrimAtkStat = cherrim.getBattleStat(Stat.ATK); + const cherrimSpDefStat = cherrim.getBattleStat(Stat.SPDEF); + + // const magikarpAtkStat = magikarp.getBattleStat(Stat.ATK);; + // const magikarpSpDefStat = magikarp.getBattleStat(Stat.SPDEF); + + game.move.select(Moves.SUNNY_DAY, 0); + game.move.select(Moves.SPLASH, 1); + + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(cherrim.formIndex).toBe(SUNSHINE_FORM); + expect(cherrim.getBattleStat(Stat.ATK)).toBe(Math.floor(cherrimAtkStat * 1.5)); + expect(cherrim.getBattleStat(Stat.SPDEF)).toBe(Math.floor(cherrimSpDefStat * 1.5)); + // expect(magikarp.getBattleStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5)); + // expect(magikarp.getBattleStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5)); + }); + + it("changes the Pokemon's form during Harsh Sunlight", async () => { + game.override.weather(WeatherType.HARSH_SUN); + await game.classicMode.startBattle([Species.CHERRIM]); + + const cherrim = game.scene.getPlayerPokemon()!; + expect(cherrim.formIndex).toBe(SUNSHINE_FORM); + + game.move.select(Moves.SPLASH); + }); + + it("reverts to Overcast Form if a Pokémon on the field has Air Lock", async () => { + await testRevertFormAgainstAbility(game, Abilities.AIR_LOCK); + }); + + it("reverts to Overcast Form if a Pokémon on the field has Cloud Nine", async () => { + await testRevertFormAgainstAbility(game, Abilities.CLOUD_NINE); + }); + + it("reverts to Overcast Form when the Pokémon loses Flower Gift, changes form under Harsh Sunlight/Sunny when it regains it", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.SKILL_SWAP)).weather(WeatherType.HARSH_SUN); + + await game.classicMode.startBattle([Species.CHERRIM]); + + const cherrim = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SKILL_SWAP); + + await game.phaseInterceptor.to("TurnStartPhase"); + expect(cherrim.formIndex).toBe(SUNSHINE_FORM); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(cherrim.formIndex).toBe(OVERCAST_FORM); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(cherrim.formIndex).toBe(SUNSHINE_FORM); + }); + + it("reverts to Overcast Form when the Flower Gift is suppressed, changes form under Harsh Sunlight/Sunny when it regains it", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.GASTRO_ACID)).weather(WeatherType.HARSH_SUN); + + await game.classicMode.startBattle([Species.CHERRIM, Species.MAGIKARP]); + + const cherrim = game.scene.getPlayerPokemon()!; + + expect(cherrim.formIndex).toBe(SUNSHINE_FORM); + + game.move.select(Moves.SPLASH); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(cherrim.summonData.abilitySuppressed).toBe(true); + expect(cherrim.formIndex).toBe(OVERCAST_FORM); + + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.phaseInterceptor.to("MovePhase"); + + expect(cherrim.summonData.abilitySuppressed).toBe(false); + expect(cherrim.formIndex).toBe(SUNSHINE_FORM); + }); + + it("should be in Overcast Form after the user is switched out", async () => { + game.override.weather(WeatherType.SUNNY); + + await game.classicMode.startBattle([Species.CASTFORM, Species.MAGIKARP]); + const cherrim = game.scene.getPlayerPokemon()!; + + expect(cherrim.formIndex).toBe(SUNSHINE_FORM); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + expect(cherrim.formIndex).toBe(OVERCAST_FORM); + }); +}); From 0c28da75b4a57236fae46e2ade4a95cbc8d09f02 Mon Sep 17 00:00:00 2001 From: flx-sta <50131232+flx-sta@users.noreply.github.com> Date: Sun, 1 Sep 2024 19:42:23 -0700 Subject: [PATCH 05/14] [Bug] Fix Opponent pokemon sprite disappears after using Roar and Dragon Tail #481 (#3927) --- src/data/battle-anims.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index a2f6e41f4ae..da4e7f6a33b 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -788,10 +788,10 @@ export abstract class BattleAnim { targetSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ]; targetSprite.setAngle(0); if (!this.isHideUser() && userSprite) { - userSprite.setVisible(true); + this.user?.getSprite().setVisible(true); // using this.user to fix context loss due to isOppAnim swap (#481) } if (!this.isHideTarget() && (targetSprite !== userSprite || !this.isHideUser())) { - targetSprite.setVisible(true); + this.target?.getSprite().setVisible(true); // using this.target to fix context loss due to isOppAnim swap (#481) } for (const ms of Object.values(spriteCache).flat()) { if (ms) { From c070f110e5ebc2f53362ebe7dd3adb1a7d1d80bb Mon Sep 17 00:00:00 2001 From: "gitlocalize-app[bot]" <55277160+gitlocalize-app[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:44:58 -0400 Subject: [PATCH 06/14] [Localization(fr)] Additional nature entries in pokemon-summary (#3869) Co-authored-by: Lugiad --- src/locales/fr/pokemon-summary.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/locales/fr/pokemon-summary.json b/src/locales/fr/pokemon-summary.json index f0b4f5a474f..01e712c8468 100644 --- a/src/locales/fr/pokemon-summary.json +++ b/src/locales/fr/pokemon-summary.json @@ -13,5 +13,32 @@ "metFragment": { "normal": "rencontré au N.{{level}},\n{{biome}}.", "apparently": "apparemment rencontré au N.{{level}},\n{{biome}}." + }, + "natureFragment": { + "Hardy": "{{nature}}", + "Lonely": "{{nature}}", + "Brave": "{{nature}}", + "Adamant": "{{nature}}", + "Naughty": "{{nature}}", + "Bold": "{{nature}}", + "Docile": "{{nature}}", + "Relaxed": "{{nature}}", + "Impish": "{{nature}}", + "Lax": "{{nature}}", + "Timid": "{{nature}}", + "Hasty": "{{nature}}", + "Serious": "{{nature}}", + "Jolly": "{{nature}}", + "Naive": "{{nature}}", + "Modest": "{{nature}}", + "Mild": "{{nature}}", + "Quiet": "{{nature}}", + "Bashful": "{{nature}}", + "Rash": "{{nature}}", + "Calm": "{{nature}}", + "Gentle": "{{nature}}", + "Sassy": "{{nature}}", + "Careful": "{{nature}}", + "Quirky": "{{nature}}" } -} \ No newline at end of file +} From 49c3158fd1e5d2ceb53312daf311d23e8a1396a1 Mon Sep 17 00:00:00 2001 From: "gitlocalize-app[bot]" <55277160+gitlocalize-app[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:45:42 -0400 Subject: [PATCH 07/14] [Localization(ko)] Change ghost type challenge acv for achv-female (#3866) Co-authored-by: sodam Co-authored-by: Enoch Co-authored-by: KimJeongSun --- src/locales/ko/achv.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locales/ko/achv.json b/src/locales/ko/achv.json index 8546dff949c..b9fd327ef3b 100644 --- a/src/locales/ko/achv.json +++ b/src/locales/ko/achv.json @@ -225,7 +225,7 @@ "name": "독침붕처럼 쏴라" }, "MONO_GHOST": { - "name": "누굴 부를 거야?" + "name": "무서운 게 딱 좋아!" }, "MONO_STEEL": { "name": "강철 심장" @@ -265,4 +265,4 @@ "name": "상성 전문가(였던 것)", "description": "거꾸로 배틀 챌린지 모드 클리어." } -} \ No newline at end of file +} From 14e0c66ed9d46f248f26ee7cc0e378e87792cfd1 Mon Sep 17 00:00:00 2001 From: "gitlocalize-app[bot]" <55277160+gitlocalize-app[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:46:53 -0400 Subject: [PATCH 08/14] [Localisation] [ES] Finished and reviewed terrain, modifier, trainer-names, splash messages (#3848) * Translate splash-messages.json via GitLocalize * Translate splash-messages.json via GitLocalize * Translate trainer-names.json via GitLocalize * Translate modifier.json via GitLocalize * Translate modifier.json via GitLocalize * Translate modifier.json via GitLocalize * Translate terrain.json via GitLocalize * Update src/locales/es/trainer-names.json Co-authored-by: Asdar --------- Co-authored-by: LilyAlternis Co-authored-by: Asdar Co-authored-by: Rafa Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/locales/es/modifier.json | 13 ++++++-- src/locales/es/splash-messages.json | 12 ++++---- src/locales/es/terrain.json | 17 +++++++++- src/locales/es/trainer-names.json | 48 ++++++++++++++--------------- 4 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/locales/es/modifier.json b/src/locales/es/modifier.json index 593b3df2f0f..a94e41a4574 100644 --- a/src/locales/es/modifier.json +++ b/src/locales/es/modifier.json @@ -1,3 +1,12 @@ { - "bypassSpeedChanceApply": "¡Gracias {{itemName}} {{pokemonName}} puede tener prioridad!" -} \ No newline at end of file + "surviveDamageApply": "{{pokemonNameWithAffix}} ha usado {{typeName}} y ha logrado resistir!", + "turnHealApply": "{{pokemonNameWithAffix}} ha recuperado unos pocos PS gracias a {{typeName}}!", + "hitHealApply": "{{pokemonNameWithAffix}} ha recuperado unos pocos PS gracias a {{typeName}}!", + "pokemonInstantReviveApply": "{{pokemonNameWithAffix}} ha sido revivido gracias a su {{typeName}}!", + "pokemonResetNegativeStatStageApply": "Las estadísticas bajadas de {{pokemonNameWithAffix}} fueron restauradas gracias a {{typeName}}!", + "moneyInterestApply": "Recibiste intereses de ₽{{moneyAmount}}\ngracias a {{typeName}}!", + "turnHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} fue absorbido\npor {{pokemonName}}'s {{typeName}}!", + "contactHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} fue robado por {{pokemonName}}'s {{typeName}}!", + "enemyTurnHealApply": "¡{{pokemonNameWithAffix}}\nrecuperó algunos PS!", + "bypassSpeedChanceApply": "¡Gracias a su {{itemName}}, {{pokemonName}} puede tener prioridad!" +} diff --git a/src/locales/es/splash-messages.json b/src/locales/es/splash-messages.json index 90ce3593b89..b1d4820b06e 100644 --- a/src/locales/es/splash-messages.json +++ b/src/locales/es/splash-messages.json @@ -3,12 +3,12 @@ "joinTheDiscord": "¡Únete al Discord!", "infiniteLevels": "¡Niveles infinitos!", "everythingStacks": "¡Todo se acumula!", - "optionalSaveScumming": "¡Trampas guardando (¡opcionales!)!", + "optionalSaveScumming": "¡Trampas de guardado opcionales!", "biomes": "¡35 biomas!", "openSource": "¡Código abierto!", "playWithSpeed": "¡Juega a velocidad 5x!", - "liveBugTesting": "¡Arreglo de bugs sobre la marcha!", - "heavyInfluence": "¡Influencia Alta en RoR2!", + "liveBugTesting": "¡Testeo de bugs en directo!", + "heavyInfluence": "¡Mucha Influencia de RoR2!", "pokemonRiskAndPokemonRain": "¡Pokémon Risk y Pokémon Rain!", "nowWithMoreSalt": "¡Con un 33% más de polémica!", "infiniteFusionAtHome": "¡Infinite Fusion en casa!", @@ -17,16 +17,16 @@ "mubstitute": "¡Mubstituto!", "thatsCrazy": "¡De locos!", "oranceJuice": "¡Zumo de narancia!", - "questionableBalancing": "¡Balance cuestionable!", + "questionableBalancing": "¡Cambios en balance cuestionables!", "coolShaders": "¡Shaders impresionantes!", "aiFree": "¡Libre de IA!", "suddenDifficultySpikes": "¡Saltos de dificultad repentinos!", "basedOnAnUnfinishedFlashGame": "¡Basado en un juego Flash inacabado!", - "moreAddictiveThanIntended": "¡Más adictivo de la cuenta!", + "moreAddictiveThanIntended": "¡Más adictivo de lo previsto!", "mostlyConsistentSeeds": "¡Semillas CASI consistentes!", "achievementPointsDontDoAnything": "¡Los Puntos de Logro no hacen nada!", "youDoNotStartAtLevel": "¡No empiezas al nivel 2000!", - "dontTalkAboutTheManaphyEggIncident": "¡No hablen del incidente del Huevo Manaphy!", + "dontTalkAboutTheManaphyEggIncident": "¡No se habla del Incidente Manaphy!", "alsoTryPokengine": "¡Prueba también Pokéngine!", "alsoTryEmeraldRogue": "¡Prueba también Emerald Rogue!", "alsoTryRadicalRed": "¡Prueba también Radical Red!", diff --git a/src/locales/es/terrain.json b/src/locales/es/terrain.json index 9e26dfeeb6e..912f5186180 100644 --- a/src/locales/es/terrain.json +++ b/src/locales/es/terrain.json @@ -1 +1,16 @@ -{} \ No newline at end of file +{ + "misty": "Niebla", + "mistyStartMessage": "¡La niebla ha envuelto el terreno de combate!", + "mistyClearMessage": "La niebla se ha disipado.", + "mistyBlockMessage": "¡El campo de niebla ha protegido a {{pokemonNameWithAffix}} ", + "electric": "Eléctrico", + "electricStartMessage": "¡Se ha formado un campo de corriente eléctrica en el terreno\nde combate!", + "electricClearMessage": "El campo de corriente eléctrica ha desaparecido.\t", + "grassy": "Hierba", + "grassyStartMessage": "¡El terreno de combate se ha cubierto de hierba!", + "grassyClearMessage": "La hierba ha desaparecido.", + "psychic": "Psíquico", + "psychicStartMessage": "¡El terreno de combate se ha vuelto muy extraño!", + "psychicClearMessage": "Ha desaparecido la extraña sensación que se percibía en el terreno\nde combate.", + "defaultBlockMessage": "¡El campo {{terrainName}} ha protegido a {{pokemonNameWithAffix}} " +} diff --git a/src/locales/es/trainer-names.json b/src/locales/es/trainer-names.json index c6758366db7..ce09a0c9037 100644 --- a/src/locales/es/trainer-names.json +++ b/src/locales/es/trainer-names.json @@ -1,7 +1,7 @@ { "brock": "Brock", "misty": "Misty", - "lt_surge": "Tt. Surge", + "lt_surge": "Teniente Surge", "erika": "Erika", "janine": "Sachiko", "sabrina": "Sabrina", @@ -23,7 +23,7 @@ "winona": "Alana", "tate": "Vito", "liza": "Leti", - "juan": "Galán", + "juan": "Galano", "roark": "Roco", "gardenia": "Gardenia", "maylene": "Brega", @@ -34,7 +34,7 @@ "volkner": "Lectro", "cilan": "Millo", "chili": "Zeo", - "cress": "Maiz", + "cress": "Maíz", "cheren": "Cheren", "lenora": "Aloe", "roxie": "Hiedra", @@ -57,7 +57,7 @@ "nessa": "Cathy", "kabu": "Naboru", "bea": "Judith", - "allister": "Allistair", + "allister": "Alistair", "opal": "Sally", "bede": "Berto", "gordie": "Morris", @@ -123,30 +123,28 @@ "leon": "Lionel", "rival": "Finn", "rival_female": "Ivy", - "archer": "Archer", - "ariana": "Ariana", - "proton": "Proton", + "archer": "Atlas", + "ariana": "Atenea", + "proton": "Protón", "petrel": "Petrel", - "tabitha": "Tabitha", - "courtney": "Courtney", - "shelly": "Shelly", - "matt": "Matt", - "mars": "Mars", - "jupiter": "Jupiter", - "saturn": "Saturn", - "zinzolin": "Zinzolin", - "rood": "Rood", - "xerosic": "Xerosic", - "bryony": "Bryony", + "tabitha": "Tatiano", + "courtney": "Carola", + "shelly": "Silvina", + "matt": "Matías", + "mars": "Venus", + "jupiter": "Ceres", + "saturn": "Saturno", + "zinzolin": "Menek", + "rood": "Ruga", + "xerosic": "Xero", + "bryony": "Begonia", + "maxie": "Magno", + "archie": "Aquiles", + "cyrus": "Helio", + "ghetsis": "Ghechis", + "lysandre": "Lysson", "faba": "Fabio", - - "maxie": "Maxie", - "archie": "Archie", - "cyrus": "Cyrus", - "ghetsis": "Ghetsis", - "lysandre": "Lysandre", "lusamine": "Samina", - "blue_red_double": "Azul y Rojo", "red_blue_double": "Rojo y Azul", "tate_liza_double": "Vito y Leti", From dcb03f4ee99635a56d8b7e9316f2c0f4a29c0b92 Mon Sep 17 00:00:00 2001 From: MokaStitcher <54149968+MokaStitcher@users.noreply.github.com> Date: Mon, 2 Sep 2024 04:47:22 +0200 Subject: [PATCH 09/14] [Test] Add test for final boss fight phase switch (#3847) * implement test for final boss encounter phase switch * Update Eternatus tests & helper function * fix endless_boss test following GameManager update --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/test/endless_boss.test.ts | 10 ++-- src/test/final_boss.test.ts | 98 +++++++++++++++++++++++++++++++---- src/test/utils/gameManager.ts | 29 +++++------ src/ui/ui.ts | 3 +- 4 files changed, 110 insertions(+), 30 deletions(-) diff --git a/src/test/endless_boss.test.ts b/src/test/endless_boss.test.ts index e983be245b6..8a564695e42 100644 --- a/src/test/endless_boss.test.ts +++ b/src/test/endless_boss.test.ts @@ -32,7 +32,7 @@ describe("Endless Boss", () => { it(`should spawn a minor boss every ${EndlessBossWave.Minor} waves in END biome in Endless`, async () => { game.override.startingWave(EndlessBossWave.Minor); - await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS); + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.ENDLESS); expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Minor); expect(game.scene.arena.biomeType).toBe(Biome.END); @@ -44,7 +44,7 @@ describe("Endless Boss", () => { it(`should spawn a major boss every ${EndlessBossWave.Major} waves in END biome in Endless`, async () => { game.override.startingWave(EndlessBossWave.Major); - await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS); + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.ENDLESS); expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Major); expect(game.scene.arena.biomeType).toBe(Biome.END); @@ -56,7 +56,7 @@ describe("Endless Boss", () => { it(`should spawn a minor boss every ${EndlessBossWave.Minor} waves in END biome in Spliced Endless`, async () => { game.override.startingWave(EndlessBossWave.Minor); - await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.SPLICED_ENDLESS); + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.SPLICED_ENDLESS); expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Minor); expect(game.scene.arena.biomeType).toBe(Biome.END); @@ -68,7 +68,7 @@ describe("Endless Boss", () => { it(`should spawn a major boss every ${EndlessBossWave.Major} waves in END biome in Spliced Endless`, async () => { game.override.startingWave(EndlessBossWave.Major); - await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.SPLICED_ENDLESS); + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.SPLICED_ENDLESS); expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Major); expect(game.scene.arena.biomeType).toBe(Biome.END); @@ -80,7 +80,7 @@ describe("Endless Boss", () => { it(`should NOT spawn major or minor boss outside wave ${EndlessBossWave.Minor}s in END biome`, async () => { game.override.startingWave(EndlessBossWave.Minor - 1); - await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS); + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.ENDLESS); expect(game.scene.currentBattle.waveIndex).not.toBe(EndlessBossWave.Minor); expect(game.scene.getEnemyPokemon()!.species.speciesId).not.toBe(Species.ETERNATUS); diff --git a/src/test/final_boss.test.ts b/src/test/final_boss.test.ts index 0f59572619b..5d006998a0b 100644 --- a/src/test/final_boss.test.ts +++ b/src/test/final_boss.test.ts @@ -1,8 +1,13 @@ +import { StatusEffect } from "#app/data/status-effect"; +import { Abilities } from "#app/enums/abilities"; import { Biome } from "#app/enums/biome"; +import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; import { GameModes } from "#app/game-mode"; +import { TurnHeldItemTransferModifier } from "#app/modifier/modifier"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import GameManager from "./utils/gameManager"; +import { SPLASH_ONLY } from "./utils/testUtils"; const FinalWave = { Classic: 200, @@ -20,7 +25,13 @@ describe("Final Boss", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.startingWave(FinalWave.Classic).startingBiome(Biome.END).disableCrits(); + game.override + .startingWave(FinalWave.Classic) + .startingBiome(Biome.END) + .disableCrits() + .enemyMoveset(SPLASH_ONLY) + .moveset([ Moves.SPLASH, Moves.WILL_O_WISP, Moves.DRAGON_PULSE ]) + .startingLevel(10000); }); afterEach(() => { @@ -28,7 +39,7 @@ describe("Final Boss", () => { }); it("should spawn Eternatus on wave 200 in END biome", async () => { - await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC); expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic); expect(game.scene.arena.biomeType).toBe(Biome.END); @@ -37,7 +48,7 @@ describe("Final Boss", () => { it("should NOT spawn Eternatus before wave 200 in END biome", async () => { game.override.startingWave(FinalWave.Classic - 1); - await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC); expect(game.scene.currentBattle.waveIndex).not.toBe(FinalWave.Classic); expect(game.scene.arena.biomeType).toBe(Biome.END); @@ -46,7 +57,7 @@ describe("Final Boss", () => { it("should NOT spawn Eternatus outside of END biome", async () => { game.override.startingBiome(Biome.FOREST); - await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC); expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic); expect(game.scene.arena.biomeType).not.toBe(Biome.END); @@ -54,12 +65,81 @@ describe("Final Boss", () => { }); it("should not have passive enabled on Eternatus", async () => { - await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC); + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC); - const eternatus = game.scene.getEnemyPokemon(); - expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS); - expect(eternatus?.hasPassive()).toBe(false); + const eternatus = game.scene.getEnemyPokemon()!; + expect(eternatus.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus.hasPassive()).toBe(false); + }); + + it("should change form on direct hit down to last boss fragment", async () => { + await game.runToFinalBossEncounter([Species.KYUREM], GameModes.CLASSIC); + await game.phaseInterceptor.to("CommandPhase"); + + // Eternatus phase 1 + const eternatus = game.scene.getEnemyPokemon()!; + const phase1Hp = eternatus.getMaxHp(); + expect(eternatus.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus.formIndex).toBe(0); + expect(eternatus.bossSegments).toBe(4); + expect(eternatus.bossSegmentIndex).toBe(3); + + game.move.select(Moves.DRAGON_PULSE); + await game.toNextTurn(); + + // Eternatus phase 2: changed form, healed and restored its shields + expect(eternatus.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus.hp).toBeGreaterThan(phase1Hp); + expect(eternatus.hp).toBe(eternatus.getMaxHp()); + expect(eternatus.formIndex).toBe(1); + expect(eternatus.bossSegments).toBe(5); + expect(eternatus.bossSegmentIndex).toBe(4); + const miniBlackHole = eternatus.getHeldItems().find(m => m instanceof TurnHeldItemTransferModifier); + expect(miniBlackHole).toBeDefined(); + expect(miniBlackHole?.stackCount).toBe(1); + }); + + it("should change form on status damage down to last boss fragment", async () => { + game.override.ability(Abilities.NO_GUARD); + + await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC); + await game.phaseInterceptor.to("CommandPhase"); + + // Eternatus phase 1 + const eternatus = game.scene.getEnemyPokemon()!; + const phase1Hp = eternatus.getMaxHp(); + expect(eternatus.species.speciesId).toBe(Species.ETERNATUS); + expect(eternatus.formIndex).toBe(0); + expect(eternatus.bossSegments).toBe(4); + expect(eternatus.bossSegmentIndex).toBe(3); + + game.move.select(Moves.WILL_O_WISP); + await game.toNextTurn(); + expect(eternatus.status?.effect).toBe(StatusEffect.BURN); + + const tickDamage = phase1Hp - eternatus.hp; + const lastShieldHp = Math.ceil(phase1Hp / eternatus.bossSegments); + // Stall until the burn is one hit away from breaking the last shield + while (eternatus.hp - tickDamage > lastShieldHp) { + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + } + + expect(eternatus.bossSegmentIndex).toBe(1); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + // Eternatus phase 2: changed form, healed and restored its shields + expect(eternatus.hp).toBeGreaterThan(phase1Hp); + expect(eternatus.hp).toBe(eternatus.getMaxHp()); + expect(eternatus.status).toBeFalsy(); + expect(eternatus.formIndex).toBe(1); + expect(eternatus.bossSegments).toBe(5); + expect(eternatus.bossSegmentIndex).toBe(4); + const miniBlackHole = eternatus.getHeldItems().find(m => m instanceof TurnHeldItemTransferModifier); + expect(miniBlackHole).toBeDefined(); + expect(miniBlackHole?.stackCount).toBe(1); }); - it.todo("should change form on direct hit down to last boss fragment", () => {}); }); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 5818244db8f..a9a71d16bf7 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -6,6 +6,7 @@ import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import Trainer from "#app/field/trainer"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; +import overrides from "#app/overrides"; import { CommandPhase } from "#app/phases/command-phase"; import { EncounterPhase } from "#app/phases/encounter-phase"; import { FaintPhase } from "#app/phases/faint-phase"; @@ -148,28 +149,26 @@ export default class GameManager { * @param species * @param mode */ - async runToFinalBossEncounter(game: GameManager, species: Species[], mode: GameModes) { + async runToFinalBossEncounter(species: Species[], mode: GameModes) { console.log("===to final boss encounter==="); - await game.runToTitle(); + await this.runToTitle(); - game.onNextPrompt("TitlePhase", Mode.TITLE, () => { - game.scene.gameMode = getGameMode(mode); - const starters = generateStarter(game.scene, species); - const selectStarterPhase = new SelectStarterPhase(game.scene); - game.scene.pushPhase(new EncounterPhase(game.scene, false)); + this.onNextPrompt("TitlePhase", Mode.TITLE, () => { + this.scene.gameMode = getGameMode(mode); + const starters = generateStarter(this.scene, species); + const selectStarterPhase = new SelectStarterPhase(this.scene); + this.scene.pushPhase(new EncounterPhase(this.scene, false)); selectStarterPhase.initBattle(starters); }); - game.onNextPrompt("EncounterPhase", Mode.MESSAGE, async () => { - // This will skip all entry dialogue (I can't figure out a way to sequentially handle the 8 chained messages via 1 prompt handler) - game.setMode(Mode.MESSAGE); - const encounterPhase = game.scene.getCurrentPhase() as EncounterPhase; + // This will consider all battle entry dialog as seens and skip them + vi.spyOn(this.scene.ui, "shouldSkipDialogue").mockReturnValue(true); - // No need to end phase, this will do it for you - encounterPhase.doEncounterCommon(false); - }); + if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) { + this.removeEnemyHeldItems(); + } - await game.phaseInterceptor.to(EncounterPhase, true); + await this.phaseInterceptor.to(EncounterPhase); console.log("===finished run to final boss encounter==="); } diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 7108a8dd480..8ec91b59480 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -317,10 +317,11 @@ export default class UI extends Phaser.GameObjects.Container { if (i18next.exists(keyOrText) ) { const i18nKey = keyOrText; hasi18n = true; + text = i18next.t(i18nKey, { context: genderStr }); // override text with translation // Skip dialogue if the player has enabled the option and the dialogue has been already seen - if (battleScene.skipSeenDialogues && battleScene.gameData.getSeenDialogues()[i18nKey] === true) { + if (this.shouldSkipDialogue(i18nKey)) { console.log(`Dialogue ${i18nKey} skipped`); callback(); return; From 709066bd1ab9619059259348b84515c8e9b9f27e Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 1 Sep 2024 19:57:07 -0700 Subject: [PATCH 10/14] [Move] Finish Alluring Voice, Burning Jealousy and Lash Out (#3508) * Implement Alluring Voice and Burning Jealousy * Fix Alluring Voice and add tests * Replace `BattlerTag.STATS_BOOSTED` with `PokemonTurnData` field * Work around bug with turn data * Remove unused variable * Replace nearby instances of `integer` with `number` * Fix imports * Implement Lash Out * Rename `battleStats(In|De)crease` -> `battleStats(In|De)creased` * Fix copy/paste error Co-authored-by: schmidtc1 <62030095+schmidtc1@users.noreply.github.com> * Update tests --------- Co-authored-by: ElliottSimmonds Co-authored-by: schmidtc1 <62030095+schmidtc1@users.noreply.github.com> Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com> --- src/data/move.ts | 61 ++++++++++++-- src/field/pokemon.ts | 32 ++++---- src/phases/stat-change-phase.ts | 36 ++++++--- src/test/moves/alluring_voice.test.ts | 54 +++++++++++++ src/test/moves/burning_jealousy.test.ts | 103 ++++++++++++++++++++++++ src/test/moves/lash_out.test.ts | 52 ++++++++++++ 6 files changed, 307 insertions(+), 31 deletions(-) create mode 100644 src/test/moves/alluring_voice.test.ts create mode 100644 src/test/moves/burning_jealousy.test.ts create mode 100644 src/test/moves/lash_out.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index 2ffa8a36de9..95d306a61ba 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6037,6 +6037,57 @@ export class DestinyBondAttr extends MoveEffectAttr { } } +/** + * Attribute to apply a battler tag to the target if they have had their stats boosted this turn. + * @extends AddBattlerTagAttr + */ +export class AddBattlerTagIfBoostedAttr extends AddBattlerTagAttr { + constructor(tag: BattlerTagType) { + super(tag, false, false, 2, 5); + } + + /** + * @param user {@linkcode Pokemon} using this move + * @param target {@linkcode Pokemon} target of this move + * @param move {@linkcode Move} being used + * @param {any[]} args N/A + * @returns true + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (target.turnData.battleStatsIncreased) { + super.apply(user, target, move, args); + } + return true; + } +} + +/** + * Attribute to apply a status effect to the target if they have had their stats boosted this turn. + * @extends MoveEffectAttr + */ +export class StatusIfBoostedAttr extends MoveEffectAttr { + public effect: StatusEffect; + + constructor(effect: StatusEffect) { + super(true, MoveEffectTrigger.HIT); + this.effect = effect; + } + + /** + * @param user {@linkcode Pokemon} using this move + * @param target {@linkcode Pokemon} target of this move + * @param move {@linkcode Move} N/A + * @param {any[]} args N/A + * @returns true + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (target.turnData.battleStatsIncreased) { + target.trySetStatus(this.effect, true, user); + } + return true; + } +} + export class LastResortAttr extends MoveAttr { getCondition(): MoveConditionFunc { return (user: Pokemon, target: Pokemon, move: Move) => { @@ -8694,10 +8745,10 @@ export function initMoves() { new AttackMove(Moves.SKITTER_SMACK, Type.BUG, MoveCategory.PHYSICAL, 70, 90, 10, 100, 0, 8) .attr(StatChangeAttr, BattleStat.SPATK, -1), new AttackMove(Moves.BURNING_JEALOUSY, Type.FIRE, MoveCategory.SPECIAL, 70, 100, 5, 100, 0, 8) - .target(MoveTarget.ALL_NEAR_ENEMIES) - .partial(), + .attr(StatusIfBoostedAttr, StatusEffect.BURN) + .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.LASH_OUT, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8) - .partial(), + .attr(MovePowerMultiplierAttr, (user, target, move) => user.turnData.battleStatsDecreased ? 2 : 1), new AttackMove(Moves.POLTERGEIST, Type.GHOST, MoveCategory.PHYSICAL, 110, 90, 5, -1, 0, 8) .attr(AttackedByItemAttr) .makesContact(false), @@ -9146,8 +9197,8 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.DRAGON_CHEER, false, true) .target(MoveTarget.NEAR_ALLY), new AttackMove(Moves.ALLURING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 9) - .soundBased() - .partial(), + .attr(AddBattlerTagIfBoostedAttr, BattlerTagType.CONFUSED) + .soundBased(), new AttackMove(Moves.TEMPER_FLARE, Type.FIRE, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 9) .attr(MovePowerMultiplierAttr, (user, target, move) => user.getLastXMoves(2)[1]?.result === MoveResult.MISS || user.getLastXMoves(2)[1]?.result === MoveResult.FAIL ? 2 : 1), new AttackMove(Moves.SUPERCELL_SLAM, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 95, 15, -1, 0, 9) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 00d5f5d3dd2..b1fcd512e35 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4286,7 +4286,7 @@ export interface TurnMove { targets?: BattlerIndex[]; result: MoveResult; virtual?: boolean; - turn?: integer; + turn?: number; } export interface QueuedMove { @@ -4298,17 +4298,17 @@ export interface QueuedMove { export interface AttackMoveResult { move: Moves; result: DamageResult; - damage: integer; + damage: number; critical: boolean; - sourceId: integer; + sourceId: number; sourceBattlerIndex: BattlerIndex; } export class PokemonSummonData { - public battleStats: integer[] = [ 0, 0, 0, 0, 0, 0, 0 ]; + public battleStats: number[] = [ 0, 0, 0, 0, 0, 0, 0 ]; public moveQueue: QueuedMove[] = []; public disabledMove: Moves = Moves.NONE; - public disabledTurns: integer = 0; + public disabledTurns: number = 0; public tags: BattlerTag[] = []; public abilitySuppressed: boolean = false; public abilitiesApplied: Abilities[] = []; @@ -4318,14 +4318,14 @@ export class PokemonSummonData { public ability: Abilities = Abilities.NONE; public gender: Gender; public fusionGender: Gender; - public stats: integer[]; + public stats: number[]; public moveset: (PokemonMove | null)[]; // If not initialized this value will not be populated from save data. public types: Type[] = []; } export class PokemonBattleData { - public hitCount: integer = 0; + public hitCount: number = 0; public endured: boolean = false; public berriesEaten: BerryType[] = []; public abilitiesApplied: Abilities[] = []; @@ -4334,21 +4334,23 @@ export class PokemonBattleData { export class PokemonBattleSummonData { /** The number of turns the pokemon has passed since entering the battle */ - public turnCount: integer = 1; + public turnCount: number = 1; /** The list of moves the pokemon has used since entering the battle */ public moveHistory: TurnMove[] = []; } export class PokemonTurnData { - public flinched: boolean; - public acted: boolean; - public hitCount: integer; - public hitsLeft: integer; - public damageDealt: integer = 0; - public currDamageDealt: integer = 0; - public damageTaken: integer = 0; + public flinched: boolean = false; + public acted: boolean = false; + public hitCount: number; + public hitsLeft: number; + public damageDealt: number = 0; + public currDamageDealt: number = 0; + public damageTaken: number = 0; public attacksReceived: AttackMoveResult[] = []; public order: number; + public battleStatsIncreased: boolean = false; + public battleStatsDecreased: boolean = false; } export enum AiType { diff --git a/src/phases/stat-change-phase.ts b/src/phases/stat-change-phase.ts index 856d0a33eea..3116c49e8ef 100644 --- a/src/phases/stat-change-phase.ts +++ b/src/phases/stat-change-phase.ts @@ -1,14 +1,14 @@ -import BattleScene from "#app/battle-scene.js"; -import { BattlerIndex } from "#app/battle.js"; -import { applyPreStatChangeAbAttrs, ProtectStatAbAttr, applyAbAttrs, StatChangeMultiplierAbAttr, StatChangeCopyAbAttr, applyPostStatChangeAbAttrs, PostStatChangeAbAttr } from "#app/data/ability.js"; -import { MistTag, ArenaTagSide } from "#app/data/arena-tag.js"; -import { BattleStat, getBattleStatName, getBattleStatLevelChangeDescription } from "#app/data/battle-stat.js"; -import Pokemon from "#app/field/pokemon.js"; -import { getPokemonNameWithAffix } from "#app/messages.js"; -import { PokemonResetNegativeStatStageModifier } from "#app/modifier/modifier.js"; -import { handleTutorial, Tutorial } from "#app/tutorial.js"; +import { BattlerIndex } from "#app/battle"; +import BattleScene from "#app/battle-scene"; +import { applyAbAttrs, applyPostStatChangeAbAttrs, applyPreStatChangeAbAttrs, PostStatChangeAbAttr, ProtectStatAbAttr, StatChangeCopyAbAttr, StatChangeMultiplierAbAttr } from "#app/data/ability"; +import { ArenaTagSide, MistTag } from "#app/data/arena-tag"; +import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat"; +import Pokemon from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { PokemonResetNegativeStatStageModifier } from "#app/modifier/modifier"; +import { handleTutorial, Tutorial } from "#app/tutorial"; +import * as Utils from "#app/utils"; import i18next from "i18next"; -import * as Utils from "#app/utils.js"; import { PokemonPhase } from "./pokemon-phase"; export type StatChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void; @@ -72,7 +72,7 @@ export class StatChangePhase extends PokemonPhase { } const battleStats = this.getPokemon().summonData.battleStats; - const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats![stat] + levels.value, 6) : Math.max(battleStats![stat] + levels.value, -6)) - battleStats![stat]); + const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]); this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels); @@ -85,6 +85,20 @@ export class StatChangePhase extends PokemonPhase { } for (const stat of filteredStats) { + if (levels.value > 0 && pokemon.summonData.battleStats[stat] < 6) { + if (!pokemon.turnData) { + // Temporary fix for missing turn data struct on turn 1 + pokemon.resetTurnData(); + } + pokemon.turnData.battleStatsIncreased = true; + } else if (levels.value < 0 && pokemon.summonData.battleStats[stat] > -6) { + if (!pokemon.turnData) { + // Temporary fix for missing turn data struct on turn 1 + pokemon.resetTurnData(); + } + pokemon.turnData.battleStatsDecreased = true; + } + pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6); } diff --git a/src/test/moves/alluring_voice.test.ts b/src/test/moves/alluring_voice.test.ts new file mode 100644 index 00000000000..e6ece39524a --- /dev/null +++ b/src/test/moves/alluring_voice.test.ts @@ -0,0 +1,54 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#app/enums/abilities"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { BerryPhase } from "#app/phases/berry-phase"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Alluring Voice", () => { + 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") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.ICE_SCALES) + .enemyMoveset(Array(4).fill(Moves.HOWL)) + .startingLevel(10) + .enemyLevel(10) + .starterSpecies(Species.FEEBAS) + .ability(Abilities.BALL_FETCH) + .moveset([Moves.ALLURING_VOICE]); + + }); + + it("should confuse the opponent if their stats were raised", async () => { + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.ALLURING_VOICE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to(BerryPhase); + + expect(enemy.getTag(BattlerTagType.CONFUSED)?.tagType).toBe("CONFUSED"); + }, TIMEOUT); +}); diff --git a/src/test/moves/burning_jealousy.test.ts b/src/test/moves/burning_jealousy.test.ts new file mode 100644 index 00000000000..2281fe74acb --- /dev/null +++ b/src/test/moves/burning_jealousy.test.ts @@ -0,0 +1,103 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { Abilities } from "#app/enums/abilities"; +import { StatusEffect } from "#app/enums/status-effect"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Burning Jealousy", () => { + 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") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.ICE_SCALES) + .enemyMoveset(Array(4).fill(Moves.HOWL)) + .startingLevel(10) + .enemyLevel(10) + .starterSpecies(Species.FEEBAS) + .ability(Abilities.BALL_FETCH) + .moveset([Moves.BURNING_JEALOUSY, Moves.GROWL]); + + }); + + it("should burn the opponent if their stats were raised", async () => { + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.BURNING_JEALOUSY); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.status?.effect).toBe(StatusEffect.BURN); + }, TIMEOUT); + + it("should still burn the opponent if their stats were both raised and lowered in the same turn", async () => { + game.override + .starterSpecies(0) + .battleType("double"); + await game.classicMode.startBattle([Species.FEEBAS, Species.ABRA]); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.BURNING_JEALOUSY); + game.move.select(Moves.GROWL, 1); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.status?.effect).toBe(StatusEffect.BURN); + }, TIMEOUT); + + it("should ignore stats raised by imposter", async () => { + game.override + .enemySpecies(Species.DITTO) + .enemyAbility(Abilities.IMPOSTER) + .enemyMoveset(SPLASH_ONLY); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.BURNING_JEALOUSY); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.status?.effect).toBeUndefined(); + }, TIMEOUT); + + it.skip("should ignore weakness policy", async () => { // TODO: Make this test if WP is implemented + await game.classicMode.startBattle(); + }, TIMEOUT); + + it("should be boosted by Sheer Force even if opponent didn't raise stats", async () => { + game.override + .ability(Abilities.SHEER_FORCE) + .enemyMoveset(SPLASH_ONLY); + vi.spyOn(allMoves[Moves.BURNING_JEALOUSY], "calculateBattlePower"); + await game.classicMode.startBattle(); + + game.move.select(Moves.BURNING_JEALOUSY); + await game.phaseInterceptor.to("BerryPhase"); + + expect(allMoves[Moves.BURNING_JEALOUSY].calculateBattlePower).toHaveReturnedWith(allMoves[Moves.BURNING_JEALOUSY].power * 5461 / 4096); + }, TIMEOUT); +}); diff --git a/src/test/moves/lash_out.test.ts b/src/test/moves/lash_out.test.ts new file mode 100644 index 00000000000..78f4b712cf0 --- /dev/null +++ b/src/test/moves/lash_out.test.ts @@ -0,0 +1,52 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { Abilities } from "#app/enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Lash Out", () => { + 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") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.FUR_COAT) + .enemyMoveset(Array(4).fill(Moves.GROWL)) + .startingLevel(10) + .enemyLevel(10) + .starterSpecies(Species.FEEBAS) + .ability(Abilities.BALL_FETCH) + .moveset([Moves.LASH_OUT]); + + }); + + it("should deal double damage if the user's stats were lowered this turn", async () => { + vi.spyOn(allMoves[Moves.LASH_OUT], "calculateBattlePower"); + await game.classicMode.startBattle(); + + game.move.select(Moves.LASH_OUT); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(allMoves[Moves.LASH_OUT].calculateBattlePower).toHaveReturnedWith(150); + }, TIMEOUT); +}); From 80828359e2b135e7b6e2c7c2df990fa6b2d9be8a Mon Sep 17 00:00:00 2001 From: "gitlocalize-app[bot]" <55277160+gitlocalize-app[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 23:28:10 -0400 Subject: [PATCH 11/14] [Localization(en)] Fix Roark Dialouge (#3873) * Translate dialogue-male.json via GitLocalize * Added female * ! * Update src/locales/en/dialogue-male.json * Remove empty files * Added missing comma --------- Co-authored-by: Jannik Tappert Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> --- src/locales/de/dialogue.json | 8 ++++---- src/locales/en/dialogue.json | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/locales/de/dialogue.json b/src/locales/de/dialogue.json index e5bcb81ce52..8a3dbb8880e 100644 --- a/src/locales/de/dialogue.json +++ b/src/locales/de/dialogue.json @@ -1403,19 +1403,19 @@ "1": "Ich muss dein Potenzial als Trainer und die Stärke der Pokémon sehen, die mit dir kämpfen!", "2": "Los geht's! Dies sind meine Gesteins-Pokémon, mein ganzer Stolz!", "3": "Gesteins-Pokémon sind einfach die besten!", - "4": "Ich muss dein Potenzial als Trainer und die Stärke der Pokémon sehen, die mit dir kämpfen!" + "4": "Tag für Tag grabe ich hier nach Fossilien.\n$Die viele Arbeit hat meine Pokémon felsenfest gemacht\nund das wirst du jetzt im Kampf zu spüren bekommen!" }, "victory": { "1": "W-was? Das kann nicht sein! Meine total tranierten Pokémon!", "2": "…Wir haben die Kontrolle verloren. Beim nächsten Mal fordere ich dich\n$zu einem Fossilien-Ausgrabungswettbewerb heraus.", "3": "Mit deinem Können ist es nur natürlich, dass du gewinnst.", - "4": "W-was?! Das kann nicht sein! Selbst das war nicht genug?", - "5": "Ich habe es vermasselt." + "4": "W-was?! Das kann nicht sein! Selbst das war nicht genug?" }, "defeat": { "1": "Siehst du? Ich bin stolz auf meinen steinigen Kampfstil!", "2": "Danke! Der Kampf hat mir Vertrauen gegeben, dass ich vielleicht meinen Vater besiegen kann!", - "3": "Ich fühle mich, als hätte ich gerade einen wirklich hartnäckigen Felsen durchbrochen!" + "3": "Na, was sagst du jetzt? Meine felsenfesten Pokémon waren hart genug für dich, was?", + "4": "Ich wusste, dass ich gewinnen würde!" } }, "morty": { diff --git a/src/locales/en/dialogue.json b/src/locales/en/dialogue.json index 1f8919f11b5..90b03a176d1 100644 --- a/src/locales/en/dialogue.json +++ b/src/locales/en/dialogue.json @@ -742,7 +742,7 @@ "plumeria": { "encounter": { "1": " ...Hmph. You don't look like anything special to me.", - "2": "It takes these dumb Grunts way too long to deal with you kids..", + "2": "It takes these dumb Grunts way too long to deal with you kids...", "3": "Mess with anyone in Team Skull, and I'll show you how serious I can get." }, "victory": { @@ -1479,21 +1479,21 @@ "1_female": "I need to see your potential as a Trainer. And, I'll need to see the toughness of the Pokémon that battle with you!", "2": "Here goes! These are my rocking Pokémon, my pride and joy!", "3": "Rock-type Pokémon are simply the best!", - "4": "I need to see your potential as a Trainer. And, I'll need to see the toughness of the Pokémon that battle with you!", - "4_female": "I need to see your potential as a Trainer. And, I'll need to see the toughness of the Pokémon that battle with you!" + "4": "Every day, I toughened up my Pokémon by digging up Fossils nonstop.\n$Could I show you how tough I made them in a battle?", + "4_female": "Every day, I toughened up my Pokémon by digging up Fossils nonstop.\n$Could I show you how tough I made them in a battle?" }, "victory": { "1": "W-what? That can't be! My buffed-up Pokémon!", "2": "…We lost control there. Next time I'd like to challenge you to a Fossil-digging race underground.", "2_female": "…We lost control there. Next time I'd like to challenge you to a Fossil-digging race underground.", "3": "With skill like yours, it's natural for you to win.", - "4": "Wh-what?! It can't be! Even that wasn't enough?", - "5": "I blew it." + "4": "Wh-what?! It can't be! Even that wasn't enough?" }, "defeat": { "1": "See? I'm proud of my rocking battle style!", "2": "Thanks! The battle gave me confidence that I may be able to beat my dad!", - "3": "I feel like I just smashed through a really stubborn boulder!" + "3": "See? These are my rocking Pokémon, my pride and joy!", + "4": "I knew I would win!" } }, "morty": { From 56b39032b9cf272e6b63c31eec2b43f834ef85be Mon Sep 17 00:00:00 2001 From: "Adrian T." <68144167+torranx@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:30:27 +0800 Subject: [PATCH 12/14] [Dev] add script to create test boilerplate file (#3954) * add script to create test boilerplate file * add more docs * add timeout to template --- create-test-boilerplate.js | 101 +++++++++++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 create-test-boilerplate.js diff --git a/create-test-boilerplate.js b/create-test-boilerplate.js new file mode 100644 index 00000000000..bf68258f321 --- /dev/null +++ b/create-test-boilerplate.js @@ -0,0 +1,101 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +/** + * This script creates a test boilerplate file for a move or ability. + * @param {string} type - The type of test to create. Either "move" or "ability". + * @param {string} fileName - The name of the file to create. + * @example npm run create-test move tackle + */ + +// Get the directory name of the current module file +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Get the arguments from the command line +const args = process.argv.slice(2); +const type = args[0]; // "move" or "ability" +let fileName = args[1]; // The file name + +if (!type || !fileName) { + console.error('Please provide both a type ("move" or "ability") and a file name.'); + process.exit(1); +} + +// Convert fileName from to snake_case if camelCase is given +fileName = fileName.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); + +// Format the description for the test case +const formattedName = fileName + .replace(/_/g, ' ') + .replace(/\b\w/g, char => char.toUpperCase()); + +// Determine the directory based on the type +let dir; +let description; +if (type === 'move') { + dir = path.join(__dirname, 'src', 'test', 'moves'); + description = `Moves - ${formattedName}`; +} else if (type === 'ability') { + dir = path.join(__dirname, 'src', 'test', 'abilities'); + description = `Abilities - ${formattedName}`; +} else { + console.error('Invalid type. Please use "move" or "ability".'); + process.exit(1); +} + +// Ensure the directory exists +if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); +} + +// Create the file with the given name +const filePath = path.join(dir, `${fileName}.test.ts`); + +if (fs.existsSync(filePath)) { + console.error(`File "${fileName}.test.ts" already exists.`); + process.exit(1); +} + +// Define the content template +const content = `import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it } from "vitest"; + +describe("${description}", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 20 * 1000; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(SPLASH_ONLY); + }); + + it("test case", async () => { + // await game.classicMode.startBattle(); + // game.move.select(); + }, TIMEOUT); +}); +`; + +// Write the template content to the file +fs.writeFileSync(filePath, content, 'utf8'); + +console.log(`File created at: ${filePath}`); \ No newline at end of file diff --git a/package.json b/package.json index b85ac639a1b..83e82585d1e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "eslint-ci": "eslint .", "docs": "typedoc", "depcruise": "depcruise src", - "depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg" + "depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg", + "create-test": "node ./create-test-boilerplate.js" }, "devDependencies": { "@eslint/js": "^9.3.0", From 1fd662111e0b668e9f98a7b2c333cd67b9f54c37 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 1 Sep 2024 20:32:31 -0700 Subject: [PATCH 13/14] [Test] Tests now default to using "Set" battle style (#3728) * Tests now default to using "Set" battle style * Fix typo --- src/test/abilities/ability_timing.test.ts | 21 +-- src/test/abilities/disguise.test.ts | 42 ++--- src/test/abilities/intimidate.test.ts | 205 ++++++++-------------- src/test/evolution.test.ts | 27 ++- src/test/evolutions/evolutions.test.ts | 48 ----- src/test/{utils => }/misc.test.ts | 6 +- src/test/moves/spikes.test.ts | 96 ++++------ src/test/utils/gameManager.ts | 2 +- src/test/utils/helpers/settingsHelper.ts | 16 +- 9 files changed, 173 insertions(+), 290 deletions(-) delete mode 100644 src/test/evolutions/evolutions.test.ts rename src/test/{utils => }/misc.test.ts (90%) diff --git a/src/test/abilities/ability_timing.test.ts b/src/test/abilities/ability_timing.test.ts index 3238f880992..fb3d3af1a6b 100644 --- a/src/test/abilities/ability_timing.test.ts +++ b/src/test/abilities/ability_timing.test.ts @@ -1,13 +1,11 @@ +import { BattleStyle } from "#app/enums/battle-style"; import { CommandPhase } from "#app/phases/command-phase"; -import { MessagePhase } from "#app/phases/message-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import i18next, { initI18n } from "#app/plugins/i18n"; import { Mode } from "#app/ui/ui"; import { Abilities } from "#enums/abilities"; -import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; -import { SPLASH_ONLY } from "#test/utils/testUtils"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -28,19 +26,18 @@ describe("Ability Timing", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleType("single"); - game.override.enemySpecies(Species.PIDGEY); - game.override.enemyAbility(Abilities.INTIMIDATE); - game.override.enemyMoveset(SPLASH_ONLY); - - game.override.ability(Abilities.BALL_FETCH); - game.override.moveset([Moves.SPLASH, Moves.ICE_BEAM]); + game.override + .battleType("single") + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.INTIMIDATE) + .ability(Abilities.BALL_FETCH); }); - it("should trigger after switch check", async() => { + it("should trigger after switch check", async () => { initI18n(); i18next.changeLanguage("en"); + game.settings.battleStyle = BattleStyle.SWITCH; await game.classicMode.runToSummon([Species.EEVEE, Species.FEEBAS]); game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { @@ -48,7 +45,7 @@ describe("Ability Timing", () => { game.endPhase(); }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); - await game.phaseInterceptor.to(MessagePhase); + await game.phaseInterceptor.to("MessagePhase"); const message = game.textInterceptor.getLatestMessage(); expect(message).toContain("Attack fell"); }, 5000); diff --git a/src/test/abilities/disguise.test.ts b/src/test/abilities/disguise.test.ts index 85141fdb491..bbb0a20dc1a 100644 --- a/src/test/abilities/disguise.test.ts +++ b/src/test/abilities/disguise.test.ts @@ -1,11 +1,5 @@ import { BattleStat } from "#app/data/battle-stat"; import { StatusEffect } from "#app/data/status-effect"; -import { CommandPhase } from "#app/phases/command-phase"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import { MoveEndPhase } from "#app/phases/move-end-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; -import { Mode } from "#app/ui/ui"; import { toDmgValue } from "#app/utils"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -33,13 +27,12 @@ describe("Abilities - Disguise", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleType("single"); - - game.override.enemySpecies(Species.MIMIKYU); - game.override.enemyMoveset(SPLASH_ONLY); - - game.override.starterSpecies(Species.REGIELEKI); - game.override.moveset([Moves.SHADOW_SNEAK, Moves.VACUUM_WAVE, Moves.TOXIC_THREAD, Moves.SPLASH]); + game.override + .battleType("single") + .enemySpecies(Species.MIMIKYU) + .enemyMoveset(SPLASH_ONLY) + .starterSpecies(Species.REGIELEKI) + .moveset([Moves.SHADOW_SNEAK, Moves.VACUUM_WAVE, Moves.TOXIC_THREAD, Moves.SPLASH]); }, TIMEOUT); it("takes no damage from attacking move and transforms to Busted form, takes 1/8 max HP damage from the disguise breaking", async () => { @@ -53,7 +46,7 @@ describe("Abilities - Disguise", () => { game.move.select(Moves.SHADOW_SNEAK); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(mimikyu.hp).equals(maxHp - disguiseDamage); expect(mimikyu.formIndex).toBe(bustedForm); @@ -68,7 +61,7 @@ describe("Abilities - Disguise", () => { game.move.select(Moves.VACUUM_WAVE); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(mimikyu.formIndex).toBe(disguisedForm); }, TIMEOUT); @@ -87,12 +80,12 @@ describe("Abilities - Disguise", () => { game.move.select(Moves.SURGING_STRIKES); // First hit - await game.phaseInterceptor.to(MoveEffectPhase); + await game.phaseInterceptor.to("MoveEffectPhase"); expect(mimikyu.hp).equals(maxHp - disguiseDamage); expect(mimikyu.formIndex).toBe(disguisedForm); // Second hit - await game.phaseInterceptor.to(MoveEffectPhase); + await game.phaseInterceptor.to("MoveEffectPhase"); expect(mimikyu.hp).lessThan(maxHp - disguiseDamage); expect(mimikyu.formIndex).toBe(bustedForm); }, TIMEOUT); @@ -105,7 +98,7 @@ describe("Abilities - Disguise", () => { game.move.select(Moves.TOXIC_THREAD); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(mimikyu.formIndex).toBe(disguisedForm); expect(mimikyu.status?.effect).toBe(StatusEffect.POISON); @@ -125,7 +118,7 @@ describe("Abilities - Disguise", () => { game.move.select(Moves.SPLASH); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(mimikyu.formIndex).toBe(bustedForm); expect(mimikyu.hp).equals(maxHp - disguiseDamage); @@ -133,7 +126,7 @@ describe("Abilities - Disguise", () => { await game.toNextTurn(); game.doSwitchPokemon(1); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(mimikyu.formIndex).toBe(bustedForm); }, TIMEOUT); @@ -194,15 +187,6 @@ describe("Abilities - Disguise", () => { await game.toNextTurn(); game.move.select(Moves.SPLASH); await game.doKillOpponents(); - game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { // TODO: Make tests run in set mode instead of switch mode - game.setMode(Mode.MESSAGE); - game.endPhase(); - }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); - - game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { - game.setMode(Mode.MESSAGE); - game.endPhase(); - }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); await game.phaseInterceptor.to("PartyHealPhase"); expect(mimikyu1.formIndex).toBe(disguisedForm); diff --git a/src/test/abilities/intimidate.test.ts b/src/test/abilities/intimidate.test.ts index 93b663d06da..bc5b5ab1a7d 100644 --- a/src/test/abilities/intimidate.test.ts +++ b/src/test/abilities/intimidate.test.ts @@ -1,12 +1,8 @@ import { BattleStat } from "#app/data/battle-stat"; import { Status, StatusEffect } from "#app/data/status-effect"; import { GameModes, getGameMode } from "#app/game-mode"; -import { CommandPhase } from "#app/phases/command-phase"; -import { DamagePhase } from "#app/phases/damage-phase"; import { EncounterPhase } from "#app/phases/encounter-phase"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { Mode } from "#app/ui/ui"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; @@ -33,36 +29,26 @@ describe("Abilities - Intimidate", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleType("single"); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.INTIMIDATE); - game.override.enemyPassiveAbility(Abilities.HYDRATION); - game.override.ability(Abilities.INTIMIDATE); - game.override.startingWave(3); - game.override.enemyMoveset(SPLASH_ONLY); + game.override + .battleType("single") + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.INTIMIDATE) + .ability(Abilities.INTIMIDATE) + .moveset([Moves.SPLASH, Moves.AERIAL_ACE]) + .enemyMoveset(SPLASH_ONLY); }); it("single - wild with switch", async () => { - await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - game.onNextPrompt( - "CheckSwitchPhase", - Mode.CONFIRM, - () => { - game.setMode(Mode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase) - ); - await game.phaseInterceptor.to(CommandPhase, false); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.MIGHTYENA); + await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); + game.doSwitchPokemon(1); - await game.phaseInterceptor.run(CommandPhase); - await game.phaseInterceptor.to(CommandPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA); + await game.phaseInterceptor.to("CommandPhase"); battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(0); @@ -73,25 +59,16 @@ describe("Abilities - Intimidate", () => { it("single - boss should only trigger once then switch", async () => { game.override.startingWave(10); - await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - game.onNextPrompt( - "CheckSwitchPhase", - Mode.CONFIRM, - () => { - game.setMode(Mode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase) - ); - await game.phaseInterceptor.to(CommandPhase, false); + await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); + game.doSwitchPokemon(1); - await game.phaseInterceptor.run(CommandPhase); - await game.phaseInterceptor.to(CommandPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA); + await game.phaseInterceptor.to("CommandPhase"); battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(0); @@ -102,25 +79,16 @@ describe("Abilities - Intimidate", () => { it("single - trainer should only trigger once with switch", async () => { game.override.startingWave(5); - await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - game.onNextPrompt( - "CheckSwitchPhase", - Mode.CONFIRM, - () => { - game.setMode(Mode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase) - ); - await game.phaseInterceptor.to(CommandPhase, false); + await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); + game.doSwitchPokemon(1); - await game.phaseInterceptor.run(CommandPhase); - await game.phaseInterceptor.to(CommandPhase); - expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA); + await game.phaseInterceptor.to("CommandPhase"); battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(0); @@ -130,21 +98,14 @@ describe("Abilities - Intimidate", () => { }, 200000); it("double - trainer should only trigger once per pokemon", async () => { - game.override.battleType("double"); - game.override.startingWave(5); - await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - game.onNextPrompt( - "CheckSwitchPhase", - Mode.CONFIRM, - () => { - game.setMode(Mode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase) - ); - await game.phaseInterceptor.to(CommandPhase, false); + game.override + .battleType("double") + .startingWave(5); + await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2); @@ -157,20 +118,11 @@ describe("Abilities - Intimidate", () => { it("double - wild: should only trigger once per pokemon", async () => { game.override.battleType("double"); - game.override.startingWave(3); - await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - game.onNextPrompt( - "CheckSwitchPhase", - Mode.CONFIRM, - () => { - game.setMode(Mode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase) - ); - await game.phaseInterceptor.to(CommandPhase, false); + await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2); @@ -182,21 +134,14 @@ describe("Abilities - Intimidate", () => { }, 20000); it("double - boss: should only trigger once per pokemon", async () => { - game.override.battleType("double"); - game.override.startingWave(10); - await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - game.onNextPrompt( - "CheckSwitchPhase", - Mode.CONFIRM, - () => { - game.setMode(Mode.MESSAGE); - game.endPhase(); - }, - () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase) - ); - await game.phaseInterceptor.to(CommandPhase, false); + game.override + .battleType("double") + .startingWave(10); + await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2); @@ -208,104 +153,101 @@ describe("Abilities - Intimidate", () => { }, 20000); it("single - wild next wave opp triger once, us: none", async () => { - game.override.startingWave(2); - game.override.moveset([Moves.AERIAL_ACE]); - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + await game.startBattle([Species.MIGHTYENA]); + let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); game.move.select(Moves.AERIAL_ACE); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(DamagePhase); - await game.killPokemon(game.scene.currentBattle.enemyParty[0]); - expect(game.scene.currentBattle.enemyParty[0].isFainted()).toBe(true); + await game.phaseInterceptor.to("DamagePhase"); + await game.doKillOpponents(); await game.toNextWave(); + battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); + battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); }, 20000); it("single - wild next turn - no retrigger on next turn", async () => { - game.override.startingWave(2); - game.override.moveset([Moves.SPLASH]); - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + await game.startBattle([Species.MIGHTYENA]); + let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - game.move.select(Moves.AERIAL_ACE); - console.log("===to new turn==="); + game.move.select(Moves.SPLASH); await game.toNextTurn(); + battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); + battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); }, 20000); it("single - trainer should only trigger once and each time he switch", async () => { - game.override.moveset([Moves.SPLASH]); - game.override.enemyMoveset([Moves.VOLT_SWITCH, Moves.VOLT_SWITCH, Moves.VOLT_SWITCH, Moves.VOLT_SWITCH]); - game.override.startingWave(5); - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + game.override + .enemyMoveset(Array(4).fill(Moves.VOLT_SWITCH)) + .startingWave(5); + await game.startBattle([Species.MIGHTYENA]); + let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - game.move.select(Moves.AERIAL_ACE); - console.log("===to new turn==="); + game.move.select(Moves.SPLASH); await game.toNextTurn(); + battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); + battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - game.move.select(Moves.AERIAL_ACE); - console.log("===to new turn==="); + game.move.select(Moves.SPLASH); await game.toNextTurn(); + battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-3); + battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); }, 200000); it("single - trainer should only trigger once whatever turn we are", async () => { - game.override.moveset([Moves.SPLASH]); - game.override.enemyMoveset(SPLASH_ONLY); game.override.startingWave(5); - await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); - let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + await game.startBattle([Species.MIGHTYENA]); + + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - game.move.select(Moves.AERIAL_ACE); - console.log("===to new turn==="); + game.move.select(Moves.SPLASH); await game.toNextTurn(); - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); - game.move.select(Moves.AERIAL_ACE); - console.log("===to new turn==="); - await game.toNextTurn(); - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); }, 20000); it("double - wild vs only 1 on player side", async () => { game.override.battleType("double"); - game.override.startingWave(3); await game.classicMode.runToSummon([Species.MIGHTYENA]); - await game.phaseInterceptor.to(CommandPhase, false); + await game.phaseInterceptor.to("CommandPhase", false); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1); @@ -315,7 +257,6 @@ describe("Abilities - Intimidate", () => { it("double - wild vs only 1 alive on player side", async () => { game.override.battleType("double"); - game.override.startingWave(3); await game.runToTitle(); game.onNextPrompt("TitlePhase", Mode.TITLE, () => { @@ -330,9 +271,11 @@ describe("Abilities - Intimidate", () => { await game.phaseInterceptor.run(EncounterPhase); - await game.phaseInterceptor.to(CommandPhase, false); + await game.phaseInterceptor.to("CommandPhase", false); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1); diff --git a/src/test/evolution.test.ts b/src/test/evolution.test.ts index f9123cf6d9a..5844e92ab8d 100644 --- a/src/test/evolution.test.ts +++ b/src/test/evolution.test.ts @@ -2,9 +2,10 @@ import { pokemonEvolutions, SpeciesFormEvolution, SpeciesWildEvolutionDelay } fr import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; +import * as Utils from "#app/utils"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { SPLASH_ONLY } from "./utils/testUtils"; describe("Evolution", () => { @@ -148,4 +149,28 @@ describe("Evolution", () => { expect(cyndaquil.hp).toBeGreaterThan(hpBefore); expect(cyndaquil.hp).toBeLessThan(cyndaquil.getMaxHp()); }, TIMEOUT); + + it("should handle rng-based split evolution", async () => { + /* this test checks to make sure that tandemaus will + * evolve into a 3 family maushold 25% of the time + * and a 4 family maushold the other 75% of the time + * This is done by using the getEvolution method in pokemon.ts + * getEvolution will give back the form that the pokemon can evolve into + * It does this by checking the pokemon conditions in pokemon-forms.ts + * For tandemaus, the conditions are random due to a randSeedInt(4) + * If the value is 0, it's a 3 family maushold, whereas if the value is + * 1, 2 or 3, it's a 4 family maushold + */ + await game.startBattle([Species.TANDEMAUS]); // starts us off with a tandemaus + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.level = 25; // tandemaus evolves at level 25 + vi.spyOn(Utils, "randSeedInt").mockReturnValue(0); // setting the random generator to be 0 to force a three family maushold + const threeForm = playerPokemon.getEvolution()!; + expect(threeForm.evoFormKey).toBe("three"); // as per pokemon-forms, the evoFormKey for 3 family mausholds is "three" + for (let f = 1; f < 4; f++) { + vi.spyOn(Utils, "randSeedInt").mockReturnValue(f); // setting the random generator to 1, 2 and 3 to force 4 family mausholds + const fourForm = playerPokemon.getEvolution()!; + expect(fourForm.evoFormKey).toBe(null); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is null + } + }, TIMEOUT); }); diff --git a/src/test/evolutions/evolutions.test.ts b/src/test/evolutions/evolutions.test.ts deleted file mode 100644 index 2028764115c..00000000000 --- a/src/test/evolutions/evolutions.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as Utils from "#app/utils"; -import { Species } from "#enums/species"; -import GameManager from "#test/utils/gameManager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -describe("Evolution tests", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - }); - - it("tandemaus evolution form test", async () => { - /* this test checks to make sure that tandemaus will - * evolve into a 3 family maushold 25% of the time - * and a 4 family maushold the other 75% of the time - * This is done by using the getEvolution method in pokemon.ts - * getEvolution will give back the form that the pokemon can evolve into - * It does this by checking the pokemon conditions in pokemon-forms.ts - * For tandemaus, the conditions are random due to a randSeedInt(4) - * If the value is 0, it's a 3 family maushold, whereas if the value is - * 1, 2 or 3, it's a 4 family maushold - */ - await game.startBattle([Species.TANDEMAUS]); // starts us off with a tandemaus - const playerPokemon = game.scene.getPlayerPokemon()!; - playerPokemon.level = 25; // tandemaus evolves at level 25 - vi.spyOn(Utils, "randSeedInt").mockReturnValue(0); // setting the random generator to be 0 to force a three family maushold - const threeForm = playerPokemon.getEvolution()!; - expect(threeForm.evoFormKey).toBe("three"); // as per pokemon-forms, the evoFormKey for 3 family mausholds is "three" - for (let f = 1; f < 4; f++) { - vi.spyOn(Utils, "randSeedInt").mockReturnValue(f); // setting the random generator to 1, 2 and 3 to force 4 family mausholds - const fourForm = playerPokemon.getEvolution()!; - expect(fourForm.evoFormKey).toBe(null); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is null - } - }, 5000); -}); diff --git a/src/test/utils/misc.test.ts b/src/test/misc.test.ts similarity index 90% rename from src/test/utils/misc.test.ts rename to src/test/misc.test.ts index d7c10144ead..1052a282a64 100644 --- a/src/test/utils/misc.test.ts +++ b/src/test/misc.test.ts @@ -30,7 +30,7 @@ describe("Test misc", () => { return response.json(); }).then(data => { spy(); // Call the spy function - expect(data).toEqual({"username":"greenlamp", "lastSessionSlot":0}); + expect(data).toEqual({ "username": "greenlamp", "lastSessionSlot": 0 }); }); expect(spy).toHaveBeenCalled(); }); @@ -43,7 +43,7 @@ describe("Test misc", () => { return response.json(); }).then(data => { spy(); // Call the spy function - expect(data).toEqual({"username":"greenlamp", "lastSessionSlot":0}); + expect(data).toEqual({ "username": "greenlamp", "lastSessionSlot": 0 }); }); expect(spy).toHaveBeenCalled(); }); @@ -54,7 +54,7 @@ describe("Test misc", () => { expect(response.ok).toBe(true); expect(response.status).toBe(200); - expect(data).toEqual({"username":"greenlamp", "lastSessionSlot":0}); + expect(data).toEqual({ "username": "greenlamp", "lastSessionSlot": 0 }); }); it("test apifetch mock sync", async () => { diff --git a/src/test/moves/spikes.test.ts b/src/test/moves/spikes.test.ts index c4096111c6f..05ea717ebbe 100644 --- a/src/test/moves/spikes.test.ts +++ b/src/test/moves/spikes.test.ts @@ -1,10 +1,10 @@ -import { CommandPhase } from "#app/phases/command-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { SPLASH_ONLY } from "../utils/testUtils"; describe("Moves - Spikes", () => { @@ -23,93 +23,61 @@ describe("Moves - Spikes", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.scene.battleStyle = 1; - game.override.battleType("single"); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.HYDRATION); - game.override.enemyPassiveAbility(Abilities.HYDRATION); - game.override.ability(Abilities.HYDRATION); - game.override.passiveAbility(Abilities.HYDRATION); - game.override.startingWave(3); - game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); - game.override.moveset([Moves.SPIKES, Moves.SPLASH, Moves.ROAR]); + game.override + .battleType("single") + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .ability(Abilities.BALL_FETCH) + .enemyMoveset(SPLASH_ONLY) + .moveset([Moves.SPIKES, Moves.SPLASH, Moves.ROAR]); }); - it("single - wild - stay on field - no damage", async () => { - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); - const initialHp = game.scene.getParty()[0].hp; - expect(game.scene.getParty()[0].hp).toBe(initialHp); + it("should not damage the team that set them", async () => { + await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + game.move.select(Moves.SPIKES); await game.toNextTurn(); + game.move.select(Moves.SPLASH); await game.toNextTurn(); - expect(game.scene.getParty()[0].hp).toBe(initialHp); - }, 20000); - - it("single - wild - take some damage", async () => { - // player set spikes on the field and switch back to back - // opponent do splash for 2 turns - // nobody should take damage - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, false); - - const initialHp = game.scene.getParty()[0].hp; - game.doSwitchPokemon(1); - await game.phaseInterceptor.run(CommandPhase); - await game.phaseInterceptor.to(CommandPhase, false); game.doSwitchPokemon(1); - await game.phaseInterceptor.run(CommandPhase); - await game.phaseInterceptor.to(CommandPhase, false); + await game.toNextTurn(); - expect(game.scene.getParty()[0].hp).toBe(initialHp); + game.doSwitchPokemon(1); + await game.toNextTurn(); + + const player = game.scene.getParty()[0]; + expect(player.hp).toBe(player.getMaxHp()); }, 20000); - it("trainer - wild - force switch opponent - should take damage", async () => { + it("should damage opposing pokemon that are forced to switch in", async () => { game.override.startingWave(5); - // player set spikes on the field and do splash for 3 turns - // opponent do splash for 4 turns - // nobody should take damage - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); - const initialHpOpponent = game.scene.currentBattle.enemyParty[1].hp; + await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + game.move.select(Moves.SPIKES); await game.toNextTurn(); + game.move.select(Moves.ROAR); await game.toNextTurn(); - expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(initialHpOpponent); + + const enemy = game.scene.getEnemyParty()[0]; + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }, 20000); - it("trainer - wild - force switch by himself opponent - should take damage", async () => { + it("should damage opposing pokemon that choose to switch in", async () => { game.override.startingWave(5); - game.override.startingLevel(5000); - game.override.enemySpecies(0); - // turn 1: player set spikes, opponent do splash - // turn 2: player do splash, opponent switch pokemon - // opponent pokemon should trigger spikes and lose HP - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); - const initialHpOpponent = game.scene.currentBattle.enemyParty[1].hp; + await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]); + game.move.select(Moves.SPIKES); await game.toNextTurn(); - game.forceOpponentToSwitch(); game.move.select(Moves.SPLASH); + game.forceOpponentToSwitch(); await game.toNextTurn(); - expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(initialHpOpponent); + + const enemy = game.scene.getEnemyParty()[0]; + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }, 20000); }); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index a9a71d16bf7..6724e05521c 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -139,7 +139,7 @@ export default class GameManager { this.scene.hpBarSpeed = 3; this.scene.enableTutorials = false; this.scene.gameData.gender = PlayerGender.MALE; // set initial player gender - + this.scene.battleStyle = this.settings.battleStyle; } /** diff --git a/src/test/utils/helpers/settingsHelper.ts b/src/test/utils/helpers/settingsHelper.ts index 76ffafdbe10..8fca2a34d47 100644 --- a/src/test/utils/helpers/settingsHelper.ts +++ b/src/test/utils/helpers/settingsHelper.ts @@ -1,16 +1,30 @@ import { PlayerGender } from "#app/enums/player-gender"; +import { BattleStyle } from "#app/enums/battle-style"; import { GameManagerHelper } from "./gameManagerHelper"; /** * Helper to handle settings for tests */ export class SettingsHelper extends GameManagerHelper { + private _battleStyle: BattleStyle = BattleStyle.SET; + + get battleStyle(): BattleStyle { + return this._battleStyle; + } + + /** + * Change the battle style to Switch or Set mode (tests default to {@linkcode BattleStyle.SET}) + * @param mode {@linkcode BattleStyle.SWITCH} or {@linkcode BattleStyle.SET} + */ + set battleStyle(mode: BattleStyle.SWITCH | BattleStyle.SET) { + this._battleStyle = mode; + } /** * Disable/Enable type hints settings * @param enable true to enabled, false to disabled */ - typeHints(enable: boolean) { + typeHints(enable: boolean): void { this.game.scene.typeHints = enable; this.log(`Type Hints ${enable? "enabled" : "disabled"}` ); } From e29f1fe5fd5f8aceb0a69ae1b25305ad2267aeaa Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sun, 1 Sep 2024 20:39:26 -0700 Subject: [PATCH 14/14] [Bug] Fix some trapping moves' interactions with Ghost-type Pokemon (#3936) * Fix secondary effects to trapping moves not applying to Ghost types * Docs for `isTrapped` * more `isTrapped` cleanup * Remove .js from imports --- src/data/battler-tags.ts | 23 ++++++++++- src/data/move.ts | 2 +- src/enums/battler-tag-type.ts | 1 + src/field/pokemon.ts | 26 +++++++++++- src/phases/command-phase.ts | 68 ++++++++++++++----------------- src/phases/enemy-command-phase.ts | 14 ++----- 6 files changed, 82 insertions(+), 52 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 38db36e86f6..2e280634d5d 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -212,7 +212,7 @@ export class TrappedTag extends BattlerTag { canAdd(pokemon: Pokemon): boolean { const isGhost = pokemon.isOfType(Type.GHOST); - const isTrapped = pokemon.getTag(BattlerTagType.TRAPPED); + const isTrapped = pokemon.getTag(TrappedTag); return !isTrapped && !isGhost; } @@ -245,6 +245,23 @@ export class TrappedTag extends BattlerTag { } } +/** + * BattlerTag implementing No Retreat's trapping effect. + * This is treated separately from other trapping effects to prevent + * Ghost-type Pokemon from being able to reuse the move. + * @extends TrappedTag + */ +class NoRetreatTag extends TrappedTag { + constructor(sourceId: number) { + super(BattlerTagType.NO_RETREAT, BattlerTagLapseType.CUSTOM, 0, Moves.NO_RETREAT, sourceId); + } + + /** overrides {@linkcode TrappedTag.apply}, removing the Ghost-type condition */ + canAdd(pokemon: Pokemon): boolean { + return !pokemon.getTag(TrappedTag); + } +} + /** * BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Flinch Flinch} status condition */ @@ -864,7 +881,7 @@ export abstract class DamagingTrapTag extends TrappedTag { } canAdd(pokemon: Pokemon): boolean { - return !pokemon.isOfType(Type.GHOST) && !pokemon.findTag(t => t instanceof DamagingTrapTag); + return !pokemon.getTag(TrappedTag); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -1883,6 +1900,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new DrowsyTag(); case BattlerTagType.TRAPPED: return new TrappedTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); + case BattlerTagType.NO_RETREAT: + return new NoRetreatTag(sourceId); case BattlerTagType.BIND: return new BindTag(turnCount, sourceId); case BattlerTagType.WRAP: diff --git a/src/data/move.ts b/src/data/move.ts index 95d306a61ba..3f87fc68b89 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8553,7 +8553,7 @@ export function initMoves() { .partial(), new SelfStatusMove(Moves.NO_RETREAT, Type.FIGHTING, -1, 5, -1, 0, 8) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true) - .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, true, false, 1) + .attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false) .condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8) .attr(StatChangeAttr, BattleStat.SPD, -1) diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 73580a107b2..20ceb1b331f 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -71,4 +71,5 @@ export enum BattlerTagType { BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING", SHELL_TRAP = "SHELL_TRAP", DRAGON_CHEER = "DRAGON_CHEER", + NO_RETREAT = "NO_RETREAT", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index b1fcd512e35..d8acddecedf 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -18,11 +18,11 @@ import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { BattleStat } from "../data/battle-stat"; -import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; 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, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr } from "../data/ability"; +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, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1210,6 +1210,28 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return !!this.getTag(GroundedTag) || (!this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.MAGNET_RISEN) && !this.getTag(SemiInvulnerableTag)); } + /** + * Determines whether this Pokemon is prevented from running or switching due + * to effects from moves and/or abilities. + * @param trappedAbMessages `string[]` If defined, ability trigger messages + * (e.g. from Shadow Tag) are forwarded through this array. + * @param simulated `boolean` if `true`, applies abilities via simulated calls. + * @returns + */ + isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean { + if (this.isOfType(Type.GHOST)) { + return false; + } + + const trappedByAbility = new Utils.BooleanHolder(false); + + this.scene.getEnemyField()!.forEach(enemyPokemon => + applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trappedByAbility, this, trappedAbMessages, simulated) + ); + + return (trappedByAbility.value || !!this.getTag(TrappedTag)); + } + /** * Calculates the type of a move when used by this Pokemon after * type-changing move and ability attributes have applied. diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 68ede826d95..9681a6eeee8 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -1,21 +1,18 @@ -import BattleScene from "#app/battle-scene.js"; -import { TurnCommand, BattleType } from "#app/battle.js"; -import { applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "#app/data/ability.js"; -import { TrappedTag, EncoreTag } from "#app/data/battler-tags.js"; -import { MoveTargetSet, getMoveTargets } from "#app/data/move.js"; -import { speciesStarters } from "#app/data/pokemon-species.js"; -import { Type } from "#app/data/type.js"; -import { Abilities } from "#app/enums/abilities.js"; -import { BattlerTagType } from "#app/enums/battler-tag-type.js"; -import { Biome } from "#app/enums/biome.js"; -import { Moves } from "#app/enums/moves.js"; -import { PokeballType } from "#app/enums/pokeball.js"; -import { FieldPosition, PlayerPokemon } from "#app/field/pokemon.js"; -import { getPokemonNameWithAffix } from "#app/messages.js"; -import { Command } from "#app/ui/command-ui-handler.js"; -import { Mode } from "#app/ui/ui.js"; +import BattleScene from "#app/battle-scene"; +import { TurnCommand, BattleType } from "#app/battle"; +import { TrappedTag, EncoreTag } from "#app/data/battler-tags"; +import { MoveTargetSet, getMoveTargets } from "#app/data/move"; +import { speciesStarters } from "#app/data/pokemon-species"; +import { Abilities } from "#app/enums/abilities"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { Biome } from "#app/enums/biome"; +import { Moves } from "#app/enums/moves"; +import { PokeballType } from "#app/enums/pokeball"; +import { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { Command } from "#app/ui/command-ui-handler"; +import { Mode } from "#app/ui/ui"; import i18next from "i18next"; -import * as Utils from "#app/utils.js"; import { FieldPhase } from "./field-phase"; import { SelectTargetPhase } from "./select-target-phase"; @@ -77,7 +74,6 @@ export class CommandPhase extends FieldPhase { handleCommand(command: Command, cursor: integer, ...args: any[]): boolean { const playerPokemon = this.scene.getPlayerField()[this.fieldIndex]; - const enemyField = this.scene.getEnemyField(); let success: boolean; switch (command) { @@ -184,14 +180,9 @@ export class CommandPhase extends FieldPhase { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); } else { - const trapTag = playerPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag; - const trapped = new Utils.BooleanHolder(false); const batonPass = isSwitch && args[0] as boolean; const trappedAbMessages: string[] = []; - if (!batonPass) { - enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped, playerPokemon, trappedAbMessages, true)); - } - if (batonPass || (!trapTag && !trapped.value)) { + if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; @@ -199,14 +190,27 @@ export class CommandPhase extends FieldPhase { if (!isSwitch && this.fieldIndex) { this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; } - } else if (trapTag) { - if (trapTag.sourceMove === Moves.INGRAIN && trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId)?.isOfType(Type.GHOST)) { - success = true; + } else if (trappedAbMessages.length > 0) { + if (!isSwitch) { + this.scene.ui.setMode(Mode.MESSAGE); + } + this.scene.ui.showText(trappedAbMessages[0], null, () => { + this.scene.ui.showText("", 0); + if (!isSwitch) { + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + } + }, null, true); + } else { + const trapTag = playerPokemon.getTag(TrappedTag); + + // trapTag should be defined at this point, but just in case... + if (!trapTag) { this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; break; } + if (!isSwitch) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); @@ -224,16 +228,6 @@ export class CommandPhase extends FieldPhase { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); } }, null, true); - } else if (trapped.value && trappedAbMessages.length > 0) { - if (!isSwitch) { - this.scene.ui.setMode(Mode.MESSAGE); - } - this.scene.ui.showText(trappedAbMessages[0], null, () => { - this.scene.ui.showText("", 0); - if (!isSwitch) { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); - } - }, null, true); } } break; diff --git a/src/phases/enemy-command-phase.ts b/src/phases/enemy-command-phase.ts index 5277b2666c7..d9bb08d6fae 100644 --- a/src/phases/enemy-command-phase.ts +++ b/src/phases/enemy-command-phase.ts @@ -1,9 +1,6 @@ -import BattleScene from "#app/battle-scene.js"; -import { BattlerIndex } from "#app/battle.js"; -import { applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "#app/data/ability.js"; -import { TrappedTag } from "#app/data/battler-tags.js"; -import { Command } from "#app/ui/command-ui-handler.js"; -import * as Utils from "#app/utils.js"; +import BattleScene from "#app/battle-scene"; +import { BattlerIndex } from "#app/battle"; +import { Command } from "#app/ui/command-ui-handler"; import { FieldPhase } from "./field-phase"; /** @@ -45,10 +42,7 @@ export class EnemyCommandPhase extends FieldPhase { if (trainer && !enemyPokemon.getMoveQueue().length) { const opponents = enemyPokemon.getOpponents(); - const trapTag = enemyPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag; - const trapped = new Utils.BooleanHolder(false); - opponents.forEach(playerPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, playerPokemon, trapped, enemyPokemon, [""], true)); - if (!trapTag && !trapped.value) { + if (!enemyPokemon.isTrapped()) { const partyMemberScores = trainer.getPartyMemberMatchupScores(enemyPokemon.trainerSlot, true); if (partyMemberScores.length) {