From b7eb95b7614d0df1674f4bd9e000c0e1cd80b04e Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Sat, 12 Oct 2024 11:22:26 -0400 Subject: [PATCH 1/8] [Test] Fix several flaky tests (#4639) --- src/test/moves/safeguard.test.ts | 2 +- .../an-offer-you-cant-refuse-encounter.test.ts | 11 +++++++---- .../encounters/berries-abound-encounter.test.ts | 12 ++++++++---- .../the-pokemon-salesman-encounter.test.ts | 4 ++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/test/moves/safeguard.test.ts b/src/test/moves/safeguard.test.ts index c180ff338a5..6505162fd04 100644 --- a/src/test/moves/safeguard.test.ts +++ b/src/test/moves/safeguard.test.ts @@ -33,7 +33,7 @@ describe("Moves - Safeguard", () => { .enemyLevel(5) .starterSpecies(Species.DRATINI) .moveset([ Moves.NUZZLE, Moves.SPORE, Moves.YAWN, Moves.SPLASH ]) - .ability(Abilities.BALL_FETCH); + .ability(Abilities.UNNERVE); // Stop wild Pokemon from potentially eating Lum Berry }); it("protects from damaging moves with additional effects", async () => { diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index 9883b4332b9..0585b4ce72b 100644 --- a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -18,6 +18,7 @@ import { Moves } from "#enums/moves"; import { ShinyRateBoosterModifier } from "#app/modifier/modifier"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import i18next from "i18next"; +import { Abilities } from "#enums/abilities"; const namespace = "mysteryEncounters/anOfferYouCantRefuse"; /** Gyarados for Indimidate */ @@ -37,10 +38,11 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override.mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves() + .ability(Abilities.INTIMIDATE); // Extortion ability const biomeMap = new Map([ [ Biome.VOLCANO, [ MysteryEncounterType.MYSTERIOUS_CHALLENGERS ]], @@ -195,6 +197,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { }); it("should award EXP to a pokemon with a move in EXTORTION_MOVES", async () => { + game.override.ability(Abilities.SYNCHRONIZE); // Not an extortion ability, so we can test extortion move await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, [ Species.ABRA ]); const party = scene.getParty(); const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 8e286468bea..bfa3d428bc0 100644 --- a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -176,10 +176,12 @@ describe("Berries Abound - Mystery Encounter", () => { const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + scene.getParty().forEach(pkm => { + vi.spyOn(pkm, "getStat").mockReturnValue(1); // for ease return for every stat + }); + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; - // Setting enemy's level arbitrarily high to outspeed - config.pokemonConfigs![0].dataSource!.level = 1000; await runMysteryEncounterToEnd(game, 2, undefined, true); @@ -198,10 +200,12 @@ describe("Berries Abound - Mystery Encounter", () => { const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + scene.getParty().forEach(pkm => { + vi.spyOn(pkm, "getStat").mockReturnValue(1); // for ease return for every stat + }); + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; - // Setting enemy's level arbitrarily high to outspeed - config.pokemonConfigs![0].dataSource!.level = 1000; await runMysteryEncounterToEnd(game, 2, undefined, true); diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index f8d1ffd3ded..040381c4ac3 100644 --- a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -147,8 +147,8 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { expect(scene.getParty().length).toBe(initialPartySize + 1); - const newlyPurchasedPokemon = scene.getParty().find(p => p.name === pokemonName); - expect(newlyPurchasedPokemon).toBeDefined(); + const newlyPurchasedPokemon = scene.getParty()[scene.getParty().length - 1]; + expect(newlyPurchasedPokemon.name).toBe(pokemonName); expect(newlyPurchasedPokemon!.moveset.length > 0).toBeTruthy(); }); From 2ac688de4bf0d4715a1aa394b3c12162073ae0fe Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Sat, 12 Oct 2024 16:06:26 -0400 Subject: [PATCH 2/8] [Misc] More complete phase logging (#4651) --- src/battle-scene.ts | 6 +++++- src/phase.ts | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 40e3971b7fc..850d0baab5d 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2316,7 +2316,10 @@ export default class BattleScene extends SceneBase { } } - this.currentPhase?.start(); + if (this.currentPhase) { + console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;"); + this.currentPhase.start(); + } } overridePhase(phase: Phase): boolean { @@ -2326,6 +2329,7 @@ export default class BattleScene extends SceneBase { this.standbyPhase = this.currentPhase; this.currentPhase = phase; + console.log(`%cStart Phase ${phase.constructor.name}`, "color:green;"); phase.start(); return true; diff --git a/src/phase.ts b/src/phase.ts index 02939757112..5cf91f2c478 100644 --- a/src/phase.ts +++ b/src/phase.ts @@ -8,7 +8,6 @@ export class Phase { } start() { - console.log(`%cStart Phase ${this.constructor.name}`, "color:green;"); if (this.scene.abilityBar.shown) { this.scene.abilityBar.resetAutoHideTimer(); } From ebb76129990751aa3e92784c179a838edaaefd96 Mon Sep 17 00:00:00 2001 From: PrabbyDD <147005742+PrabbyDD@users.noreply.github.com> Date: Sat, 12 Oct 2024 15:29:36 -0700 Subject: [PATCH 3/8] [Bug] Stat Stages are now changed individually instead of all at once (#4457) Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> --- src/phases/stat-stage-change-phase.ts | 10 ++++ src/test/abilities/competitive.test.ts | 72 ++++++++++++++++++++++++++ src/test/abilities/defiant.test.ts | 70 +++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 src/test/abilities/competitive.test.ts create mode 100644 src/test/abilities/defiant.test.ts diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 4418c38c849..bfe19ea9ca5 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -36,6 +36,16 @@ export class StatStageChangePhase extends PokemonPhase { } start() { + + // Check if multiple stats are being changed at the same time, then run SSCPhase for each of them + if (this.stats.length > 1) { + for (let i = 0; i < this.stats.length; i++) { + const stat = [ this.stats[i] ]; + this.scene.unshiftPhase(new StatStageChangePhase(this.scene, this.battlerIndex, this.selfTarget, stat, this.stages, this.showMessage, this.ignoreAbilities, this.canBeCopied, this.onChange)); + } + return this.end(); + } + const pokemon = this.getPokemon(); if (!pokemon.isActive(true)) { diff --git a/src/test/abilities/competitive.test.ts b/src/test/abilities/competitive.test.ts new file mode 100644 index 00000000000..ecb276a1b8d --- /dev/null +++ b/src/test/abilities/competitive.test.ts @@ -0,0 +1,72 @@ +import { Stat } from "#enums/stat"; +import { TurnInitPhase } from "#app/phases/turn-init-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"; + +describe("Abilities - Competitive", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override.battleType("single") + .enemySpecies(Species.BEEDRILL) + .enemyMoveset(Moves.TICKLE) + .startingLevel(1) + .moveset([ Moves.SPLASH, Moves.CLOSE_COMBAT ]) + .ability(Abilities.COMPETITIVE); + }); + + it("lower atk and def by 1 via tickle, then increase spatk by 4 via competitive", async () => { + await game.classicMode.startBattle([ Species.FLYGON ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(4); + }); + + it("lowering your own stats should not trigger competitive", async () => { + game.override.enemyMoveset(Moves.SPLASH); + await game.classicMode.startBattle([ Species.FLYGON ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.CLOSE_COMBAT); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(playerPokemon.getStatStage(Stat.SPDEF)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); + }); + + it("white herb should remove only the negative effects", async () => { + game.override.startingHeldItems([{ name: "WHITE_HERB" }]); + await game.classicMode.startBattle([ Species.FLYGON ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(4); + }); +}); diff --git a/src/test/abilities/defiant.test.ts b/src/test/abilities/defiant.test.ts new file mode 100644 index 00000000000..aa8d250dad7 --- /dev/null +++ b/src/test/abilities/defiant.test.ts @@ -0,0 +1,70 @@ +import { Stat } from "#enums/stat"; +import { TurnInitPhase } from "#app/phases/turn-init-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"; + +describe("Abilities - Defiant", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override.battleType("single") + .enemySpecies(Species.BEEDRILL) + .enemyMoveset(Moves.TICKLE) + .startingLevel(1) + .moveset([ Moves.SPLASH, Moves.CLOSE_COMBAT ]) + .ability(Abilities.DEFIANT); + }); + + it("lower atk and def by 1 via tickle, then increase atk by 4 via defiant", async () => { + await game.classicMode.startBattle([ Species.FLYGON ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(3); + expect(playerPokemon.getStatStage(Stat.DEF)).toBe(-1); + }); + + it("lowering your own stats should not trigger defiant", async () => { + game.override.enemyMoveset(Moves.SPLASH); + await game.classicMode.startBattle([ Species.FLYGON ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.CLOSE_COMBAT); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(playerPokemon.getStatStage(Stat.SPDEF)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); + }); + + it("white herb should remove only the negative effects", async () => { + game.override.startingHeldItems([{ name: "WHITE_HERB" }]); + await game.classicMode.startBattle([ Species.FLYGON ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to(TurnInitPhase); + + expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(playerPokemon.getStatStage(Stat.ATK)).toBe(3); + }); +}); From caf29e2ce3fe5bd194a0a42eac9b91f32b65dc89 Mon Sep 17 00:00:00 2001 From: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> Date: Sat, 12 Oct 2024 23:42:20 -0400 Subject: [PATCH 4/8] [Documentation] Document all (P) moves (#4650) * Document all (P) moves * Fix some typos * Fix more typos * Address innerthunder comments * Add circle throw and dragon tail (P) --- src/data/move.ts | 81 +++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index bd3706545f9..8e9977337cc 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -365,6 +365,14 @@ export default class Move implements Localizable { return this; } + /** + * Internal dev flag for documenting edge cases. When using this, please document the known edge case. + * @returns the called object {@linkcode Move} + */ + edgeCase(): this { + return this; + } + /** * Marks the move as "partial": appends texts to the move name * @returns the called object {@linkcode Move} @@ -7084,7 +7092,8 @@ export function initMoves() { .attr(ForceSwitchOutAttr) .ignoresSubstitute() .hidesTarget() - .windMove(), + .windMove() + .partial(), // Should force random switches new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) .attr(ChargeAttr, ChargeAnim.FLY_CHARGING, i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }), BattlerTagType.FLYING) .condition(failOnGravityCondition) @@ -7161,7 +7170,8 @@ export function initMoves() { new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1) .attr(ForceSwitchOutAttr) .soundBased() - .hidesTarget(), + .hidesTarget() + .partial(), // Should force random switching new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP) .soundBased(), @@ -7302,7 +7312,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), new AttackMove(Moves.QUICK_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 1), new AttackMove(Moves.RAGE, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 20, -1, 0, 1) - .partial(), + .partial(), // No effect implemented new SelfStatusMove(Moves.TELEPORT, Type.PSYCHIC, -1, 20, -1, -6, 1) .attr(ForceSwitchOutAttr, true) .hidesUser(), @@ -7617,7 +7627,7 @@ export function initMoves() { new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2) .attr(StatStageChangeAttr, [ Stat.ATK ], -2), new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2) - .partial() + .partial() // Does not lock the user, also does not increase damage properly .attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL), new AttackMove(Moves.FALSE_SWIPE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 2) .attr(SurviveDamageAttr), @@ -7688,7 +7698,7 @@ export function initMoves() { .ignoresSubstitute() .condition((user, target, move) => new EncoreTag(user.id).canAdd(target)), new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2) - .partial(), + .partial(), // No effect implemented new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2) .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true) .attr(RemoveBattlerTagAttr, [ @@ -7753,7 +7763,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .ballBombMove(), new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2) - .partial() + .partial() // Complete buggy mess .attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", { pokemonName: "{USER}" })), new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2) .attr(StatStageChangeAttr, [ Stat.DEF ], -1), @@ -7771,7 +7781,7 @@ export function initMoves() { .ignoresVirtual() .soundBased() .target(MoveTarget.RANDOM_NEAR_ENEMY) - .partial(), + .partial(), // Does not lock the user, does not stop Pokemon from sleeping new SelfStatusMove(Moves.STOCKPILE, Type.NORMAL, -1, 20, -1, 0, 3) .condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3) .attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true), @@ -7794,7 +7804,7 @@ export function initMoves() { .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) .ignoresSubstitute() - .partial() // Incomplete implementation because of Uproar's partial implementation + .edgeCase() // Incomplete implementation because of Uproar's partial implementation .attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1), new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1) @@ -7883,7 +7893,7 @@ export function initMoves() { .unimplemented(), new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3) .makesContact(false) - .partial(), + .partial(), // No effect implemented new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) .attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true) .attr(GulpMissileTagAttr) @@ -7914,7 +7924,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.MUD_SPORT, 5) .target(MoveTarget.BOTH_SIDES), new AttackMove(Moves.ICE_BALL, Type.ICE, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 3) - .partial() + .partial() // Does not lock the user properly, does not increase damage correctly .attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL) .ballBombMove(), new AttackMove(Moves.NEEDLE_ARM, Type.GRASS, MoveCategory.PHYSICAL, 60, 100, 15, 30, 0, 3) @@ -8057,7 +8067,7 @@ export function initMoves() { .attr(ConfuseAttr) .pulseMove(), new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3) - .partial() + .partial() // Complete buggy mess .attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", { pokemonName: "{USER}" })), new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true), @@ -8467,7 +8477,7 @@ export function initMoves() { .attr(AfterYouAttr), new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) .soundBased() - .partial(), + .partial(), // No effect implemented new AttackMove(Moves.ECHOED_VOICE, Type.NORMAL, MoveCategory.SPECIAL, 40, 100, 15, -1, 0, 5) .attr(ConsecutiveUseMultiBasePowerAttr, 5, false) .soundBased(), @@ -8509,7 +8519,8 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), new AttackMove(Moves.CIRCLE_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5) - .attr(ForceSwitchOutAttr), + .attr(ForceSwitchOutAttr) + .partial(), // Should force random switches new AttackMove(Moves.INCINERATE, Type.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) .target(MoveTarget.ALL_NEAR_ENEMIES) .attr(RemoveHeldItemAttr, true), @@ -8577,7 +8588,8 @@ export function initMoves() { .attr(CritOnlyAttr), new AttackMove(Moves.DRAGON_TAIL, Type.DRAGON, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5) .attr(ForceSwitchOutAttr) - .hidesTarget(), + .hidesTarget() + .partial(), // Should force random switches new SelfStatusMove(Moves.WORK_UP, Type.NORMAL, -1, 30, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, true), new AttackMove(Moves.ELECTROWEB, Type.ELECTRIC, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5) @@ -8705,7 +8717,7 @@ export function initMoves() { .ignoresVirtual(), new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) .attr(AddTypeAttr, Type.GHOST) - .partial(), + .edgeCase(), // Weird interaction with Forest's Curse, reflect type, burn up new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) .soundBased(), @@ -8718,7 +8730,7 @@ export function initMoves() { .triageMove(), new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6) .attr(AddTypeAttr, Type.GRASS) - .partial(), + .edgeCase(), // Weird interaction with Trick or Treat, reflect type, burn up new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6) .windMove() .makesContact(false) @@ -8726,7 +8738,7 @@ export function initMoves() { new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6) .attr(StatusEffectAttr, StatusEffect.FREEZE) .attr(WaterSuperEffectTypeMultiplierAttr) - .partial(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell. + .edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell. new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -9104,15 +9116,15 @@ export function initMoves() { /* Unused */ new AttackMove(Moves.SINISTER_ARROW_RAID, Type.GHOST, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) .makesContact(false) - .partial() + .edgeCase() // I assume it's because the user needs spirit shackle and decidueye .ignoresVirtual(), new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) .attr(AlwaysHitMinimizeAttr) .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) - .partial() + .edgeCase() // I assume it's because it needs darkest lariat and incineroar .ignoresVirtual(), new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) - .partial() + .edgeCase() // I assume it's because it needs sparkling aria and primarina .ignoresVirtual(), new AttackMove(Moves.GUARDIAN_OF_ALOLA, Type.FAIRY, MoveCategory.SPECIAL, -1, -1, 1, -1, 0, 7) .unimplemented() @@ -9121,10 +9133,10 @@ export function initMoves() { .unimplemented() .ignoresVirtual(), new AttackMove(Moves.STOKED_SPARKSURFER, Type.ELECTRIC, MoveCategory.SPECIAL, 175, -1, 1, 100, 0, 7) - .partial() + .edgeCase() // I assume it's because it needs thunderbolt and Alola Raichu .ignoresVirtual(), new AttackMove(Moves.PULVERIZING_PANCAKE, Type.NORMAL, MoveCategory.PHYSICAL, 210, -1, 1, -1, 0, 7) - .partial() + .edgeCase() // I assume it's because it needs giga impact and snorlax .ignoresVirtual(), new SelfStatusMove(Moves.EXTREME_EVOBOOST, Type.NORMAL, -1, 1, -1, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) @@ -9155,13 +9167,13 @@ export function initMoves() { .attr(RechargeAttr), new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7) .ignoresSubstitute() - .partial(), + .partial(), // Does not steal stats new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) .ignoresAbilities() - .partial(), + .edgeCase(), // Should not ignore abilities when called virtually (metronome) new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .ignoresAbilities() - .partial(), + .edgeCase(), // Should not ignore abilities when called virtually (metronome) new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) @@ -9172,7 +9184,7 @@ export function initMoves() { .attr(FormChangeItemTypeAttr), /* Unused */ new AttackMove(Moves.TEN_MILLION_VOLT_THUNDERBOLT, Type.ELECTRIC, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) - .partial() + .edgeCase() // I assume it's because it needs thunderbolt and pikachu in a cap .ignoresVirtual(), /* End Unused */ new AttackMove(Moves.MIND_BLOWN, Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 7) @@ -9185,7 +9197,7 @@ export function initMoves() { new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) .ignoresAbilities() - .partial(), + .edgeCase(), // Should not ignore abilities when called virtually (metronome) /* Unused */ new AttackMove(Moves.LIGHT_THAT_BURNS_THE_SKY, Type.PSYCHIC, MoveCategory.SPECIAL, 200, -1, 1, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) @@ -9198,7 +9210,7 @@ export function initMoves() { .ignoresAbilities() .ignoresVirtual(), new AttackMove(Moves.LETS_SNUGGLE_FOREVER, Type.FAIRY, MoveCategory.PHYSICAL, 190, -1, 1, -1, 0, 7) - .partial() + .edgeCase() // I assume it needs play rough and mimikyu .ignoresVirtual(), new AttackMove(Moves.SPLINTERED_STORMSHARDS, Type.ROCK, MoveCategory.PHYSICAL, 190, -1, 1, -1, 0, 7) .attr(ClearTerrainAttr) @@ -9208,7 +9220,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, undefined, undefined, undefined, undefined, true) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES) - .partial() + .edgeCase() // I assume it needs clanging scales and Kommo-O .ignoresVirtual(), /* End Unused */ new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) //LGPE Implementation @@ -9271,14 +9283,14 @@ export function initMoves() { new AttackMove(Moves.JAW_LOCK, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 8) .attr(JawLockAttr) .bitingMove(), - new SelfStatusMove(Moves.STUFF_CHEEKS, Type.NORMAL, -1, 10, -1, 0, 8) // TODO: Stuff Cheeks should not be selectable when the user does not have a berry, see wiki + new SelfStatusMove(Moves.STUFF_CHEEKS, Type.NORMAL, -1, 10, -1, 0, 8) .attr(EatBerryAttr) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) .condition((user) => { const userBerries = user.scene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()); return userBerries.length > 0; }) - .partial(), + .edgeCase(), // Stuff Cheeks should not be selectable when the user does not have a berry, see wiki new SelfStatusMove(Moves.NO_RETREAT, Type.FIGHTING, -1, 5, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false) @@ -9292,7 +9304,7 @@ export function initMoves() { new AttackMove(Moves.DRAGON_DARTS, Type.DRAGON, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 8) .attr(MultiHitAttr, MultiHitType._2) .makesContact(false) - .partial(), + .partial(), // smart targetting is unimplemented new StatusMove(Moves.TEATIME, Type.NORMAL, -1, 10, -1, 0, 8) .attr(EatBerryAttr) .target(MoveTarget.ALL), @@ -9731,7 +9743,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), new AttackMove(Moves.ORDER_UP, Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9) .makesContact(false) - .partial(), + .partial(), // No effect implemented (requires Commander) new AttackMove(Moves.JET_PUNCH, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9) .punchingMove(), new StatusMove(Moves.SPICY_EXTRACT, Type.GRASS, -1, 15, -1, 0, 9) @@ -9943,8 +9955,7 @@ export function initMoves() { new AttackMove(Moves.UPPER_HAND, Type.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9) .attr(FlinchAttr) .condition((user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS && allMoves[user.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].priority > 0 ) // TODO: is this bang correct? - //TODO: Should also apply when target move priority increased by ability ex. gale wings - .partial(), + .partial(), // Should also apply when target move priority increased by ability ex. gale wings new AttackMove(Moves.MALIGNANT_CHAIN, Type.POISON, MoveCategory.SPECIAL, 100, 100, 5, 50, 0, 9) .attr(StatusEffectAttr, StatusEffect.TOXIC) ); From 8e7aea0f8990450d5d06e273f83188fffa87857e Mon Sep 17 00:00:00 2001 From: damocleas Date: Sat, 12 Oct 2024 23:51:14 -0400 Subject: [PATCH 5/8] Fixed Charizard and Kingler BST, fixed ability indexing for gmax forms (#4652) --- src/data/pokemon-species.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 1ceb5971f6a..8bb23cfc208 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -975,7 +975,7 @@ export function initSpecies() { new PokemonSpecies(Species.VENUSAUR, 1, false, false, false, "Seed Pokémon", Type.GRASS, Type.POISON, 2, 100, Abilities.OVERGROW, Abilities.NONE, Abilities.CHLOROPHYLL, 525, 80, 82, 83, 100, 100, 80, 45, 50, 263, GrowthRate.MEDIUM_SLOW, 87.5, true, true, new PokemonForm("Normal", "", Type.GRASS, Type.POISON, 2, 100, Abilities.OVERGROW, Abilities.NONE, Abilities.CHLOROPHYLL, 525, 80, 82, 83, 100, 100, 80, 45, 50, 263, true, null, true), new PokemonForm("Mega", SpeciesFormKey.MEGA, Type.GRASS, Type.POISON, 2.4, 155.5, Abilities.THICK_FAT, Abilities.THICK_FAT, Abilities.THICK_FAT, 625, 80, 100, 123, 122, 120, 80, 45, 50, 263, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.POISON, 24, 999.9, Abilities.EFFECT_SPORE, Abilities.EFFECT_SPORE, Abilities.EFFECT_SPORE, 625, 120, 82, 98, 130, 115, 80, 45, 50, 263, true), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.POISON, 24, 999.9, Abilities.EFFECT_SPORE, Abilities.NONE, Abilities.EFFECT_SPORE, 625, 120, 82, 98, 130, 115, 80, 45, 50, 263, true), ), new PokemonSpecies(Species.CHARMANDER, 1, false, false, false, "Lizard Pokémon", Type.FIRE, null, 0.6, 8.5, Abilities.BLAZE, Abilities.NONE, Abilities.SOLAR_POWER, 309, 39, 52, 43, 60, 50, 65, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.CHARMELEON, 1, false, false, false, "Flame Pokémon", Type.FIRE, null, 1.1, 19, Abilities.BLAZE, Abilities.NONE, Abilities.SOLAR_POWER, 405, 58, 64, 58, 80, 65, 80, 45, 50, 142, GrowthRate.MEDIUM_SLOW, 87.5, false), @@ -983,20 +983,20 @@ export function initSpecies() { new PokemonForm("Normal", "", Type.FIRE, Type.FLYING, 1.7, 90.5, Abilities.BLAZE, Abilities.NONE, Abilities.SOLAR_POWER, 534, 78, 84, 78, 109, 85, 100, 45, 50, 267, false, null, true), new PokemonForm("Mega X", SpeciesFormKey.MEGA_X, Type.FIRE, Type.DRAGON, 1.7, 110.5, Abilities.TOUGH_CLAWS, Abilities.NONE, Abilities.TOUGH_CLAWS, 634, 78, 130, 111, 130, 85, 100, 45, 50, 267), new PokemonForm("Mega Y", SpeciesFormKey.MEGA_Y, Type.FIRE, Type.FLYING, 1.7, 100.5, Abilities.DROUGHT, Abilities.NONE, Abilities.DROUGHT, 634, 78, 104, 78, 159, 115, 100, 45, 50, 267), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.FLYING, 28, 999.9, Abilities.BERSERK, Abilities.BERSERK, Abilities.BERSERK, 634, 118, 84, 93, 139, 110, 100, 45, 50, 267), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.FLYING, 28, 999.9, Abilities.BERSERK, Abilities.NONE, Abilities.BERSERK, 634, 118, 84, 93, 139, 100, 100, 45, 50, 267), ), new PokemonSpecies(Species.SQUIRTLE, 1, false, false, false, "Tiny Turtle Pokémon", Type.WATER, null, 0.5, 9, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 314, 44, 48, 65, 50, 64, 43, 45, 50, 63, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.WARTORTLE, 1, false, false, false, "Turtle Pokémon", Type.WATER, null, 1, 22.5, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 405, 59, 63, 80, 65, 80, 58, 45, 50, 142, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.BLASTOISE, 1, false, false, false, "Shellfish Pokémon", Type.WATER, null, 1.6, 85.5, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 530, 79, 83, 100, 85, 105, 78, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.WATER, null, 1.6, 85.5, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 530, 79, 83, 100, 85, 105, 78, 45, 50, 265, false, null, true), new PokemonForm("Mega", SpeciesFormKey.MEGA, Type.WATER, null, 1.6, 101.1, Abilities.MEGA_LAUNCHER, Abilities.NONE, Abilities.MEGA_LAUNCHER, 630, 79, 103, 120, 135, 115, 78, 45, 50, 265), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.STEEL, 25, 999.9, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, 630, 119, 83, 135, 115, 110, 68, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.STEEL, 25, 999.9, Abilities.SHELL_ARMOR, Abilities.NONE, Abilities.SHELL_ARMOR, 630, 119, 83, 135, 115, 110, 68, 45, 50, 265), ), new PokemonSpecies(Species.CATERPIE, 1, false, false, false, "Worm Pokémon", Type.BUG, null, 0.3, 2.9, Abilities.SHIELD_DUST, Abilities.NONE, Abilities.RUN_AWAY, 195, 45, 30, 35, 20, 20, 45, 255, 50, 39, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.METAPOD, 1, false, false, false, "Cocoon Pokémon", Type.BUG, null, 0.7, 9.9, Abilities.SHED_SKIN, Abilities.NONE, Abilities.SHED_SKIN, 205, 50, 20, 55, 25, 25, 30, 120, 50, 72, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.BUTTERFREE, 1, false, false, false, "Butterfly Pokémon", Type.BUG, Type.FLYING, 1.1, 32, Abilities.COMPOUND_EYES, Abilities.NONE, Abilities.TINTED_LENS, 395, 60, 45, 50, 90, 80, 70, 45, 50, 198, GrowthRate.MEDIUM_FAST, 50, true, true, new PokemonForm("Normal", "", Type.BUG, Type.FLYING, 1.1, 32, Abilities.COMPOUND_EYES, Abilities.NONE, Abilities.TINTED_LENS, 395, 60, 45, 50, 90, 80, 70, 45, 50, 198, true, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.FLYING, 17, 999.9, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, 495, 85, 35, 80, 120, 90, 85, 45, 50, 198, true), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.FLYING, 17, 999.9, Abilities.COMPOUND_EYES, Abilities.NONE, Abilities.COMPOUND_EYES, 495, 85, 35, 80, 120, 90, 85, 45, 50, 198, true), ), new PokemonSpecies(Species.WEEDLE, 1, false, false, false, "Hairy Bug Pokémon", Type.BUG, Type.POISON, 0.3, 3.2, Abilities.SHIELD_DUST, Abilities.NONE, Abilities.RUN_AWAY, 195, 40, 35, 30, 20, 20, 50, 255, 70, 39, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.KAKUNA, 1, false, false, false, "Cocoon Pokémon", Type.BUG, Type.POISON, 0.6, 10, Abilities.SHED_SKIN, Abilities.NONE, Abilities.SHED_SKIN, 205, 45, 25, 50, 25, 25, 35, 120, 70, 72, GrowthRate.MEDIUM_FAST, 50, false), @@ -1025,7 +1025,7 @@ export function initSpecies() { new PokemonForm("Cute Cosplay", "cute-cosplay", Type.ELECTRIC, null, 0.4, 6, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 430, 45, 80, 50, 75, 60, 120, 190, 50, 112, true, null, true), //Custom new PokemonForm("Smart Cosplay", "smart-cosplay", Type.ELECTRIC, null, 0.4, 6, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 430, 45, 80, 50, 75, 60, 120, 190, 50, 112, true, null, true), //Custom new PokemonForm("Tough Cosplay", "tough-cosplay", Type.ELECTRIC, null, 0.4, 6, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 430, 45, 80, 50, 75, 60, 120, 190, 50, 112, true, null, true), //Custom - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, null, 21, 999.9, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, 530, 125, 95, 60, 90, 70, 90, 190, 50, 112), //+100 BST from Partner Form + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, null, 21, 999.9, Abilities.LIGHTNING_ROD, Abilities.NONE, Abilities.LIGHTNING_ROD, 530, 125, 95, 60, 90, 70, 90, 190, 50, 112), //+100 BST from Partner Form ), new PokemonSpecies(Species.RAICHU, 1, false, false, false, "Mouse Pokémon", Type.ELECTRIC, null, 0.8, 30, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 485, 60, 90, 55, 90, 80, 110, 75, 50, 243, GrowthRate.MEDIUM_FAST, 50, true), new PokemonSpecies(Species.SANDSHREW, 1, false, false, false, "Mouse Pokémon", Type.GROUND, null, 0.6, 12, Abilities.SAND_VEIL, Abilities.NONE, Abilities.SAND_RUSH, 300, 50, 75, 85, 20, 30, 40, 255, 50, 60, GrowthRate.MEDIUM_FAST, 50, false), @@ -1110,7 +1110,7 @@ export function initSpecies() { new PokemonSpecies(Species.GENGAR, 1, false, false, false, "Shadow Pokémon", Type.GHOST, Type.POISON, 1.5, 40.5, Abilities.CURSED_BODY, Abilities.NONE, Abilities.NONE, 500, 60, 65, 60, 130, 75, 110, 45, 50, 250, GrowthRate.MEDIUM_SLOW, 50, false, true, new PokemonForm("Normal", "", Type.GHOST, Type.POISON, 1.5, 40.5, Abilities.CURSED_BODY, Abilities.NONE, Abilities.NONE, 500, 60, 65, 60, 130, 75, 110, 45, 50, 250, false, null, true), new PokemonForm("Mega", SpeciesFormKey.MEGA, Type.GHOST, Type.POISON, 1.4, 40.5, Abilities.SHADOW_TAG, Abilities.NONE, Abilities.NONE, 600, 60, 65, 80, 170, 95, 130, 45, 50, 250), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GHOST, Type.POISON, 20, 999.9, Abilities.CURSED_BODY, Abilities.CURSED_BODY, Abilities.CURSED_BODY, 600, 140, 65, 70, 140, 85, 100, 45, 50, 250), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GHOST, Type.POISON, 20, 999.9, Abilities.CURSED_BODY, Abilities.NONE, Abilities.NONE, 600, 140, 65, 70, 140, 85, 100, 45, 50, 250), ), new PokemonSpecies(Species.ONIX, 1, false, false, false, "Rock Snake Pokémon", Type.ROCK, Type.GROUND, 8.8, 210, Abilities.ROCK_HEAD, Abilities.STURDY, Abilities.WEAK_ARMOR, 385, 35, 45, 160, 30, 45, 70, 45, 50, 77, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.DROWZEE, 1, false, false, false, "Hypnosis Pokémon", Type.PSYCHIC, null, 1, 32.4, Abilities.INSOMNIA, Abilities.FOREWARN, Abilities.INNER_FOCUS, 328, 60, 48, 45, 43, 90, 42, 190, 70, 66, GrowthRate.MEDIUM_FAST, 50, false), @@ -1118,7 +1118,7 @@ export function initSpecies() { new PokemonSpecies(Species.KRABBY, 1, false, false, false, "River Crab Pokémon", Type.WATER, null, 0.4, 6.5, Abilities.HYPER_CUTTER, Abilities.SHELL_ARMOR, Abilities.SHEER_FORCE, 325, 30, 105, 90, 25, 25, 50, 225, 50, 65, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.KINGLER, 1, false, false, false, "Pincer Pokémon", Type.WATER, null, 1.3, 60, Abilities.HYPER_CUTTER, Abilities.SHELL_ARMOR, Abilities.SHEER_FORCE, 475, 55, 130, 115, 50, 50, 75, 60, 50, 166, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.WATER, null, 1.3, 60, Abilities.HYPER_CUTTER, Abilities.SHELL_ARMOR, Abilities.SHEER_FORCE, 475, 55, 130, 115, 50, 50, 75, 60, 50, 166, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 19, 999.9, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, 575, 90, 155, 140, 50, 80, 70, 60, 50, 166), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 19, 999.9, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, 575, 90, 155, 140, 50, 70, 70, 60, 50, 166), ), new PokemonSpecies(Species.VOLTORB, 1, false, false, false, "Ball Pokémon", Type.ELECTRIC, null, 0.5, 10.4, Abilities.SOUNDPROOF, Abilities.STATIC, Abilities.AFTERMATH, 330, 40, 30, 50, 55, 55, 100, 190, 70, 66, GrowthRate.MEDIUM_FAST, null, false), new PokemonSpecies(Species.ELECTRODE, 1, false, false, false, "Ball Pokémon", Type.ELECTRIC, null, 1.2, 66.6, Abilities.SOUNDPROOF, Abilities.STATIC, Abilities.AFTERMATH, 490, 60, 50, 70, 80, 80, 150, 60, 70, 172, GrowthRate.MEDIUM_FAST, null, false), @@ -2298,25 +2298,25 @@ export function initSpecies() { new PokemonSpecies(Species.MELTAN, 7, false, false, true, "Hex Nut Pokémon", Type.STEEL, null, 0.2, 8, Abilities.MAGNET_PULL, Abilities.NONE, Abilities.NONE, 300, 46, 65, 65, 55, 35, 34, 3, 0, 150, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.MELMETAL, 7, false, false, true, "Hex Nut Pokémon", Type.STEEL, null, 2.5, 800, Abilities.IRON_FIST, Abilities.NONE, Abilities.NONE, 600, 135, 143, 143, 80, 65, 34, 3, 0, 300, GrowthRate.SLOW, null, false, true, new PokemonForm("Normal", "", Type.STEEL, null, 2.5, 800, Abilities.IRON_FIST, Abilities.NONE, Abilities.NONE, 600, 135, 143, 143, 80, 65, 34, 3, 0, 300, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, null, 25, 999.9, Abilities.IRON_FIST, Abilities.IRON_FIST, Abilities.IRON_FIST, 700, 175, 165, 155, 85, 75, 45, 3, 0, 300), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, null, 25, 999.9, Abilities.IRON_FIST, Abilities.NONE, Abilities.NONE, 700, 175, 165, 155, 85, 75, 45, 3, 0, 300), ), new PokemonSpecies(Species.GROOKEY, 8, false, false, false, "Chimp Pokémon", Type.GRASS, null, 0.3, 5, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 310, 50, 65, 50, 40, 40, 65, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.THWACKEY, 8, false, false, false, "Beat Pokémon", Type.GRASS, null, 0.7, 14, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 420, 70, 85, 70, 55, 60, 80, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.RILLABOOM, 8, false, false, false, "Drummer Pokémon", Type.GRASS, null, 2.1, 90, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 530, 100, 125, 90, 60, 70, 85, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.GRASS, null, 2.1, 90, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 530, 100, 125, 90, 60, 70, 85, 45, 50, 265, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, null, 28, 999.9, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, 630, 125, 150, 105, 85, 85, 80, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, null, 28, 999.9, Abilities.GRASSY_SURGE, Abilities.NONE, Abilities.GRASSY_SURGE, 630, 125, 150, 105, 85, 85, 80, 45, 50, 265), ), new PokemonSpecies(Species.SCORBUNNY, 8, false, false, false, "Rabbit Pokémon", Type.FIRE, null, 0.3, 4.5, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 310, 50, 71, 40, 40, 40, 69, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.RABOOT, 8, false, false, false, "Rabbit Pokémon", Type.FIRE, null, 0.6, 9, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 420, 65, 86, 60, 55, 60, 94, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.CINDERACE, 8, false, false, false, "Striker Pokémon", Type.FIRE, null, 1.4, 33, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 530, 80, 116, 75, 65, 75, 119, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.FIRE, null, 1.4, 33, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 530, 80, 116, 75, 65, 75, 119, 45, 50, 265, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, null, 27, 999.9, Abilities.LIBERO, Abilities.LIBERO, Abilities.LIBERO, 630, 100, 146, 80, 90, 80, 134, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, null, 27, 999.9, Abilities.LIBERO, Abilities.NONE, Abilities.LIBERO, 630, 100, 146, 80, 90, 80, 134, 45, 50, 265), ), new PokemonSpecies(Species.SOBBLE, 8, false, false, false, "Water Lizard Pokémon", Type.WATER, null, 0.3, 4, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 310, 50, 40, 40, 70, 40, 70, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.DRIZZILE, 8, false, false, false, "Water Lizard Pokémon", Type.WATER, null, 0.7, 11.5, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 420, 65, 60, 55, 95, 55, 90, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.INTELEON, 8, false, false, false, "Secret Agent Pokémon", Type.WATER, null, 1.9, 45.2, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 530, 70, 85, 65, 125, 65, 120, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.WATER, null, 1.9, 45.2, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 530, 70, 85, 65, 125, 65, 120, 45, 50, 265, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 40, 999.9, Abilities.SNIPER, Abilities.SNIPER, Abilities.SNIPER, 630, 95, 97, 77, 147, 77, 137, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 40, 999.9, Abilities.SNIPER, Abilities.NONE, Abilities.SNIPER, 630, 95, 97, 77, 147, 77, 137, 45, 50, 265), ), new PokemonSpecies(Species.SKWOVET, 8, false, false, false, "Cheeky Pokémon", Type.NORMAL, null, 0.3, 2.5, Abilities.CHEEK_POUCH, Abilities.NONE, Abilities.GLUTTONY, 275, 70, 55, 55, 35, 35, 25, 255, 50, 55, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.GREEDENT, 8, false, false, false, "Greedy Pokémon", Type.NORMAL, null, 0.6, 6, Abilities.CHEEK_POUCH, Abilities.NONE, Abilities.GLUTTONY, 460, 120, 95, 95, 55, 75, 20, 90, 50, 161, GrowthRate.MEDIUM_FAST, 50, false), @@ -2422,7 +2422,7 @@ export function initSpecies() { new PokemonForm("Ruby Swirl", "ruby-swirl", Type.FAIRY, null, 0.3, 0.5, Abilities.SWEET_VEIL, Abilities.NONE, Abilities.AROMA_VEIL, 495, 65, 60, 75, 110, 121, 64, 100, 50, 173, false, null, true), new PokemonForm("Caramel Swirl", "caramel-swirl", Type.FAIRY, null, 0.3, 0.5, Abilities.SWEET_VEIL, Abilities.NONE, Abilities.AROMA_VEIL, 495, 65, 60, 75, 110, 121, 64, 100, 50, 173, false, null, true), new PokemonForm("Rainbow Swirl", "rainbow-swirl", Type.FAIRY, null, 0.3, 0.5, Abilities.SWEET_VEIL, Abilities.NONE, Abilities.AROMA_VEIL, 495, 65, 60, 75, 110, 121, 64, 100, 50, 173, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FAIRY, null, 30, 999.9, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, 595, 135, 60, 75, 130, 131, 64, 100, 50, 173), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FAIRY, null, 30, 999.9, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 595, 135, 60, 75, 130, 131, 64, 100, 50, 173), ), new PokemonSpecies(Species.FALINKS, 8, false, false, false, "Formation Pokémon", Type.FIGHTING, null, 3, 62, Abilities.BATTLE_ARMOR, Abilities.NONE, Abilities.DEFIANT, 470, 65, 100, 100, 70, 60, 75, 45, 50, 165, GrowthRate.MEDIUM_FAST, null, false), new PokemonSpecies(Species.PINCURCHIN, 8, false, false, false, "Sea Urchin Pokémon", Type.ELECTRIC, null, 0.3, 1, Abilities.LIGHTNING_ROD, Abilities.NONE, Abilities.ELECTRIC_SURGE, 435, 48, 101, 95, 91, 85, 15, 75, 50, 152, GrowthRate.MEDIUM_FAST, 50, false), @@ -2444,7 +2444,7 @@ export function initSpecies() { new PokemonSpecies(Species.CUFANT, 8, false, false, false, "Copperderm Pokémon", Type.STEEL, null, 1.2, 100, Abilities.SHEER_FORCE, Abilities.NONE, Abilities.HEAVY_METAL, 330, 72, 80, 49, 40, 49, 40, 190, 50, 66, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.COPPERAJAH, 8, false, false, false, "Copperderm Pokémon", Type.STEEL, null, 3, 650, Abilities.SHEER_FORCE, Abilities.NONE, Abilities.HEAVY_METAL, 500, 122, 130, 69, 80, 69, 30, 90, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.STEEL, null, 3, 650, Abilities.SHEER_FORCE, Abilities.NONE, Abilities.HEAVY_METAL, 500, 122, 130, 69, 80, 69, 30, 90, 50, 175, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.GROUND, 23, 999.9, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, 600, 167, 155, 89, 80, 89, 20, 90, 50, 175), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.GROUND, 23, 999.9, Abilities.MOLD_BREAKER, Abilities.NONE, Abilities.MOLD_BREAKER, 600, 167, 155, 89, 80, 89, 20, 90, 50, 175), ), new PokemonSpecies(Species.DRACOZOLT, 8, false, false, false, "Fossil Pokémon", Type.ELECTRIC, Type.DRAGON, 1.8, 190, Abilities.VOLT_ABSORB, Abilities.HUSTLE, Abilities.SAND_RUSH, 505, 90, 100, 90, 80, 70, 75, 45, 50, 177, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.ARCTOZOLT, 8, false, false, false, "Fossil Pokémon", Type.ELECTRIC, Type.ICE, 2.3, 150, Abilities.VOLT_ABSORB, Abilities.STATIC, Abilities.SLUSH_RUSH, 505, 90, 100, 90, 90, 80, 55, 45, 50, 177, GrowthRate.SLOW, null, false), From 391f38c3c8bd6106f7c38f4a451883add3bac310 Mon Sep 17 00:00:00 2001 From: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> Date: Sun, 13 Oct 2024 00:45:38 -0400 Subject: [PATCH 6/8] [Documentation] Document all (P) abilities (#4649) * Document partial abilities * Fix typo * Address comments * Fix typo Terapagos -> Ogerpon --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 6a391818866..07fd48e2f91 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -118,6 +118,14 @@ export class Ability implements Localizable { this.nameAppend += " (N)"; return this; } + + /** + * Internal flag used for developers to document edge cases. When using this, please be sure to document the edge case. + * @returns the ability + */ + edgeCase(): this { + return this; + } } type AbAttrApplyFunc = (attr: TAttr, passive: boolean) => boolean | Promise; @@ -4906,7 +4914,7 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.SHIELD_DUST, 3) .attr(IgnoreMoveEffectsAbAttr) - .partial(), + .edgeCase(), // Does not work with secret power (unimplemented) new Ability(Abilities.OWN_TEMPO, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(IntimidateImmunityAbAttr) @@ -4951,7 +4959,7 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.SERENE_GRACE, 3) .attr(MoveEffectChanceMultiplierAbAttr, 2) - .partial(), + .edgeCase(), // does not work with secret power (unimplemented) new Ability(Abilities.SWIFT_SWIM, 3) .attr(StatMultiplierAbAttr, Stat.SPD, 2) .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), @@ -5235,7 +5243,8 @@ export function initAbilities() { new Ability(Abilities.SHEER_FORCE, 5) .attr(MovePowerBoostAbAttr, (user, target, move) => move.chance >= 1, 5461 / 4096) .attr(MoveEffectChanceMultiplierAbAttr, 0) - .partial(), + .edgeCase() // Should disable shell bell and Meloetta's relic song transformation + .edgeCase(), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented new Ability(Abilities.CONTRARY, 5) .attr(StatStageChangeMultiplierAbAttr, -1) .ignorable(), @@ -5278,7 +5287,7 @@ export function initAbilities() { /** Rate is doubled when under sun {@link https://dex.pokemonshowdown.com/abilities/harvest} */ (pokemon) => 0.5 * (getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)(pokemon) ? 2 : 1) ) - .partial(), + .edgeCase(), // Cannot recover berries used up by fling or natural gift (unimplemented) new Ability(Abilities.TELEPATHY, 5) .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon.getAlly() === attacker && move instanceof AttackMove) .ignorable(), @@ -5357,7 +5366,7 @@ export function initAbilities() { .bypassFaint(), new Ability(Abilities.VICTORY_STAR, 5) .attr(StatMultiplierAbAttr, Stat.ACC, 1.1) - .partial(), + .partial(), // Does not boost ally's accuracy new Ability(Abilities.TURBOBLAZE, 5) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTurboblaze", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(MoveAbilityBypassAbAttr), @@ -5468,7 +5477,7 @@ export function initAbilities() { .attr(UnsuppressableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .bypassFaint() - .partial(), + .partial(), // Meteor form should protect against status effects and yawn new Ability(Abilities.STAKEOUT, 7) .attr(MovePowerBoostAbAttr, (user, target, move) => user?.scene.currentBattle.turnCommands[target?.getBattlerIndex() ?? BattlerIndex.ATTACKER]?.command === Command.POKEMON, 2), new Ability(Abilities.WATER_BUBBLE, 7) @@ -5536,9 +5545,9 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .bypassFaint() .partial(), - new Ability(Abilities.CORROSION, 7) // TODO: Test Corrosion against Magic Bounce once it is implemented + new Ability(Abilities.CORROSION, 7) .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ]) - .partial(), + .edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented), fling with toxic orb (not implemented yet), and synchronize (not fully implemented yet) new Ability(Abilities.COMATOSE, 7) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) @@ -5693,7 +5702,7 @@ export function initAbilities() { new Ability(Abilities.WANDERING_SPIRIT, 8) .attr(PostDefendAbilitySwapAbAttr) .bypassFaint() - .partial(), + .edgeCase(), // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil new Ability(Abilities.GORILLA_TACTICS, 8) .attr(GorillaTacticsAbAttr), new Ability(Abilities.NEUTRALIZING_GAS, 8) @@ -5702,7 +5711,7 @@ export function initAbilities() { .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonNeutralizingGas", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) - .partial(), + .partial(), // A bunch of weird interactions with other abilities being suppressed then unsuppressed new Ability(Abilities.PASTEL_VEIL, 8) .attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) .attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) @@ -5807,7 +5816,7 @@ export function initAbilities() { new Ability(Abilities.GOOD_AS_GOLD, 9) .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.category === MoveCategory.STATUS) .ignorable() - .partial(), + .partial(), // Lots of weird interactions with moves and abilities such as negating status moves that target the field new Ability(Abilities.VESSEL_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.SPATK, 0.75) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonVesselOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPATK)) })) @@ -5840,7 +5849,7 @@ export function initAbilities() { .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), new Ability(Abilities.SUPREME_OVERLORD, 9) .attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 5)) - .partial(), + .partial(), // Counter resets every wave new Ability(Abilities.COSTAR, 9) .attr(PostSummonCopyAllyStatsAbAttr), new Ability(Abilities.TOXIC_DEBRIS, 9) @@ -5873,25 +5882,25 @@ export function initAbilities() { .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) - .partial(), + .partial(), // Ogerpon tera interactions new Ability(Abilities.EMBODY_ASPECT_WELLSPRING, 9) .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.SPDEF ], 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) - .partial(), + .partial(), // Ogerpon tera interactions new Ability(Abilities.EMBODY_ASPECT_HEARTHFLAME, 9) .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.ATK ], 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) - .partial(), + .partial(), // Ogerpon tera interactions new Ability(Abilities.EMBODY_ASPECT_CORNERSTONE, 9) .attr(PostBattleInitStatStageChangeAbAttr, [ Stat.DEF ], 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) - .partial(), + .partial(), // Ogerpon tera interactions new Ability(Abilities.TERA_SHIFT, 9) .attr(PostSummonFormChangeAbAttr, p => p.getFormKey() ? 0 : 1) .attr(UncopiableAbilityAbAttr) From 470f9e4e19276ec341736df795949743acbcee71 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:46:41 -0700 Subject: [PATCH 7/8] [P3] Fix Substitute visual error on wave transition (#4648) --- src/field/pokemon.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c495e0833cd..136c1eb1685 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -749,9 +749,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const relX = newOffset[0] - initialOffset[0]; const relY = newOffset[1] - initialOffset[1]; + const subTag = this.getTag(SubstituteTag); + if (duration) { + // TODO: can this use stricter typing? + const targets: any[] = [ this ]; + if (subTag?.sprite) { + targets.push(subTag.sprite); + } this.scene.tweens.add({ - targets: this, + targets: targets, x: (_target, _key, value: number) => value + relX, y: (_target, _key, value: number) => value + relY, duration: duration, @@ -761,6 +768,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } else { this.x += relX; this.y += relY; + if (subTag?.sprite) { + subTag.sprite.x += relX; + subTag.sprite.y += relY; + } } }); } From 8a355d500a27ca8c74482665a8680528007ab727 Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Sun, 13 Oct 2024 00:30:04 -0700 Subject: [PATCH 8/8] [Bug] Move Restriction Battler Tag bugs (#4536) * Added fixes * Revert "Added fixes" This reverts commit 3feccd792ddef0b32ddc40782b60f23f952cad23. * Added loadTag functions * Fixes * typeodcs * Torment * yawn * hsldklahdlhalhdlahldhlah * Imprison Fixes * Fixed imprison not interrupting PRE_MOVE * just kidding * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fixing what broke * added scp[es * missed a scope * Update src/data/battler-tags.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * good tp go * merge * battler tags * Apply suggestions from code review * Changed function names * publics --------- Co-authored-by: frutescens Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/arena-tag.ts | 68 ++++++++++++++++++++------------- src/data/battler-tags.ts | 75 ++++++++++++++++++++++++------------- src/data/move.ts | 3 +- src/field/pokemon.ts | 8 ++-- src/phases/command-phase.ts | 4 +- 5 files changed, 99 insertions(+), 59 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 11d28ab7e25..71cf11fa06f 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -4,7 +4,7 @@ import { Type } from "#app/data/type"; import * as Utils from "#app/utils"; import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move"; import { getPokemonNameWithAffix } from "#app/messages"; -import Pokemon, { HitResult, PlayerPokemon, PokemonMove, EnemyPokemon } from "#app/field/pokemon"; +import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; import { StatusEffect } from "#app/data/status-effect"; import { BattlerIndex } from "#app/battle"; import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; @@ -71,6 +71,32 @@ export abstract class ArenaTag { this.sourceId = source.sourceId; this.side = source.side; } + + /** + * Helper function that retrieves the source Pokemon + * @param scene medium to retrieve the source Pokemon + * @returns The source {@linkcode Pokemon} or `null` if none is found + */ + public getSourcePokemon(scene: BattleScene): Pokemon | null { + return this.sourceId ? scene.getPokemonById(this.sourceId) : null; + } + + /** + * Helper function that retrieves the Pokemon affected + * @param scene - medium to retrieve the involved Pokemon + * @returns list of PlayerPokemon or EnemyPokemon on the field + */ + public getAffectedPokemon(scene: BattleScene): Pokemon[] { + switch (this.side) { + case ArenaTagSide.PLAYER: + return scene.getPlayerField() ?? []; + case ArenaTagSide.ENEMY: + return scene.getEnemyField() ?? []; + case ArenaTagSide.BOTH: + default: + return scene.getField(true) ?? []; + } + } } /** @@ -978,36 +1004,24 @@ class NoneTag extends ArenaTag { * Imprison will apply to any opposing Pokemon that switch onto the field as well. */ class ImprisonTag extends ArenaTrapTag { - private source: Pokemon; - constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.IMPRISON, Moves.IMPRISON, sourceId, side, 1); } - /** - * Helper function that retrieves the Pokemon affected - * @param {BattleScene} scene medium to retrieve the involved Pokemon - * @returns list of PlayerPokemon or EnemyPokemon on the field - */ - private retrieveField(scene: BattleScene): PlayerPokemon[] | EnemyPokemon[] { - if (!this.source.isPlayer()) { - return scene.getPlayerField() ?? []; - } - return scene.getEnemyField() ?? []; - } - /** * This function applies the effects of Imprison to the opposing Pokemon already present on the field. * @param arena */ override onAdd({ scene }: Arena) { - this.source = scene.getPokemonById(this.sourceId!)!; - if (this.source) { - const party = this.retrieveField(scene); - party?.forEach((p: PlayerPokemon | EnemyPokemon ) => { - p.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId); + const source = this.getSourcePokemon(scene); + if (source) { + const party = this.getAffectedPokemon(scene); + party?.forEach((p: Pokemon ) => { + if (p.isAllowedInBattle()) { + p.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId); + } }); - scene.queueMessage(i18next.t("battlerTags:imprisonOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(this.source) })); + scene.queueMessage(i18next.t("battlerTags:imprisonOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(source) })); } } @@ -1016,8 +1030,9 @@ class ImprisonTag extends ArenaTrapTag { * @param _arena * @returns `true` if the source of the tag is still active on the field | `false` if not */ - override lapse(_arena: Arena): boolean { - return this.source.isActive(true); + override lapse({ scene }: Arena): boolean { + const source = this.getSourcePokemon(scene); + return source ? source.isActive(true) : false; } /** @@ -1026,7 +1041,8 @@ class ImprisonTag extends ArenaTrapTag { * @returns `true` */ override activateTrap(pokemon: Pokemon): boolean { - if (this.source.isActive(true)) { + const source = this.getSourcePokemon(pokemon.scene); + if (source && source.isActive(true) && pokemon.isAllowedInBattle()) { pokemon.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId); } return true; @@ -1037,8 +1053,8 @@ class ImprisonTag extends ArenaTrapTag { * @param arena */ override onRemove({ scene }: Arena): void { - const party = this.retrieveField(scene); - party?.forEach((p: PlayerPokemon | EnemyPokemon) => { + const party = this.getAffectedPokemon(scene); + party?.forEach((p: Pokemon) => { p.removeTag(BattlerTagType.IMPRISON); }); } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 18f03ada941..a5016746013 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -23,6 +23,7 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase"; import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; +import BattleScene from "#app/battle-scene"; export enum BattlerTagLapseType { FAINT, @@ -90,6 +91,15 @@ export class BattlerTag { this.sourceMove = source.sourceMove; this.sourceId = source.sourceId; } + + /** + * Helper function that retrieves the source Pokemon object + * @param scene medium to retrieve the source Pokemon + * @returns The source {@linkcode Pokemon} or `null` if none is found + */ + public getSourcePokemon(scene: BattleScene): Pokemon | null { + return this.sourceId ? scene.getPokemonById(this.sourceId) : null; + } } export interface WeatherBattlerTag { @@ -120,7 +130,7 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag { const phase = pokemon.scene.getCurrentPhase() as MovePhase; const move = phase.move; - if (this.isMoveRestricted(move.moveId)) { + if (this.isMoveRestricted(move.moveId, pokemon)) { if (this.interruptedText(pokemon, move.moveId)) { pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId)); } @@ -136,10 +146,11 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag { /** * Gets whether this tag is restricting a move. * - * @param {Moves} move {@linkcode Moves} ID to check restriction for. - * @returns {boolean} `true` if the move is restricted by this tag, otherwise `false`. + * @param move - {@linkcode Moves} ID to check restriction for. + * @param user - The {@linkcode Pokemon} involved + * @returns `true` if the move is restricted by this tag, otherwise `false`. */ - abstract isMoveRestricted(move: Moves): boolean; + public abstract isMoveRestricted(move: Moves, user?: Pokemon): boolean; /** * Checks if this tag is restricting a move based on a user's decisions during the target selection phase @@ -327,6 +338,16 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false); } + /** + * Loads the Gorilla Tactics Battler Tag along with its unique class variable moveId + * @override + * @param source Gorilla Tactics' {@linkcode BattlerTag} information + */ + public override loadTag(source: BattlerTag | any): void { + super.loadTag(source); + this.moveId = source.moveId; + } + /** * * @override @@ -2510,8 +2531,6 @@ export class MysteryEncounterPostSummonTag extends BattlerTag { * Torment does not interrupt the move if the move is performed consecutively in the same turn and right after Torment is applied */ export class TormentTag extends MoveRestrictionBattlerTag { - private target: Pokemon; - constructor(sourceId: number) { super(BattlerTagType.TORMENT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.TORMENT, sourceId); } @@ -2523,7 +2542,6 @@ export class TormentTag extends MoveRestrictionBattlerTag { */ override onAdd(pokemon: Pokemon) { super.onAdd(pokemon); - this.target = pokemon; pokemon.scene.queueMessage(i18next.t("battlerTags:tormentOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); } @@ -2542,15 +2560,18 @@ export class TormentTag extends MoveRestrictionBattlerTag { * @param {Moves} move the move under investigation * @returns `true` if there is valid consecutive usage | `false` if the moves are different from each other */ - override isMoveRestricted(move: Moves): boolean { - const lastMove = this.target.getLastXMoves(1)[0]; + public override isMoveRestricted(move: Moves, user: Pokemon): boolean { + if (!user) { + return false; + } + const lastMove = user.getLastXMoves(1)[0]; if ( !lastMove ) { return false; } // This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY // Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts const moveObj = allMoves[lastMove.move]; - const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || this.target.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr); + const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr); const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS); if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) { return true; @@ -2602,37 +2623,39 @@ export class TauntTag extends MoveRestrictionBattlerTag { * The tag is only removed when the source-user is removed from the field. */ export class ImprisonTag extends MoveRestrictionBattlerTag { - private source: Pokemon | null; - constructor(sourceId: number) { super(BattlerTagType.IMPRISON, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE ], 1, Moves.IMPRISON, sourceId); } - override onAdd(pokemon: Pokemon) { - if (this.sourceId) { - this.source = pokemon.scene.getPokemonById(this.sourceId); - } - } - /** * Checks if the source of Imprison is still active - * @param _pokemon - * @param _lapseType + * @override + * @param pokemon The pokemon this tag is attached to * @returns `true` if the source is still active */ - override lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { - return this.source?.isActive(true) ?? false; + public override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + const source = this.getSourcePokemon(pokemon.scene); + if (source) { + if (lapseType === BattlerTagLapseType.PRE_MOVE) { + return super.lapse(pokemon, lapseType) && source.isActive(true); + } else { + return source.isActive(true); + } + } + return false; } /** * Checks if the source of the tag has the parameter move in its moveset and that the source is still active + * @override * @param {Moves} move the move under investigation * @returns `false` if either condition is not met */ - override isMoveRestricted(move: Moves): boolean { - if (this.source) { - const sourceMoveset = this.source.getMoveset().map(m => m!.moveId); - return sourceMoveset?.includes(move) && this.source.isActive(true); + public override isMoveRestricted(move: Moves, user: Pokemon): boolean { + const source = this.getSourcePokemon(user.scene); + if (source) { + const sourceMoveset = source.getMoveset().map(m => m!.moveId); + return sourceMoveset?.includes(move) && source.isActive(true); } return false; } diff --git a/src/data/move.ts b/src/data/move.ts index 8e9977337cc..d4f3b2ce3ee 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7883,7 +7883,8 @@ export function initMoves() { .attr(SwitchAbilitiesAttr), new StatusMove(Moves.IMPRISON, Type.PSYCHIC, 100, 10, -1, 0, 3) .ignoresSubstitute() - .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false), + .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false) + .target(MoveTarget.ENEMY_SIDE), new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3) .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 136c1eb1685..9ae83753e62 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3062,8 +3062,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * * @see {@linkcode MoveRestrictionBattlerTag} */ - isMoveRestricted(moveId: Moves): boolean { - return this.getRestrictingTag(moveId) !== null; + public isMoveRestricted(moveId: Moves, pokemon?: Pokemon): boolean { + return this.getRestrictingTag(moveId, pokemon) !== null; } /** @@ -3096,7 +3096,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { */ getRestrictingTag(moveId: Moves, user?: Pokemon, target?: Pokemon): MoveRestrictionBattlerTag | null { for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) { - if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId)) { + if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId, user)) { return tag as MoveRestrictionBattlerTag; } else if (user && target && (tag as MoveRestrictionBattlerTag).isMoveTargetRestricted(moveId, user, target)) { return tag as MoveRestrictionBattlerTag; @@ -5139,7 +5139,7 @@ export class PokemonMove { * @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`. */ isUsable(pokemon: Pokemon, ignorePp: boolean = false, ignoreRestrictionTags: boolean = false): boolean { - if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId)) { + if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)) { return false; } diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index e85c66543ac..cf66631bd96 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -114,8 +114,8 @@ export class CommandPhase extends FieldPhase { // Decides between a Disabled, Not Implemented, or No PP translation message const errorMessage = - playerPokemon.isMoveRestricted(move.moveId) - ? playerPokemon.getRestrictingTag(move.moveId)!.selectionDeniedText(playerPokemon, move.moveId) + playerPokemon.isMoveRestricted(move.moveId, playerPokemon) + ? playerPokemon.getRestrictingTag(move.moveId, playerPokemon)!.selectionDeniedText(playerPokemon, move.moveId) : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP"; const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator