From 6e20f8fba79fad3a83bca385538e6109576ae2d0 Mon Sep 17 00:00:00 2001 From: Temps Ray Date: Fri, 13 Sep 2024 18:58:40 -0400 Subject: [PATCH 1/3] Implement Autotomize --- src/data/ability.ts | 4 +++ src/data/battler-tags.ts | 30 +++++++++++++++++ src/data/move.ts | 2 +- src/enums/battler-tag-type.ts | 1 + src/field/pokemon.ts | 18 ++++++++-- src/locales/en/battler-tags.json | 3 +- src/test/moves/autotomize.test.ts | 55 +++++++++++++++++++++++++++++++ 7 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 src/test/moves/autotomize.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index b38d9ea0fb3..4275b317d1c 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4214,6 +4214,10 @@ export class ReduceBerryUseThresholdAbAttr extends AbAttr { } } +/** + * Ability attribute used for abilites that change the ability owner's weight + * Used for Heavy Metal (doubling weight) and Light Metal (halving weight) + */ export class WeightMultiplierAbAttr extends AbAttr { private multiplier: integer; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index a43fa58ba1d..f908af69193 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2217,6 +2217,36 @@ export class TarShotTag extends BattlerTag { } } +/** + * Battler Tag that keeps track of how many times the user has Autotomized + * Each count of Autotomization reduces the weight by 100kg + */ +export class AutotomizedTag extends BattlerTag { + public autotomizeCount: number = 0; + constructor(sourceMove: Moves = Moves.NONE) { + super(BattlerTagType.AUTOTOMIZED, BattlerTagLapseType.CUSTOM, 1, sourceMove); + } + + /** + * Adds an autotmize count to the Pokemon. Each stack reduces weight by 100kg + * If the Pokemon is over 0.1kg it also displays a message. + * @param pokemon The Pokemon that is being autotomized + */ + onAdd(pokemon: Pokemon): void { + const minWeight = 0.1; + if (pokemon.getWeight() > minWeight) { + pokemon.scene.queueMessage(i18next.t("battlerTags:autotomizeOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) + })); + } + this.autotomizeCount += 1; + } + + onOverlap(pokemon: Pokemon): void { + this.onAdd(pokemon); + } +} + export class SubstituteTag extends BattlerTag { /** The substitute's remaining HP. If HP is depleted, the Substitute fades. */ public hp: number; diff --git a/src/data/move.ts b/src/data/move.ts index 1d1a788e768..e590a6c3e23 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8079,7 +8079,7 @@ export function initMoves() { .attr(MovePowerMultiplierAttr, (user, target, move) => target.status && (target.status.effect === StatusEffect.POISON || target.status.effect === StatusEffect.TOXIC) ? 2 : 1), new SelfStatusMove(Moves.AUTOTOMIZE, Type.STEEL, -1, 15, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true) - .partial(), + .attr(AddBattlerTagAttr, BattlerTagType.AUTOTOMIZED, true), new SelfStatusMove(Moves.RAGE_POWDER, Type.BUG, -1, 20, -1, 2, 5) .powderMove() .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true), diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 657f0d47375..4169a14724e 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -79,4 +79,5 @@ export enum BattlerTagType { TAR_SHOT = "TAR_SHOT", BURNED_UP = "BURNED_UP", DOUBLE_SHOCKED = "DOUBLE_SHOCKED", + AUTOTOMIZED = "AUTOTOMIZED", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c648ff485b7..825415a4927 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -17,7 +17,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; 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 { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags"; +import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability"; @@ -1342,11 +1342,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } + /** + * Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first + * and then multiplicative modifiers happening after (Heavy Metal and Light Metal) + * @returns the kg of the Pokemon (minimum of 0.1) + */ getWeight(): number { - const weight = new Utils.NumberHolder(this.species.weight); + const autotomizedTag = this.getTag(AutotomizedTag); + let weightRemoved = 0; + if (autotomizedTag !== null) { + weightRemoved = 100 * autotomizedTag.autotomizeCount; + } + const minWeight = 0.1; + const weight = new Utils.NumberHolder(this.species.weight - weightRemoved); + // This will trigger the ability overlay so only call this function when necessary applyAbAttrs(WeightMultiplierAbAttr, this, null, false, weight); - return weight.value; + return Math.max(minWeight, weight.value); } /** diff --git a/src/locales/en/battler-tags.json b/src/locales/en/battler-tags.json index b31826b0244..6b513f3a832 100644 --- a/src/locales/en/battler-tags.json +++ b/src/locales/en/battler-tags.json @@ -73,5 +73,6 @@ "tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!", "substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!", "substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!", - "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!" + "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!", + "autotomizeOnAdd": "{{pokemonNameWIthAffix}} became nimble!" } diff --git a/src/test/moves/autotomize.test.ts b/src/test/moves/autotomize.test.ts new file mode 100644 index 00000000000..c3439e4228a --- /dev/null +++ b/src/test/moves/autotomize.test.ts @@ -0,0 +1,55 @@ +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, it, expect } from "vitest"; + +describe("Moves - Autotomize", () => { + 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 + .moveset([Moves.AUTOTOMIZE]) + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("Autotomize should reduce weight", async () => { + const baseDracozoltWeight = 190; + const oneAutotomizeDracozoltWeight = 90; + const twoAutotomizeDracozoltWeight = 0.1; + const threeAutotomizeDracozoltWeight = 0.1; + const playerPokemon = game.scene.getPlayerPokemon()!; + + await game.classicMode.startBattle([Species.DRACOZOLT]); + expect(playerPokemon.getWeight()).toBe(baseDracozoltWeight); + game.move.select(Moves.AUTOTOMIZE); + // expect a queued message here + expect(playerPokemon.getWeight()).toBe(oneAutotomizeDracozoltWeight); + await game.toNextTurn(); + + game.move.select(Moves.AUTOTOMIZE); + //expect a queued message here + expect(playerPokemon.getWeight()).toBe(twoAutotomizeDracozoltWeight); + await game.toNextTurn(); + + game.move.select(Moves.AUTOTOMIZE); + // expect no queued message here + expect(playerPokemon.getWeight()).toBe(threeAutotomizeDracozoltWeight); + }, TIMEOUT); +}); From bdd0850e6f64f6875abec52a599eb2636ac5cd72 Mon Sep 17 00:00:00 2001 From: Temps Ray Date: Tue, 17 Sep 2024 23:21:55 -0400 Subject: [PATCH 2/3] Another linting --- src/data/battler-tags.ts | 2 ++ src/field/pokemon.ts | 2 +- src/phases/form-change-phase.ts | 2 ++ src/phases/quiet-form-change-phase.ts | 2 ++ src/test/moves/autotomize.test.ts | 31 +++++++++++++++++++++++++-- 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index cf1a60ae868..f5b80a06c18 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2518,6 +2518,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new GorillaTacticsTag(); case BattlerTagType.SUBSTITUTE: return new SubstituteTag(sourceMove, sourceId); + case BattlerTagType.AUTOTOMIZED: + return new AutotomizedTag(); case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON: return new MysteryEncounterPostSummonTag(); case BattlerTagType.NONE: diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index aa9f47b8001..4eeab536bc5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1401,7 +1401,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getWeight(): number { const autotomizedTag = this.getTag(AutotomizedTag); let weightRemoved = 0; - if (autotomizedTag !== null) { + if (autotomizedTag !== null && autotomizedTag !== undefined) { weightRemoved = 100 * autotomizedTag.autotomizeCount; } const minWeight = 0.1; diff --git a/src/phases/form-change-phase.ts b/src/phases/form-change-phase.ts index 33c1f8e8cef..1f18457146d 100644 --- a/src/phases/form-change-phase.ts +++ b/src/phases/form-change-phase.ts @@ -9,6 +9,7 @@ import PartyUiHandler from "../ui/party-ui-handler"; import { getPokemonNameWithAffix } from "../messages"; import { EndEvolutionPhase } from "./end-evolution-phase"; import { EvolutionPhase } from "./evolution-phase"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; export class FormChangePhase extends EvolutionPhase { private formChange: SpeciesFormChange; @@ -157,6 +158,7 @@ export class FormChangePhase extends EvolutionPhase { } end(): void { + this.pokemon.findAndRemoveTags(t => t.tagType === BattlerTagType.AUTOTOMIZED); if (this.modal) { this.scene.ui.revertMode().then(() => { if (this.scene.ui.getMode() === Mode.PARTY) { diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index dde500e156a..c28cc28b592 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -3,6 +3,7 @@ import { SemiInvulnerableTag } from "#app/data/battler-tags"; import { SpeciesFormChange, getSpeciesFormChangeMessage } from "#app/data/pokemon-forms"; import { getTypeRgb } from "#app/data/type"; import { BattleSpec } from "#app/enums/battle-spec"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; import Pokemon, { EnemyPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { BattlePhase } from "./battle-phase"; @@ -113,6 +114,7 @@ export class QuietFormChangePhase extends BattlePhase { } end(): void { + this.pokemon.findAndRemoveTags(t => t.tagType === BattlerTagType.AUTOTOMIZED); if (this.pokemon.scene?.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && this.pokemon instanceof EnemyPokemon) { this.scene.playBgm(); this.scene.unshiftPhase(new PokemonHealPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getMaxHp(), null, false, false, false, true)); diff --git a/src/test/moves/autotomize.test.ts b/src/test/moves/autotomize.test.ts index c3439e4228a..36f90216a0a 100644 --- a/src/test/moves/autotomize.test.ts +++ b/src/test/moves/autotomize.test.ts @@ -23,7 +23,7 @@ describe("Moves - Autotomize", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([Moves.AUTOTOMIZE]) + .moveset([Moves.AUTOTOMIZE, Moves.KINGS_SHIELD, Moves.FALSE_SWIPE]) .battleType("single") .enemyAbility(Abilities.BALL_FETCH) .enemyMoveset(Moves.SPLASH); @@ -34,9 +34,9 @@ describe("Moves - Autotomize", () => { const oneAutotomizeDracozoltWeight = 90; const twoAutotomizeDracozoltWeight = 0.1; const threeAutotomizeDracozoltWeight = 0.1; - const playerPokemon = game.scene.getPlayerPokemon()!; await game.classicMode.startBattle([Species.DRACOZOLT]); + const playerPokemon = game.scene.getPlayerPokemon()!; expect(playerPokemon.getWeight()).toBe(baseDracozoltWeight); game.move.select(Moves.AUTOTOMIZE); // expect a queued message here @@ -52,4 +52,31 @@ describe("Moves - Autotomize", () => { // expect no queued message here expect(playerPokemon.getWeight()).toBe(threeAutotomizeDracozoltWeight); }, TIMEOUT); + + it("Changing forms should revert weight", async () => { + const baseAegislashWeight = 53; + const autotomizeAegislashWeight = 0.1; + + await game.classicMode.startBattle([Species.AEGISLASH]); + const playerPokemon = game.scene.getPlayerPokemon()!; + + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + game.move.select(Moves.AUTOTOMIZE); + expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + await game.toNextTurn(); + + game.move.select(Moves.KINGS_SHIELD); + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + await game.toNextTurn(); + + game.move.select(Moves.AUTOTOMIZE); + expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + + game.move.select(Moves.FALSE_SWIPE); + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + await game.toNextTurn(); + + game.move.select(Moves.AUTOTOMIZE); + expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + }, TIMEOUT); }); From 17e5ef789dce525e79fac48fa0fcb06657d41eba Mon Sep 17 00:00:00 2001 From: Temps Ray Date: Tue, 17 Sep 2024 23:40:03 -0400 Subject: [PATCH 3/3] Fix unit tests --- src/data/battler-tags.ts | 4 ++-- src/field/pokemon.ts | 2 +- src/test/moves/autotomize.test.ts | 30 +++++++++++++++++++++++------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index f5b80a06c18..9e910430c09 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2207,12 +2207,12 @@ export class TarShotTag extends BattlerTag { */ export class AutotomizedTag extends BattlerTag { public autotomizeCount: number = 0; - constructor(sourceMove: Moves = Moves.NONE) { + constructor(sourceMove: Moves = Moves.AUTOTOMIZE) { super(BattlerTagType.AUTOTOMIZED, BattlerTagLapseType.CUSTOM, 1, sourceMove); } /** - * Adds an autotmize count to the Pokemon. Each stack reduces weight by 100kg + * Adds an autotomize count to the Pokemon. Each stack reduces weight by 100kg * If the Pokemon is over 0.1kg it also displays a message. * @param pokemon The Pokemon that is being autotomized */ diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 4eeab536bc5..9a9156e327d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1401,7 +1401,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getWeight(): number { const autotomizedTag = this.getTag(AutotomizedTag); let weightRemoved = 0; - if (autotomizedTag !== null && autotomizedTag !== undefined) { + if (!Utils.isNullOrUndefined(autotomizedTag)) { weightRemoved = 100 * autotomizedTag.autotomizeCount; } const minWeight = 0.1; diff --git a/src/test/moves/autotomize.test.ts b/src/test/moves/autotomize.test.ts index 36f90216a0a..4da246736f9 100644 --- a/src/test/moves/autotomize.test.ts +++ b/src/test/moves/autotomize.test.ts @@ -39,17 +39,16 @@ describe("Moves - Autotomize", () => { const playerPokemon = game.scene.getPlayerPokemon()!; expect(playerPokemon.getWeight()).toBe(baseDracozoltWeight); game.move.select(Moves.AUTOTOMIZE); - // expect a queued message here + await game.toNextTurn(); expect(playerPokemon.getWeight()).toBe(oneAutotomizeDracozoltWeight); - await game.toNextTurn(); game.move.select(Moves.AUTOTOMIZE); - //expect a queued message here + await game.toNextTurn(); expect(playerPokemon.getWeight()).toBe(twoAutotomizeDracozoltWeight); - await game.toNextTurn(); + game.move.select(Moves.AUTOTOMIZE); - // expect no queued message here + await game.toNextTurn(); expect(playerPokemon.getWeight()).toBe(threeAutotomizeDracozoltWeight); }, TIMEOUT); @@ -62,21 +61,38 @@ describe("Moves - Autotomize", () => { expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); + game.move.select(Moves.FALSE_SWIPE); await game.toNextTurn(); game.move.select(Moves.KINGS_SHIELD); - expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); game.move.select(Moves.FALSE_SWIPE); - expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(baseAegislashWeight); game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); expect(playerPokemon.getWeight()).toBe(autotomizeAegislashWeight); }, TIMEOUT); + + it("Autotomize should interact with light metal correctly", async () => { + const baseLightGroudonWeight = 475; + const autotomizeLightGroudonWeight = 425; + game.override.ability(Abilities.LIGHT_METAL); + await game.classicMode.startBattle([Species.GROUDON]); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getWeight()).toBe(baseLightGroudonWeight); + game.move.select(Moves.AUTOTOMIZE); + await game.toNextTurn(); + expect(playerPokemon.getWeight()).toBe(autotomizeLightGroudonWeight); + }, TIMEOUT); });