This commit is contained in:
DustinLin 2024-09-17 20:07:28 -07:00 committed by GitHub
commit e2543cb9ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 123 additions and 16 deletions

View File

@ -8,7 +8,7 @@ import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { WeatherType } from "./weather"; import { WeatherType } from "./weather";
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAbAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability"; import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAbAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr, ProtectStatAbAttr } from "./ability";
import { allAbilities } from "./ability"; import { allAbilities } from "./ability";
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle"; import { BattlerIndex, BattleType } from "../battle";
@ -5180,7 +5180,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
const switchOutTarget = this.user ? user : target; const switchOutTarget = this.user ? user : target;
if (switchOutTarget instanceof PlayerPokemon) { if (switchOutTarget instanceof PlayerPokemon) {
switchOutTarget.leaveField(!this.batonPass); switchOutTarget.leaveField(!this.batonPass);
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
user.scene.prependToPhase(new SwitchPhase(user.scene, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); user.scene.prependToPhase(new SwitchPhase(user.scene, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
resolve(true); resolve(true);
@ -5276,6 +5275,45 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
} }
/**
* Attr used by parting shot, it is a combo of ForceSwitchOut and StatChange with a special getCondition()
*/
export class PartingShotAttr extends ForceSwitchOutAttr {
private statChange = new StatStageChangeAttr([ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY);
private canLowerStats = true;
// using inherited constructor
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
// apply stat change, conditionally apply switch-out
this.statChange.apply(user, target, move, args);
if (this.canLowerStats) {
return super.apply(user, target, move, args);
} else {
return new Promise(resolve => {
resolve(false);
});
}
}
getCondition(): MoveConditionFunc {
return (user, target, move) => {
// conditions on if move should fail or not don't depend on if user is able to switch
// getCondition() is called before move is applied: move will only switch out if canLowerStats === true
if (target.hasAbilityWithAttr(ProtectStatAbAttr) ||
(target.getStatStage(Stat.ATK) === -6 && target.getStatStage(Stat.SPATK) === -6 && !target.hasAbility(Abilities.CONTRARY, true, false)) ||
target.scene.arena.findTagsOnSide(t => t.tagType === ArenaTagType.MIST, ArenaTagSide.ENEMY).length > 0 ||
(target.getStatStage(Stat.ATK) === 6 && target.getStatStage(Stat.SPATK) === 6 && target.hasAbility(Abilities.CONTRARY, true, false))
) {
this.canLowerStats = false;
} else {
this.canLowerStats = true;
}
return true;
};
}
}
export class RemoveTypeAttr extends MoveEffectAttr { export class RemoveTypeAttr extends MoveEffectAttr {
private removedType: Type; private removedType: Type;
@ -8381,8 +8419,7 @@ export function initMoves() {
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) .attr(PartingShotAttr, true, false)
.attr(ForceSwitchOutAttr, true, false)
.soundBased(), .soundBased(),
new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6)
.attr(InvertStatsAttr), .attr(InvertStatsAttr),

View File

@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, test } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
import GameManager from "../utils/gameManager"; import GameManager from "../utils/gameManager";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { BerryPhase } from "#app/phases/berry-phase"; import { BerryPhase } from "#app/phases/berry-phase";
@ -76,8 +76,8 @@ describe("Moves - Parting Shot", () => {
}, TIMEOUT }, TIMEOUT
); );
it.skip( // TODO: fix this bug to pass the test! test(
"Parting shot should fail if target is -6/-6 de-buffed", "Parting shot should not switch out if target is -6/-6 de-buffed",
async () => { async () => {
game.override.moveset([Moves.PARTING_SHOT, Moves.MEMENTO, Moves.SPLASH]); game.override.moveset([Moves.PARTING_SHOT, Moves.MEMENTO, Moves.SPLASH]);
await game.startBattle([Species.MEOWTH, Species.MEOWTH, Species.MEOWTH, Species.MURKROW, Species.ABRA]); await game.startBattle([Species.MEOWTH, Species.MEOWTH, Species.MEOWTH, Species.MURKROW, Species.ABRA]);
@ -118,7 +118,7 @@ describe("Moves - Parting Shot", () => {
}, TIMEOUT }, TIMEOUT
); );
it.skip( // TODO: fix this bug to pass the test! test(
"Parting shot shouldn't allow switch out when mist is active", "Parting shot shouldn't allow switch out when mist is active",
async () => { async () => {
game.override game.override
@ -135,11 +135,11 @@ describe("Moves - Parting Shot", () => {
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to(BerryPhase, false);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.SNORLAX);
}, TIMEOUT }, TIMEOUT
); );
it.skip( // TODO: fix this bug to pass the test! test(
"Parting shot shouldn't allow switch out against clear body ability", "Parting shot shouldn't allow switch out against clear body ability",
async () => { async () => {
game.override game.override
@ -155,13 +155,81 @@ describe("Moves - Parting Shot", () => {
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to(BerryPhase, false);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.SNORLAX);
}, TIMEOUT }, TIMEOUT
); );
it.skip( // TODO: fix this bug to pass the test! test(
"Parting shot shouldn't allow switch out against white smoke ability",
async () => {
game.override
.enemySpecies(Species.TORKOAL)
.enemyAbility(Abilities.WHITE_SMOKE);
await game.startBattle([Species.SNORLAX, Species.MEOWTH]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).toBeDefined();
game.move.select(Moves.PARTING_SHOT);
await game.phaseInterceptor.to(BerryPhase, false);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.SNORLAX);
}, TIMEOUT
);
test(
"Parting shot should not switch out if target is +6/+6 buffed and has contrary ability",
async () => {
game.override
.enemySpecies(Species.SHUCKLE)
.enemyAbility(Abilities.CONTRARY)
.enemyMoveset(Array(4).fill(Moves.SPLASH));
game.override.moveset([Moves.PARTING_SHOT, Moves.MEMENTO, Moves.SPLASH]);
await game.startBattle([Species.MEOWTH, Species.MEOWTH, Species.MEOWTH, Species.MURKROW, Species.ABRA]);
// use Memento 3 times to buff enemy
game.move.select(Moves.MEMENTO);
await game.phaseInterceptor.to(FaintPhase);
expect(game.scene.getParty()[0].isFainted()).toBe(true);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to(TurnInitPhase, false);
game.move.select(Moves.MEMENTO);
await game.phaseInterceptor.to(FaintPhase);
expect(game.scene.getParty()[0].isFainted()).toBe(true);
game.doSelectPartyPokemon(2);
await game.phaseInterceptor.to(TurnInitPhase, false);
game.move.select(Moves.MEMENTO);
await game.phaseInterceptor.to(FaintPhase);
expect(game.scene.getParty()[0].isFainted()).toBe(true);
game.doSelectPartyPokemon(3);
// set up done - enemy should be at +6/+6
await game.phaseInterceptor.to(TurnInitPhase, false);
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon).toBeDefined();
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(6);
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(6);
// now parting shot should fail
game.move.select(Moves.PARTING_SHOT);
await game.phaseInterceptor.to(BerryPhase, false);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(6);
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(6);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MURKROW);
}, TIMEOUT
);
test(
"Parting shot should de-buff and not fail if no party available to switch - party size 1", "Parting shot should de-buff and not fail if no party available to switch - party size 1",
async () => { async () => {
game.override
.enemySpecies(Species.MAGIKARP);
await game.startBattle([Species.MURKROW]); await game.startBattle([Species.MURKROW]);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -176,9 +244,11 @@ describe("Moves - Parting Shot", () => {
}, TIMEOUT }, TIMEOUT
); );
it.skip( // TODO: fix this bug to pass the test! test(
"Parting shot regularly not fail if no party available to switch - party fainted", "Parting shot shouldn't fail if no party available to switch - party fainted",
async () => { async () => {
game.override
.enemySpecies(Species.MAGIKARP);
await game.startBattle([Species.MURKROW, Species.MEOWTH]); await game.startBattle([Species.MURKROW, Species.MEOWTH]);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
@ -193,8 +263,8 @@ describe("Moves - Parting Shot", () => {
await game.phaseInterceptor.to(BerryPhase, false); await game.phaseInterceptor.to(BerryPhase, false);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1);
expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH);
}, TIMEOUT }, TIMEOUT
); );