From 4389bff5d02331c06e381a62135a27d3e0f1e8bd Mon Sep 17 00:00:00 2001 From: flx-sta <50131232+flx-sta@users.noreply.github.com> Date: Tue, 17 Sep 2024 19:14:41 -0700 Subject: [PATCH 1/3] [Bug] Fix stat-protection-attribute not taking inverts (e.g. Contrary) into account (#4031) * add generic types to`Pokemon.getAbilityAttrs()` * add invert check to `ProtectStatAbAttr.apply...` This makes sure that a stat is only protected if no other ability inverts the change. E.g. `Contrary` inverts any decrease to an increase * migrate contrary.test.ts to game.classicMode * move `StatStageChangeMultiplierAbAttr` resolve above `ProtectStatAbAttr` The effect of StatStageChangeMultiplierAbAttr is now applied before resolving any ProtectStatAbAttr. Thus the stage (level) of the BattleStat change was properly altered at the time of resolving the protection * revert ability.ts changes * add automated tests for `Clear Body` + `Contrary` * StateStageChangePhase replace ~~`IntegerHolder`~~ with `NumberHolder` Update Utils import and replace all occurcences of `Utils.` * contrary.test.ts: remove `js` import --- src/data/ability.ts | 18 +++++++++++-- src/field/pokemon.ts | 8 +++--- src/phases/stat-stage-change-phase.ts | 20 +++++++-------- src/test/abilities/contrary.test.ts | 37 ++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 944ee10244a..0edbc172ad5 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2625,7 +2625,11 @@ export class PreStatStageChangeAbAttr extends AbAttr { } } +/** + * Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities + */ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { + /** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */ private protectedStat?: BattleStat; constructor(protectedStat?: BattleStat) { @@ -2634,7 +2638,17 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { this.protectedStat = protectedStat; } - applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean { + /** + * Apply the {@linkcode ProtectedStatAbAttr} to an interaction + * @param _pokemon + * @param _passive + * @param simulated + * @param stat the {@linkcode BattleStat} being affected + * @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true if the stat is protected + * @param _args + * @returns true if the stat is protected, false otherwise + */ + applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean { if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) { cancelled.value = true; return true; @@ -3757,7 +3771,7 @@ export class StatStageChangeMultiplierAbAttr extends AbAttr { this.multiplier = multiplier; } - apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { (args[0] as Utils.IntegerHolder).value *= this.multiplier; return true; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ed36bcfe4b3..7593cb870b4 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1273,13 +1273,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param attrType {@linkcode AbAttr} The ability attribute to check for. * @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active * @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects - * @returns {AbAttr[]} A list of all the ability attributes on this ability. + * @returns A list of all the ability attributes on this ability. */ - getAbilityAttrs(attrType: { new(...args: any[]): AbAttr }, canApply: boolean = true, ignoreOverride?: boolean): AbAttr[] { - const abilityAttrs: AbAttr[] = []; + getAbilityAttrs(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride?: boolean): T[] { + const abilityAttrs: T[] = []; if (!canApply || this.canApplyAbility()) { - abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); + abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); } if (!canApply || this.canApplyAbility(true)) { diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 55faaa29903..4418c38c849 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -6,7 +6,7 @@ import Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { ResetNegativeStatStageModifier } from "#app/modifier/modifier"; import { handleTutorial, Tutorial } from "#app/tutorial"; -import * as Utils from "#app/utils"; +import { NumberHolder, BooleanHolder } from "#app/utils"; import i18next from "i18next"; import { PokemonPhase } from "./pokemon-phase"; import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; @@ -42,17 +42,23 @@ export class StatStageChangePhase extends PokemonPhase { return this.end(); } + const stages = new NumberHolder(this.stages); + + if (!this.ignoreAbilities) { + applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages); + } + let simulate = false; const filteredStats = this.stats.filter(stat => { - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); - if (!this.selfTarget && this.stages < 0) { + if (!this.selfTarget && stages.value < 0) { // TODO: Include simulate boolean when tag applications can be simulated this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); } - if (!cancelled.value && !this.selfTarget && this.stages < 0) { + if (!cancelled.value && !this.selfTarget && stages.value < 0) { applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); } @@ -64,12 +70,6 @@ export class StatStageChangePhase extends PokemonPhase { return !cancelled.value; }); - const stages = new Utils.IntegerHolder(this.stages); - - if (!this.ignoreAbilities) { - applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages); - } - const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s)); this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels); diff --git a/src/test/abilities/contrary.test.ts b/src/test/abilities/contrary.test.ts index 95a209395dc..5221e821e70 100644 --- a/src/test/abilities/contrary.test.ts +++ b/src/test/abilities/contrary.test.ts @@ -31,7 +31,7 @@ describe("Abilities - Contrary", () => { }); it("should invert stat changes when applied", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.SLOWBRO ]); @@ -39,4 +39,39 @@ describe("Abilities - Contrary", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); }, 20000); + + describe("With Clear Body", () => { + it("should apply positive effects", async () => { + game.override + .enemyPassiveAbility(Abilities.CLEAR_BODY) + .moveset([Moves.TAIL_WHIP]); + await game.classicMode.startBattle([Species.SLOWBRO]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + + game.move.select(Moves.TAIL_WHIP); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(1); + }); + + it("should block negative effects", async () => { + game.override + .enemyPassiveAbility(Abilities.CLEAR_BODY) + .enemyMoveset([Moves.HOWL, Moves.HOWL, Moves.HOWL, Moves.HOWL]) + .moveset([Moves.SPLASH]); + await game.classicMode.startBattle([Species.SLOWBRO]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + }); + }); }); From 106ed6b27b849d316340c49f9a1bd18ef70c919a Mon Sep 17 00:00:00 2001 From: podar <1999688+podarsmarty@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:15:47 -0500 Subject: [PATCH 2/3] [Bug] Using default animation for errors that occur. (#4266) * Using default animation for errors that occur. * Renaming function to make it clear that logging happens * Updating logging for missing animations * Missed committing linter changes * Update src/data/battle-anims.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/battle-anims.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/battle-anims.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> --- src/data/battle-anims.ts | 36 ++++++++++++++++++++++++++++++++---- src/utils.ts | 10 ++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index f07d6cb2409..d972e48df7c 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -488,14 +488,14 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { moveAnims.set(move, null); const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP; - const moveName = Moves[move].toLowerCase().replace(/\_/g, "-"); + const fetchAnimAndResolve = (move: Moves) => { - scene.cachedFetch(`./battle-anims/${moveName}.json`) + scene.cachedFetch(`./battle-anims/${Utils.animationFileName(move)}.json`) .then(response => { const contentType = response.headers.get("content-type"); if (!response.ok || contentType?.indexOf("application/json") === -1) { - console.error(`Could not load animation file for move '${moveName}'`, response.status, response.statusText); - populateMoveAnim(move, moveAnims.get(defaultMoveAnim)); + useDefaultAnim(move, defaultMoveAnim); + logMissingMoveAnim(move, response.status, response.statusText); return resolve(); } return response.json(); @@ -515,6 +515,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { resolve(); } + }) + .catch(error => { + useDefaultAnim(move, defaultMoveAnim); + logMissingMoveAnim(move, error); + return resolve(); }); }; fetchAnimAndResolve(move); @@ -522,6 +527,29 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { }); } +/** + * Populates the default animation for the given move. + * + * @param move the move to populate an animation for + * @param defaultMoveAnim the move to use as the default animation + */ +function useDefaultAnim(move: Moves, defaultMoveAnim: Moves) { + populateMoveAnim(move, moveAnims.get(defaultMoveAnim)); +} + +/** + * Helper method for printing a warning to the console when a move animation is missing. + * + * @param move the move to populate an animation for + * @param optionalParams parameters to add to the error logging + * + * @remarks use {@linkcode useDefaultAnim} to use a default animation + */ +function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) { + const moveName = Utils.animationFileName(move); + console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams); +} + /** * Fetches animation configs to be used in a Mystery Encounter * @param scene diff --git a/src/utils.ts b/src/utils.ts index a8206bf4dcf..e526d086316 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { MoneyFormat } from "#enums/money-format"; +import { Moves } from "#enums/moves"; import i18next from "i18next"; export const MissingTextureKey = "__MISSING"; @@ -628,3 +629,12 @@ export function getLocalizedSpriteKey(baseKey: string) { export function isBetween(num: number, min: number, max: number): boolean { return num >= min && num <= max; } + +/** + * Helper method to return the animation filename for a given move + * + * @param move the move for which the animation filename is needed + */ +export function animationFileName(move: Moves): string { + return Moves[move].toLowerCase().replace(/\_/g, "-"); +} From e386504977db353496efaefd6044e2355d7c9117 Mon Sep 17 00:00:00 2001 From: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Date: Wed, 18 Sep 2024 04:26:47 +0200 Subject: [PATCH 3/3] [BUG][Beta] Fix English Status Symbols (#4293) * Have English Status Symbols show up again :) * It now uses the function instead --------- Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> --- src/ui/battle-info.ts | 2 +- src/ui/move-info-overlay.ts | 4 ++-- src/ui/party-ui-handler.ts | 2 +- src/ui/summary-ui-handler.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 4fac75932ae..1d598f6ac4e 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -162,7 +162,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.splicedIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains); this.add(this.splicedIcon); - this.statusIndicator = this.scene.add.sprite(0, 0, `statuses_${i18next.resolvedLanguage}`); + this.statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses")); this.statusIndicator.setName("icon_status"); this.statusIndicator.setVisible(false); this.statusIndicator.setOrigin(0, 0); diff --git a/src/ui/move-info-overlay.ts b/src/ui/move-info-overlay.ts index 77010f84528..a99e4c81e27 100644 --- a/src/ui/move-info-overlay.ts +++ b/src/ui/move-info-overlay.ts @@ -91,7 +91,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem valuesBg.setOrigin(0, 0); this.val.add(valuesBg); - this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, `types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, "unknown"); + this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, Utils.getLocalizedSpriteKey("types"), "unknown"); this.typ.setScale(0.8); this.val.add(this.typ); @@ -138,7 +138,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem this.pow.setText(move.power >= 0 ? move.power.toString() : "---"); this.acc.setText(move.accuracy >= 0 ? move.accuracy.toString() : "---"); this.pp.setText(move.pp >= 0 ? move.pp.toString() : "---"); - this.typ.setTexture(`types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, Type[move.type].toLowerCase()); + this.typ.setTexture(Utils.getLocalizedSpriteKey("types"), Type[move.type].toLowerCase()); this.cat.setFrame(MoveCategory[move.category].toLowerCase()); this.desc.setText(move?.effect || ""); diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index b5c9e76bf8c..8c777350964 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -1272,7 +1272,7 @@ class PartySlot extends Phaser.GameObjects.Container { } if (this.pokemon.status) { - const statusIndicator = this.scene.add.sprite(0, 0, `statuses_${i18next.resolvedLanguage}`); + const statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses")); statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase()); statusIndicator.setOrigin(0, 0); statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 0892bf8ab1b..fb9f1561447 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -214,7 +214,7 @@ export default class SummaryUiHandler extends UiHandler { this.statusContainer.add(statusLabel); - this.status = this.scene.add.sprite(91, 4, `statuses_${i18next.resolvedLanguage}`); + this.status = this.scene.add.sprite(91, 4, Utils.getLocalizedSpriteKey("statuses")); this.status.setOrigin(0.5, 0); this.statusContainer.add(this.status);