[Move] Implement Substitute (#2559)
* Implement Substitute Squashed commit from working branch * Fix integration test imports * Use Override Helper utils + Fix Baton Pass test * Update src/test/moves/substitute.test.ts Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> * Fix test imports + nits * Document RemoveAllSubstitutesAttr * Fix some strict-null issues * more strict-null fixes * Fix baton pass test * Reorganized Substitute translation keys * Added checks for substitute in contact logic * Clean up Unseen Fist contact logic * Remove misleading comment in Download attr * RIP phases.ts * Fix imports post-phase migration * Rewrite `move.canIgnoreSubstitute` to `move.hitsSubstitute` * Also fixed interactions with Shell Trap and Beak Blast * Removed some leftover `canIgnoreSubstitute`s * fix issues after beta merge * Status move effectiveness now accounts for substitute * More edge case tests (Counter test failing) * Fix Counter + Trap edge cases + add Fail messagesd * Fix leftover nit * Resolve leftover test issues * Fix Sub offset carrying over to Trainer fights * Hide substitute sprite during catch attempts * Make substitutes baton-passable again * Remove placeholder locale keys and SPLASH_ONLY * Fix imports and other nits Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * ESLint * Fix imports * Fix incorrect `resetSprite` timing * Fix substitute disappearing on hit (maybe?) * More animation fixes (mostly for Roar) --------- Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
parent
526f9ae2bc
commit
70295280da
|
@ -64,6 +64,7 @@ import { PlayerGender } from "#enums/player-gender";
|
|||
import { Species } from "#enums/species";
|
||||
import { UiTheme } from "#enums/ui-theme";
|
||||
import { TimedEventManager } from "#app/timed-event-manager";
|
||||
import { PokemonAnimType } from "#enums/pokemon-anim-type";
|
||||
import i18next from "i18next";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { battleSpecDialogue } from "./data/dialogue";
|
||||
|
@ -74,6 +75,7 @@ import { MessagePhase } from "./phases/message-phase";
|
|||
import { MovePhase } from "./phases/move-phase";
|
||||
import { NewBiomeEncounterPhase } from "./phases/new-biome-encounter-phase";
|
||||
import { NextEncounterPhase } from "./phases/next-encounter-phase";
|
||||
import { PokemonAnimPhase } from "./phases/pokemon-anim-phase";
|
||||
import { QuietFormChangePhase } from "./phases/quiet-form-change-phase";
|
||||
import { ReturnPhase } from "./phases/return-phase";
|
||||
import { SelectBiomePhase } from "./phases/select-biome-phase";
|
||||
|
@ -2721,6 +2723,16 @@ export default class BattleScene extends SceneBase {
|
|||
return false;
|
||||
}
|
||||
|
||||
triggerPokemonBattleAnim(pokemon: Pokemon, battleAnimType: PokemonAnimType, fieldAssets?: Phaser.GameObjects.Sprite[], delayed: boolean = false): boolean {
|
||||
const phase: Phase = new PokemonAnimPhase(this, battleAnimType, pokemon, fieldAssets);
|
||||
if (delayed) {
|
||||
this.pushPhase(phase);
|
||||
} else {
|
||||
this.unshiftPhase(phase);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
validateAchvs(achvType: Constructor<Achv>, ...args: unknown[]): void {
|
||||
const filteredAchvs = Object.values(achvs).filter(a => a instanceof achvType);
|
||||
for (const achv of filteredAchvs) {
|
||||
|
|
|
@ -1706,6 +1706,10 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
|
|||
}
|
||||
|
||||
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
|
||||
if (pokemon !== attacker && move.hitsSubstitute(attacker, pokemon)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**Status inflicted by abilities post attacking are also considered additional effects.*/
|
||||
if (!attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !simulated && pokemon !== attacker && (!this.contactRequired || move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance && !pokemon.status) {
|
||||
const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)];
|
||||
|
@ -2064,6 +2068,10 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
|
|||
if (this.intimidate) {
|
||||
applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated);
|
||||
applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated);
|
||||
|
||||
if (opponent.getTag(BattlerTagType.SUBSTITUTE)) {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
if (!cancelled.value) {
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages));
|
||||
|
@ -2143,7 +2151,6 @@ export class DownloadAbAttr extends PostSummonAbAttr {
|
|||
private enemyCountTally: integer;
|
||||
private stats: BattleStat[];
|
||||
|
||||
// TODO: Implement the Substitute feature(s) once move is implemented.
|
||||
/**
|
||||
* Checks to see if it is the opening turn (starting a new game), if so, Download won't work. This is because Download takes into account
|
||||
* vitamins and items, so it needs to use the Stat and the stat alone.
|
||||
|
@ -4781,7 +4788,7 @@ export const allAbilities = [ new Ability(Abilities.NONE, 3) ];
|
|||
export function initAbilities() {
|
||||
allAbilities.push(
|
||||
new Ability(Abilities.STENCH, 3)
|
||||
.attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr(FlinchAttr) ? 10 : 0, BattlerTagType.FLINCHED),
|
||||
.attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr(FlinchAttr) && !move.hitsSubstitute(user, target) ? 10 : 0, BattlerTagType.FLINCHED),
|
||||
new Ability(Abilities.DRIZZLE, 3)
|
||||
.attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN)
|
||||
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN),
|
||||
|
|
|
@ -6,6 +6,7 @@ import * as Utils from "../utils";
|
|||
import { BattlerIndex } from "../battle";
|
||||
import { Element } from "json-stable-stringify";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { SubstituteTag } from "./battler-tags";
|
||||
//import fs from 'vite-plugin-fs/browser';
|
||||
|
||||
export enum AnimFrameTarget {
|
||||
|
@ -700,7 +701,7 @@ export abstract class BattleAnim {
|
|||
return false;
|
||||
}
|
||||
|
||||
private getGraphicFrameData(scene: BattleScene, frames: AnimFrame[]): Map<integer, Map<AnimFrameTarget, GraphicFrameData>> {
|
||||
private getGraphicFrameData(scene: BattleScene, frames: AnimFrame[], onSubstitute?: boolean): Map<integer, Map<AnimFrameTarget, GraphicFrameData>> {
|
||||
const ret: Map<integer, Map<AnimFrameTarget, GraphicFrameData>> = new Map([
|
||||
[AnimFrameTarget.GRAPHIC, new Map<AnimFrameTarget, GraphicFrameData>() ],
|
||||
[AnimFrameTarget.USER, new Map<AnimFrameTarget, GraphicFrameData>() ],
|
||||
|
@ -711,12 +712,15 @@ export abstract class BattleAnim {
|
|||
const user = !isOppAnim ? this.user : this.target;
|
||||
const target = !isOppAnim ? this.target : this.user;
|
||||
|
||||
const targetSubstitute = (onSubstitute && user !== target) ? target!.getTag(SubstituteTag) : null;
|
||||
|
||||
const userInitialX = user!.x; // TODO: is this bang correct?
|
||||
const userInitialY = user!.y; // TODO: is this bang correct?
|
||||
const userHalfHeight = user!.getSprite().displayHeight! / 2; // TODO: is this bang correct?
|
||||
const targetInitialX = target!.x; // TODO: is this bang correct?
|
||||
const targetInitialY = target!.y; // TODO: is this bang correct?
|
||||
const targetHalfHeight = target!.getSprite().displayHeight! / 2; // TODO: is this bang correct?
|
||||
|
||||
const targetInitialX = targetSubstitute?.sprite?.x ?? target!.x; // TODO: is this bang correct?
|
||||
const targetInitialY = targetSubstitute?.sprite?.y ?? target!.y; // TODO: is this bang correct?
|
||||
const targetHalfHeight = (targetSubstitute?.sprite ?? target!.getSprite()).displayHeight! / 2; // TODO: is this bang correct?
|
||||
|
||||
let g = 0;
|
||||
let u = 0;
|
||||
|
@ -754,7 +758,7 @@ export abstract class BattleAnim {
|
|||
return ret;
|
||||
}
|
||||
|
||||
play(scene: BattleScene, callback?: Function) {
|
||||
play(scene: BattleScene, onSubstitute?: boolean, callback?: Function) {
|
||||
const isOppAnim = this.isOppAnim();
|
||||
const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct?
|
||||
const target = !isOppAnim ? this.target : this.user;
|
||||
|
@ -766,8 +770,10 @@ export abstract class BattleAnim {
|
|||
return;
|
||||
}
|
||||
|
||||
const targetSubstitute = (!!onSubstitute && user !== target) ? target.getTag(SubstituteTag) : null;
|
||||
|
||||
const userSprite = user.getSprite();
|
||||
const targetSprite = target.getSprite();
|
||||
const targetSprite = targetSubstitute?.sprite ?? target.getSprite();
|
||||
|
||||
const spriteCache: SpriteCache = {
|
||||
[AnimFrameTarget.GRAPHIC]: [],
|
||||
|
@ -782,16 +788,34 @@ export abstract class BattleAnim {
|
|||
userSprite.setAlpha(1);
|
||||
userSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ];
|
||||
userSprite.setAngle(0);
|
||||
if (!targetSubstitute) {
|
||||
targetSprite.setPosition(0, 0);
|
||||
targetSprite.setScale(1);
|
||||
targetSprite.setAlpha(1);
|
||||
} else {
|
||||
targetSprite.setPosition(
|
||||
target.x - target.getSubstituteOffset()[0],
|
||||
target.y - target.getSubstituteOffset()[1]
|
||||
);
|
||||
targetSprite.setScale(target.getSpriteScale() * (target.isPlayer() ? 0.5 : 1));
|
||||
targetSprite.setAlpha(1);
|
||||
}
|
||||
targetSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ];
|
||||
targetSprite.setAngle(0);
|
||||
if (!this.isHideUser() && userSprite) {
|
||||
this.user?.getSprite().setVisible(true); // using this.user to fix context loss due to isOppAnim swap (#481)
|
||||
|
||||
/**
|
||||
* This and `targetSpriteToShow` are used to restore context lost
|
||||
* from the `isOppAnim` swap. Using these references instead of `this.user`
|
||||
* and `this.target` prevent the target's Substitute doll from disappearing
|
||||
* after being the target of an animation.
|
||||
*/
|
||||
const userSpriteToShow = !isOppAnim ? userSprite : targetSprite;
|
||||
const targetSpriteToShow = !isOppAnim ? targetSprite : userSprite;
|
||||
if (!this.isHideUser() && userSpriteToShow) {
|
||||
userSpriteToShow.setVisible(true);
|
||||
}
|
||||
if (!this.isHideTarget() && (targetSprite !== userSprite || !this.isHideUser())) {
|
||||
this.target?.getSprite().setVisible(true); // using this.target to fix context loss due to isOppAnim swap (#481)
|
||||
if (!this.isHideTarget() && (targetSpriteToShow !== userSpriteToShow || !this.isHideUser())) {
|
||||
targetSpriteToShow.setVisible(true);
|
||||
}
|
||||
for (const ms of Object.values(spriteCache).flat()) {
|
||||
if (ms) {
|
||||
|
@ -814,8 +838,8 @@ export abstract class BattleAnim {
|
|||
|
||||
const userInitialX = user.x;
|
||||
const userInitialY = user.y;
|
||||
const targetInitialX = target.x;
|
||||
const targetInitialY = target.y;
|
||||
const targetInitialX = targetSubstitute?.sprite?.x ?? target.x;
|
||||
const targetInitialY = targetSubstitute?.sprite?.y ?? target.y;
|
||||
|
||||
this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ];
|
||||
this.dstLine = [ userInitialX, userInitialY, targetInitialX, targetInitialY ];
|
||||
|
@ -833,7 +857,7 @@ export abstract class BattleAnim {
|
|||
}
|
||||
|
||||
const spriteFrames = anim!.frames[f]; // TODO: is the bang correcT?
|
||||
const frameData = this.getGraphicFrameData(scene, anim!.frames[f]); // TODO: is the bang correct?
|
||||
const frameData = this.getGraphicFrameData(scene, anim!.frames[f], onSubstitute); // TODO: is the bang correct?
|
||||
let u = 0;
|
||||
let t = 0;
|
||||
let g = 0;
|
||||
|
@ -846,6 +870,7 @@ export abstract class BattleAnim {
|
|||
const sprites = spriteCache[isUser ? AnimFrameTarget.USER : AnimFrameTarget.TARGET];
|
||||
const spriteSource = isUser ? userSprite : targetSprite;
|
||||
if ((isUser ? u : t) === sprites.length) {
|
||||
if (!isUser && !!targetSubstitute) {
|
||||
const sprite = scene.addPokemonSprite(isUser ? user! : target, 0, 0, spriteSource!.texture, spriteSource!.frame.name, true); // TODO: are those bangs correct?
|
||||
[ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = (isUser ? user! : target).getSprite().pipelineData[k]); // TODO: are those bangs correct?
|
||||
sprite.setPipelineData("spriteKey", (isUser ? user! : target).getBattleSpriteKey());
|
||||
|
@ -855,15 +880,24 @@ export abstract class BattleAnim {
|
|||
spriteSource.on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame));
|
||||
scene.field.add(sprite);
|
||||
sprites.push(sprite);
|
||||
} else {
|
||||
const sprite = scene.addFieldSprite(spriteSource.x, spriteSource.y, spriteSource.texture);
|
||||
spriteSource.on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame));
|
||||
scene.field.add(sprite);
|
||||
sprites.push(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
const spriteIndex = isUser ? u++ : t++;
|
||||
const pokemonSprite = sprites[spriteIndex];
|
||||
const graphicFrameData = frameData.get(frame.target)!.get(spriteIndex)!; // TODO: are the bangs correct?
|
||||
pokemonSprite.setPosition(graphicFrameData.x, graphicFrameData.y - ((spriteSource.height / 2) * (spriteSource.parentContainer.scale - 1)));
|
||||
const spriteSourceScale = (isUser || !targetSubstitute)
|
||||
? spriteSource.parentContainer.scale
|
||||
: target.getSpriteScale() * (target.isPlayer() ? 0.5 : 1);
|
||||
pokemonSprite.setPosition(graphicFrameData.x, graphicFrameData.y - ((spriteSource.height / 2) * (spriteSourceScale - 1)));
|
||||
|
||||
pokemonSprite.setAngle(graphicFrameData.angle);
|
||||
pokemonSprite.setScale(graphicFrameData.scaleX * spriteSource.parentContainer.scale, graphicFrameData.scaleY * spriteSource.parentContainer.scale);
|
||||
pokemonSprite.setScale(graphicFrameData.scaleX * spriteSourceScale, graphicFrameData.scaleY * spriteSourceScale);
|
||||
|
||||
pokemonSprite.setData("locked", frame.locked);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { MovePhase } from "#app/phases/move-phase";
|
|||
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";
|
||||
|
||||
export enum BattlerTagLapseType {
|
||||
FAINT,
|
||||
|
@ -30,6 +31,7 @@ export enum BattlerTagLapseType {
|
|||
AFTER_MOVE,
|
||||
MOVE_EFFECT,
|
||||
TURN_END,
|
||||
HIT,
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
|
@ -391,10 +393,12 @@ export class BeakBlastChargingTag extends BattlerTag {
|
|||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||
const effectPhase = pokemon.scene.getCurrentPhase();
|
||||
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
|
||||
if (effectPhase instanceof MoveEffectPhase) {
|
||||
const attacker = effectPhase.getPokemon();
|
||||
if (effectPhase.move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) {
|
||||
attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.lapse(pokemon, lapseType);
|
||||
|
@ -451,10 +455,14 @@ export class TrappedTag extends BattlerTag {
|
|||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
const source = pokemon.scene.getPokemonById(this.sourceId!)!;
|
||||
const move = allMoves[this.sourceMove];
|
||||
|
||||
const isGhost = pokemon.isOfType(Type.GHOST);
|
||||
const isTrapped = pokemon.getTag(TrappedTag);
|
||||
const hasSubstitute = move.hitsSubstitute(source, pokemon);
|
||||
|
||||
return !isTrapped && !isGhost;
|
||||
return !isTrapped && !isGhost && !hasSubstitute;
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
|
@ -1121,7 +1129,7 @@ export abstract class DamagingTrapTag extends TrappedTag {
|
|||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
return !pokemon.getTag(TrappedTag);
|
||||
return !pokemon.getTag(TrappedTag) && !pokemon.getTag(BattlerTagType.SUBSTITUTE);
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
|
@ -2007,7 +2015,6 @@ export class FormBlockDamageTag extends BattlerTag {
|
|||
pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger);
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides the additional weather-based effects of the Ice Face ability */
|
||||
export class IceFaceBlockDamageTag extends FormBlockDamageTag {
|
||||
constructor(tagType: BattlerTagType) {
|
||||
|
@ -2055,7 +2062,6 @@ export class StockpilingTag extends BattlerTag {
|
|||
if (defChange) {
|
||||
this.statChangeCounts[Stat.DEF]++;
|
||||
}
|
||||
|
||||
if (spDefChange) {
|
||||
this.statChangeCounts[Stat.SPDEF]++;
|
||||
}
|
||||
|
@ -2211,6 +2217,93 @@ export class TarShotTag extends BattlerTag {
|
|||
}
|
||||
}
|
||||
|
||||
export class SubstituteTag extends BattlerTag {
|
||||
/** The substitute's remaining HP. If HP is depleted, the Substitute fades. */
|
||||
public hp: number;
|
||||
/** A reference to the sprite representing the Substitute doll */
|
||||
public sprite: Phaser.GameObjects.Sprite;
|
||||
/** Is the source Pokemon "in focus," i.e. is it fully visible on the field? */
|
||||
public sourceInFocus: boolean;
|
||||
|
||||
constructor(sourceMove: Moves, sourceId: integer) {
|
||||
super(BattlerTagType.SUBSTITUTE, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE, BattlerTagLapseType.HIT], 0, sourceMove, sourceId, true);
|
||||
}
|
||||
|
||||
/** Sets the Substitute's HP and queues an on-add battle animation that initializes the Substitute's sprite. */
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
this.hp = Math.floor(pokemon.scene.getPokemonById(this.sourceId!)!.getMaxHp() / 4);
|
||||
this.sourceInFocus = false;
|
||||
|
||||
// Queue battle animation and message
|
||||
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_ADD);
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500);
|
||||
|
||||
// Remove any trapping effects from the user
|
||||
pokemon.findAndRemoveTags(tag => tag instanceof TrappedTag);
|
||||
}
|
||||
|
||||
/** Queues an on-remove battle animation that removes the Substitute's sprite. */
|
||||
onRemove(pokemon: Pokemon): void {
|
||||
// Only play the animation if the cause of removal isn't from the source's own move
|
||||
if (!this.sourceInFocus) {
|
||||
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_REMOVE, [this.sprite]);
|
||||
} else {
|
||||
this.sprite.destroy();
|
||||
}
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnRemove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
switch (lapseType) {
|
||||
case BattlerTagLapseType.PRE_MOVE:
|
||||
this.onPreMove(pokemon);
|
||||
break;
|
||||
case BattlerTagLapseType.AFTER_MOVE:
|
||||
this.onAfterMove(pokemon);
|
||||
break;
|
||||
case BattlerTagLapseType.HIT:
|
||||
this.onHit(pokemon);
|
||||
break;
|
||||
}
|
||||
return lapseType !== BattlerTagLapseType.CUSTOM; // only remove this tag on custom lapse
|
||||
}
|
||||
|
||||
/** Triggers an animation that brings the Pokemon into focus before it uses a move */
|
||||
onPreMove(pokemon: Pokemon): void {
|
||||
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_PRE_MOVE, [this.sprite]);
|
||||
this.sourceInFocus = true;
|
||||
}
|
||||
|
||||
/** Triggers an animation that brings the Pokemon out of focus after it uses a move */
|
||||
onAfterMove(pokemon: Pokemon): void {
|
||||
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_POST_MOVE, [this.sprite]);
|
||||
this.sourceInFocus = false;
|
||||
}
|
||||
|
||||
/** If the Substitute redirects damage, queue a message to indicate it. */
|
||||
onHit(pokemon: Pokemon): void {
|
||||
const moveEffectPhase = pokemon.scene.getCurrentPhase();
|
||||
if (moveEffectPhase instanceof MoveEffectPhase) {
|
||||
const attacker = moveEffectPhase.getUserPokemon()!;
|
||||
const move = moveEffectPhase.move.getMove();
|
||||
const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft);
|
||||
|
||||
if (firstHit && move.hitsSubstitute(attacker, pokemon)) {
|
||||
pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnHit", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When given a battler tag or json representing one, load the data for it.
|
||||
* @param {BattlerTag | any} source A battler tag
|
||||
*/
|
||||
loadTag(source: BattlerTag | any): void {
|
||||
super.loadTag(source);
|
||||
this.hp = source.hp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
|
||||
*
|
||||
|
@ -2370,6 +2463,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
|||
return new ThroatChoppedTag();
|
||||
case BattlerTagType.GORILLA_TACTICS:
|
||||
return new GorillaTacticsTag();
|
||||
case BattlerTagType.SUBSTITUTE:
|
||||
return new SubstituteTag(sourceMove, sourceId);
|
||||
case BattlerTagType.NONE:
|
||||
default:
|
||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||
|
|
227
src/data/move.ts
227
src/data/move.ts
|
@ -1,5 +1,5 @@
|
|||
import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims";
|
||||
import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags";
|
||||
import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, SubstituteTag, TypeBoostTag } from "./battler-tags";
|
||||
import { getPokemonNameWithAffix } from "../messages";
|
||||
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon";
|
||||
import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects } from "./status-effect";
|
||||
|
@ -116,8 +116,10 @@ export enum MoveFlags {
|
|||
IGNORE_ABILITIES = 1 << 16,
|
||||
/** Enables all hits of a multi-hit move to be accuracy checked individually */
|
||||
CHECK_ALL_HITS = 1 << 17,
|
||||
/** Indicates a move is able to bypass its target's Substitute (if the target has one) */
|
||||
IGNORE_SUBSTITUTE = 1 << 18,
|
||||
/** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */
|
||||
REDIRECT_COUNTER = 1 << 18,
|
||||
REDIRECT_COUNTER = 1 << 19,
|
||||
}
|
||||
|
||||
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
|
||||
|
@ -333,6 +335,22 @@ export default class Move implements Localizable {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the move would hit its target's Substitute instead of the target itself.
|
||||
* @param user The {@linkcode Pokemon} using this move
|
||||
* @param target The {@linkcode Pokemon} targeted by this move
|
||||
* @returns `true` if the move can bypass the target's Substitute; `false` otherwise.
|
||||
*/
|
||||
hitsSubstitute(user: Pokemon, target: Pokemon | null): boolean {
|
||||
if (this.moveTarget === MoveTarget.USER || !target?.getTag(BattlerTagType.SUBSTITUTE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !user.hasAbility(Abilities.INFILTRATOR)
|
||||
&& !this.hasFlag(MoveFlags.SOUND_BASED)
|
||||
&& !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a move condition to the move
|
||||
* @param condition {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}, appends to conditions array a new MoveCondition object
|
||||
|
@ -576,6 +594,17 @@ export default class Move implements Localizable {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@linkcode MoveFlags.IGNORE_SUBSTITUTE} flag for the calling Move
|
||||
* @param ignoresSubstitute The value (boolean) to set the flag to
|
||||
* example: @see {@linkcode Moves.WHIRLWIND}
|
||||
* @returns The {@linkcode Move} that called this function
|
||||
*/
|
||||
ignoresSubstitute(ignoresSubstitute: boolean = true): this {
|
||||
this.setFlag(MoveFlags.IGNORE_SUBSTITUTE, ignoresSubstitute);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@linkcode MoveFlags.REDIRECT_COUNTER} flag for the calling Move
|
||||
* @param redirectCounter The value (boolean) to set the flag to
|
||||
|
@ -598,7 +627,7 @@ export default class Move implements Localizable {
|
|||
// special cases below, eg: if the move flag is MAKES_CONTACT, and the user pokemon has an ability that ignores contact (like "Long Reach"), then overrides and move does not make contact
|
||||
switch (flag) {
|
||||
case MoveFlags.MAKES_CONTACT:
|
||||
if (user.hasAbilityWithAttr(IgnoreContactAbAttr)) {
|
||||
if (user.hasAbilityWithAttr(IgnoreContactAbAttr) || this.hitsSubstitute(user, target)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
@ -612,8 +641,8 @@ export default class Move implements Localizable {
|
|||
}
|
||||
break;
|
||||
case MoveFlags.IGNORE_PROTECT:
|
||||
if (user.hasAbilityWithAttr(IgnoreProtectOnContactAbAttr) &&
|
||||
this.checkFlag(MoveFlags.MAKES_CONTACT, user, target)) {
|
||||
if (user.hasAbilityWithAttr(IgnoreProtectOnContactAbAttr)
|
||||
&& this.checkFlag(MoveFlags.MAKES_CONTACT, user, null)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
@ -1446,6 +1475,58 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to put in a {@link https://bulbapedia.bulbagarden.net/wiki/Substitute_(doll) | Substitute Doll}
|
||||
* for the user.
|
||||
* @extends MoveEffectAttr
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class AddSubstituteAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes 1/4 of the user's maximum HP (rounded down) to create a substitute for the user
|
||||
* @param user the {@linkcode Pokemon} that used the move.
|
||||
* @param target n/a
|
||||
* @param move the {@linkcode Move} with this attribute.
|
||||
* @param args n/a
|
||||
* @returns true if the attribute successfully applies, false otherwise
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hpCost = Math.floor(user.getMaxHp() / 4);
|
||||
user.damageAndUpdate(hpCost, HitResult.OTHER, false, true, true);
|
||||
user.addTag(BattlerTagType.SUBSTITUTE, 0, move.id, user.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
if (user.isBoss()) {
|
||||
return -10;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => !user.getTag(SubstituteTag) && user.hp > Math.floor(user.getMaxHp() / 4) && user.getMaxHp() > 1;
|
||||
}
|
||||
|
||||
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
|
||||
if (user.getTag(SubstituteTag)) {
|
||||
return i18next.t("moveTriggers:substituteOnOverlap", { pokemonName: getPokemonNameWithAffix(user) });
|
||||
} else if (user.hp <= Math.floor(user.getMaxHp() / 4) || user.getMaxHp() === 1) {
|
||||
return i18next.t("moveTriggers:substituteNotEnoughHp");
|
||||
} else {
|
||||
return i18next.t("battle:attackFailed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum MultiHitType {
|
||||
_2,
|
||||
_2_TO_5,
|
||||
|
@ -1949,6 +2030,10 @@ export class StatusEffectAttr extends MoveEffectAttr {
|
|||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance;
|
||||
if (statusCheck) {
|
||||
|
@ -2048,6 +2133,9 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
|||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
return new Promise<boolean>(resolve => {
|
||||
if (move.hitsSubstitute(user, target)) {
|
||||
return resolve(false);
|
||||
}
|
||||
const rand = Phaser.Math.RND.realInRange(0, 1);
|
||||
if (rand >= this.chance) {
|
||||
return resolve(false);
|
||||
|
@ -2117,6 +2205,10 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (move.hitsSubstitute(user, target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft
|
||||
|
||||
|
@ -2236,6 +2328,9 @@ export class StealEatBerryAttr extends EatBerryAttr {
|
|||
* @returns {boolean} true if the function succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (move.hitsSubstitute(user, target)) {
|
||||
return false;
|
||||
}
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // check for abilities that block item theft
|
||||
if (cancelled.value === true) {
|
||||
|
@ -2286,6 +2381,10 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Special edge case for shield dust blocking Sparkling Aria curing burn
|
||||
const moveTargets = getMoveTargets(user, move.id);
|
||||
if (target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && move.id === Moves.SPARKLING_ARIA && moveTargets.targets.length === 1) {
|
||||
|
@ -2463,7 +2562,7 @@ export class ChargeAttr extends OverrideMoveEffectAttr {
|
|||
const lastMove = user.getLastXMoves().find(() => true);
|
||||
if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && lastMove.turn !== user.scene.currentBattle.turn)) {
|
||||
(args[0] as Utils.BooleanHolder).value = true;
|
||||
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, () => {
|
||||
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
|
||||
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
|
||||
if (this.tagType) {
|
||||
user.addTag(this.tagType, 1, move.id, user.id);
|
||||
|
@ -2563,7 +2662,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
|||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
if (args.length < 2 || !args[1]) {
|
||||
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, () => {
|
||||
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
|
||||
(args[0] as Utils.BooleanHolder).value = true;
|
||||
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
|
||||
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
|
||||
|
@ -2597,6 +2696,10 @@ export class StatStageChangeAttr extends MoveEffectAttr {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
|
||||
const stages = this.getLevels(user);
|
||||
|
@ -2793,9 +2896,11 @@ export class ResetStatsAttr extends MoveEffectAttr {
|
|||
activePokemon.forEach(p => promises.push(this.resetStats(p)));
|
||||
target.scene.queueMessage(i18next.t("moveTriggers:statEliminated"));
|
||||
} else { // Affects only the single target when Clear Smog is used
|
||||
if (!move.hitsSubstitute(user, target)) {
|
||||
promises.push(this.resetStats(target));
|
||||
target.scene.queueMessage(i18next.t("moveTriggers:resetStats", {pokemonName: getPokemonNameWithAffix(target)}));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
return true;
|
||||
|
@ -4621,6 +4726,13 @@ export class FlinchAttr extends AddBattlerTagAttr {
|
|||
constructor() {
|
||||
super(BattlerTagType.FLINCHED, false);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!move.hitsSubstitute(user, target)) {
|
||||
return super.apply(user, target, move, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfuseAttr extends AddBattlerTagAttr {
|
||||
|
@ -4636,8 +4748,11 @@ export class ConfuseAttr extends AddBattlerTagAttr {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!move.hitsSubstitute(user, target)) {
|
||||
return super.apply(user, target, move, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class RechargeAttr extends AddBattlerTagAttr {
|
||||
|
@ -4710,6 +4825,36 @@ export class FaintCountdownAttr extends AddBattlerTagAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to remove all Substitutes from the field.
|
||||
* @extends MoveEffectAttr
|
||||
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Tidy_Up_(move) | Tidy Up}
|
||||
* @see {@linkcode SubstituteTag}
|
||||
*/
|
||||
export class RemoveAllSubstitutesAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove's the Substitute Doll effect from all active Pokemon on the field
|
||||
* @param user {@linkcode Pokemon} the Pokemon using this move
|
||||
* @param target n/a
|
||||
* @param move {@linkcode Move} the move applying this effect
|
||||
* @param args n/a
|
||||
* @returns `true` if the effect successfully applies
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
user.scene.getField(true).forEach(pokemon =>
|
||||
pokemon.findAndRemoveTags(tag => tag.tagType === BattlerTagType.SUBSTITUTE));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used when a move hits a {@linkcode BattlerTagType} for double damage
|
||||
* @extends MoveAttr
|
||||
|
@ -5099,6 +5244,10 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||
const switchOutTarget = (this.user ? user : target);
|
||||
const player = switchOutTarget instanceof PlayerPokemon;
|
||||
|
||||
if (!this.user && move.hitsSubstitute(user, target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr) || target.isMax())) {
|
||||
return false;
|
||||
}
|
||||
|
@ -6615,6 +6764,7 @@ export function initMoves() {
|
|||
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
|
||||
.attr(ForceSwitchOutAttr)
|
||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
||||
.ignoresSubstitute()
|
||||
.hidesTarget()
|
||||
.windMove(),
|
||||
new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
|
||||
|
@ -6705,6 +6855,7 @@ export function initMoves() {
|
|||
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
|
||||
.condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined)
|
||||
.ignoresSubstitute()
|
||||
.condition(failOnMaxCondition),
|
||||
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
||||
|
@ -6842,6 +6993,7 @@ export function initMoves() {
|
|||
.attr(LevelDamageAttr),
|
||||
new StatusMove(Moves.MIMIC, Type.NORMAL, -1, 10, -1, 0, 1)
|
||||
.attr(MovesetCopyMoveAttr)
|
||||
.ignoresSubstitute()
|
||||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -2)
|
||||
|
@ -6870,6 +7022,7 @@ export function initMoves() {
|
|||
.attr(AddArenaTagAttr, ArenaTagType.LIGHT_SCREEN, 5, true)
|
||||
.target(MoveTarget.USER_SIDE),
|
||||
new SelfStatusMove(Moves.HAZE, Type.ICE, -1, 30, -1, 0, 1)
|
||||
.ignoresSubstitute()
|
||||
.attr(ResetStatsAttr, true),
|
||||
new StatusMove(Moves.REFLECT, Type.PSYCHIC, -1, 20, -1, 0, 1)
|
||||
.attr(AddArenaTagAttr, ArenaTagType.REFLECT, 5, true)
|
||||
|
@ -7012,14 +7165,14 @@ export function initMoves() {
|
|||
.attr(HighCritAttr)
|
||||
.slicingMove(),
|
||||
new SelfStatusMove(Moves.SUBSTITUTE, Type.NORMAL, -1, 10, -1, 0, 1)
|
||||
.attr(RecoilAttr)
|
||||
.unimplemented(),
|
||||
.attr(AddSubstituteAttr),
|
||||
new AttackMove(Moves.STRUGGLE, Type.NORMAL, MoveCategory.PHYSICAL, 50, -1, 1, -1, 0, 1)
|
||||
.attr(RecoilAttr, true, 0.25, true)
|
||||
.attr(TypelessAttr)
|
||||
.ignoresVirtual()
|
||||
.target(MoveTarget.RANDOM_NEAR_ENEMY),
|
||||
new StatusMove(Moves.SKETCH, Type.NORMAL, -1, 1, -1, 0, 2)
|
||||
.ignoresSubstitute()
|
||||
.attr(SketchAttr)
|
||||
.ignoresVirtual(),
|
||||
new AttackMove(Moves.TRIPLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 10, 90, 10, -1, 0, 2)
|
||||
|
@ -7045,12 +7198,14 @@ export function initMoves() {
|
|||
.soundBased(),
|
||||
new StatusMove(Moves.CURSE, Type.GHOST, -1, 10, -1, 0, 2)
|
||||
.attr(CurseAttr)
|
||||
.ignoresSubstitute()
|
||||
.ignoresProtect(true)
|
||||
.target(MoveTarget.CURSE),
|
||||
new AttackMove(Moves.FLAIL, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2)
|
||||
.attr(LowHpPowerAttr),
|
||||
new StatusMove(Moves.CONVERSION_2, Type.NORMAL, -1, 30, -1, 0, 2)
|
||||
.attr(ResistLastMoveTypeAttr)
|
||||
.ignoresSubstitute()
|
||||
.partial(), // Checks the move's original typing and not if its type is changed through some other means
|
||||
new AttackMove(Moves.AEROBLAST, Type.FLYING, MoveCategory.SPECIAL, 100, 95, 5, -1, 0, 2)
|
||||
.windMove()
|
||||
|
@ -7062,6 +7217,7 @@ export function initMoves() {
|
|||
new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2)
|
||||
.attr(LowHpPowerAttr),
|
||||
new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2)
|
||||
.ignoresSubstitute()
|
||||
.attr(ReducePpMoveAttr, 4),
|
||||
new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2)
|
||||
.attr(StatusEffectAttr, StatusEffect.FREEZE)
|
||||
|
@ -7095,7 +7251,8 @@ export function initMoves() {
|
|||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.ballBombMove(),
|
||||
new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2)
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST),
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
|
||||
.ignoresSubstitute(),
|
||||
new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2)
|
||||
.ignoresProtect()
|
||||
.attr(DestinyBondAttr)
|
||||
|
@ -7164,6 +7321,7 @@ export function initMoves() {
|
|||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
|
||||
new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INFATUATED)
|
||||
.ignoresSubstitute()
|
||||
.condition((user, target, move) => user.isOppositeGender(target)),
|
||||
new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2)
|
||||
.attr(BypassSleepAttr)
|
||||
|
@ -7209,6 +7367,7 @@ export function initMoves() {
|
|||
.hidesUser(),
|
||||
new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
|
||||
.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(),
|
||||
|
@ -7267,6 +7426,7 @@ export function initMoves() {
|
|||
.attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.SPECIAL, 2)
|
||||
.target(MoveTarget.ATTACKER),
|
||||
new StatusMove(Moves.PSYCH_UP, Type.NORMAL, -1, 10, -1, 0, 2)
|
||||
.ignoresSubstitute()
|
||||
.attr(CopyStatsAttr),
|
||||
new AttackMove(Moves.EXTREME_SPEED, Type.NORMAL, MoveCategory.PHYSICAL, 80, 100, 5, -1, 2, 2),
|
||||
new AttackMove(Moves.ANCIENT_POWER, Type.ROCK, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 2)
|
||||
|
@ -7315,6 +7475,7 @@ export function initMoves() {
|
|||
.attr(WeatherChangeAttr, WeatherType.HAIL)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1)
|
||||
|
@ -7345,13 +7506,16 @@ export function initMoves() {
|
|||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 1, true)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false),
|
||||
new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND)
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.NEAR_ALLY),
|
||||
new StatusMove(Moves.TRICK, Type.PSYCHIC, 100, 10, -1, 0, 3)
|
||||
.unimplemented(),
|
||||
new StatusMove(Moves.ROLE_PLAY, Type.PSYCHIC, -1, 10, -1, 0, 3)
|
||||
.ignoresSubstitute()
|
||||
.attr(AbilityCopyAttr),
|
||||
new SelfStatusMove(Moves.WISH, Type.NORMAL, -1, 10, -1, 0, 3)
|
||||
.triageMove()
|
||||
|
@ -7384,8 +7548,10 @@ export function initMoves() {
|
|||
.attr(HpPowerAttr)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new StatusMove(Moves.SKILL_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 3)
|
||||
.ignoresSubstitute()
|
||||
.attr(SwitchAbilitiesAttr),
|
||||
new SelfStatusMove(Moves.IMPRISON, Type.PSYCHIC, -1, 10, -1, 0, 3)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN)
|
||||
|
@ -7470,7 +7636,8 @@ export function initMoves() {
|
|||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE),
|
||||
new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3)
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST),
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
|
||||
.ignoresSubstitute(),
|
||||
new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
||||
.makesContact(false),
|
||||
|
@ -7582,7 +7749,8 @@ export function initMoves() {
|
|||
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4)
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK),
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
|
||||
.ignoresSubstitute(),
|
||||
new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1)
|
||||
.attr(HealStatusEffectAttr, false, StatusEffect.SLEEP),
|
||||
|
@ -7659,6 +7827,7 @@ export function initMoves() {
|
|||
.attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true)
|
||||
.target(MoveTarget.USER_SIDE),
|
||||
new StatusMove(Moves.ME_FIRST, Type.NORMAL, -1, 20, -1, 0, 4)
|
||||
.ignoresSubstitute()
|
||||
.ignoresVirtual()
|
||||
.target(MoveTarget.NEAR_ENEMY)
|
||||
.unimplemented(),
|
||||
|
@ -7666,9 +7835,11 @@ export function initMoves() {
|
|||
.attr(CopyMoveAttr)
|
||||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.POWER_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4)
|
||||
.attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ]),
|
||||
.attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ])
|
||||
.ignoresSubstitute(),
|
||||
new StatusMove(Moves.GUARD_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4)
|
||||
.attr(SwapStatStagesAttr, [ Stat.DEF, Stat.SPDEF ]),
|
||||
.attr(SwapStatStagesAttr, [ Stat.DEF, Stat.SPDEF ])
|
||||
.ignoresSubstitute(),
|
||||
new AttackMove(Moves.PUNISHMENT, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4)
|
||||
.makesContact(true)
|
||||
.attr(PunishmentPowerAttr),
|
||||
|
@ -7682,7 +7853,8 @@ export function initMoves() {
|
|||
.attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES)
|
||||
.target(MoveTarget.ENEMY_SIDE),
|
||||
new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4)
|
||||
.attr(SwapStatStagesAttr, BATTLE_STATS),
|
||||
.attr(SwapStatStagesAttr, BATTLE_STATS)
|
||||
.ignoresSubstitute(),
|
||||
new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
||||
new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4)
|
||||
|
@ -7965,6 +8137,7 @@ export function initMoves() {
|
|||
.attr(AbilityGiveAttr),
|
||||
new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5)
|
||||
.ignoresProtect()
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.NEAR_OTHER)
|
||||
.condition(failIfSingleBattle)
|
||||
.condition((user, target, move) => !target.turnData.acted)
|
||||
|
@ -8007,6 +8180,7 @@ export function initMoves() {
|
|||
.partial() // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/
|
||||
.attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, i18next.t("moveTriggers:tookTargetIntoSky", {pokemonName: "{USER}", targetName: "{TARGET}"}), BattlerTagType.FLYING) // TODO: Add 2nd turn message
|
||||
.condition(failOnGravityCondition)
|
||||
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
|
||||
.ignoresVirtual(),
|
||||
new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
|
||||
|
@ -8021,6 +8195,7 @@ export function initMoves() {
|
|||
new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferrable).reduce((v, m) => v + m.stackCount, 0))),
|
||||
new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5)
|
||||
.ignoresSubstitute()
|
||||
.attr(CopyTypeAttr),
|
||||
new AttackMove(Moves.RETALIATE, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 5, -1, 0, 5)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => {
|
||||
|
@ -8037,6 +8212,7 @@ export function initMoves() {
|
|||
.attr(SacrificialAttrOnHit),
|
||||
new StatusMove(Moves.BESTOW, Type.NORMAL, -1, 15, -1, 0, 5)
|
||||
.ignoresProtect()
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.INFERNO, Type.FIRE, MoveCategory.SPECIAL, 100, 50, 5, 100, 0, 5)
|
||||
.attr(StatusEffectAttr, StatusEffect.BURN),
|
||||
|
@ -8246,13 +8422,15 @@ export function initMoves() {
|
|||
.soundBased()
|
||||
.target(MoveTarget.ALL_NEAR_OTHERS),
|
||||
new StatusMove(Moves.FAIRY_LOCK, Type.FAIRY, -1, 10, -1, 0, 6)
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.BOTH_SIDES)
|
||||
.unimplemented(),
|
||||
new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6)
|
||||
.attr(ProtectAttr, BattlerTagType.KINGS_SHIELD)
|
||||
.condition(failIfLastCondition),
|
||||
new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -1),
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -1)
|
||||
.ignoresSubstitute(),
|
||||
new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
|
||||
.soundBased(),
|
||||
|
@ -8265,7 +8443,8 @@ export function initMoves() {
|
|||
.attr(HealStatusEffectAttr, false, StatusEffect.FREEZE)
|
||||
.attr(StatusEffectAttr, StatusEffect.BURN),
|
||||
new AttackMove(Moves.HYPERSPACE_HOLE, Type.PSYCHIC, MoveCategory.SPECIAL, 80, -1, 5, -1, 0, 6)
|
||||
.ignoresProtect(),
|
||||
.ignoresProtect()
|
||||
.ignoresSubstitute(),
|
||||
new AttackMove(Moves.WATER_SHURIKEN, Type.WATER, MoveCategory.SPECIAL, 15, 100, 20, -1, 1, 6)
|
||||
.attr(MultiHitAttr)
|
||||
.attr(WaterShurikenPowerAttr)
|
||||
|
@ -8277,6 +8456,7 @@ export function initMoves() {
|
|||
.condition(failIfLastCondition),
|
||||
new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 1)
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.NEAR_ALLY),
|
||||
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2),
|
||||
|
@ -8284,6 +8464,7 @@ export function initMoves() {
|
|||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6)
|
||||
.ignoresSubstitute()
|
||||
.powderMove()
|
||||
.unimplemented(),
|
||||
new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
|
||||
|
@ -8292,6 +8473,7 @@ export function initMoves() {
|
|||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false)))
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.USER_AND_ALLIES)
|
||||
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))),
|
||||
new StatusMove(Moves.HAPPY_HOUR, Type.NORMAL, -1, 30, -1, 0, 6) // No animation
|
||||
|
@ -8304,6 +8486,7 @@ export function initMoves() {
|
|||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new SelfStatusMove(Moves.CELEBRATE, Type.NORMAL, -1, 40, -1, 0, 6),
|
||||
new StatusMove(Moves.HOLD_HANDS, Type.NORMAL, -1, 40, -1, 0, 6)
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.NEAR_ALLY),
|
||||
new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -1),
|
||||
|
@ -8349,6 +8532,7 @@ export function initMoves() {
|
|||
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true),
|
||||
new AttackMove(Moves.HYPERSPACE_FURY, Type.DARK, MoveCategory.PHYSICAL, 100, -1, 5, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true)
|
||||
.ignoresSubstitute()
|
||||
.makesContact(false)
|
||||
.ignoresProtect(),
|
||||
/* Unused */
|
||||
|
@ -8508,6 +8692,7 @@ export function initMoves() {
|
|||
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
|
||||
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false)))
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.USER_AND_ALLIES)
|
||||
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))),
|
||||
new AttackMove(Moves.THROAT_CHOP, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7)
|
||||
|
@ -8538,7 +8723,8 @@ export function initMoves() {
|
|||
user.scene.queueMessage(i18next.t("moveTriggers:burnedItselfOut", {pokemonName: getPokemonNameWithAffix(user)}));
|
||||
}),
|
||||
new StatusMove(Moves.SPEED_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 7)
|
||||
.attr(SwapStatAttr, Stat.SPD),
|
||||
.attr(SwapStatAttr, Stat.SPD)
|
||||
.ignoresSubstitute(),
|
||||
new AttackMove(Moves.SMART_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 7),
|
||||
new StatusMove(Moves.PURIFY, Type.POISON, -1, 20, -1, 0, 7)
|
||||
.condition(
|
||||
|
@ -8555,6 +8741,7 @@ export function initMoves() {
|
|||
new AttackMove(Moves.TROP_KICK, Type.GRASS, MoveCategory.PHYSICAL, 70, 100, 15, 100, 0, 7)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -1),
|
||||
new StatusMove(Moves.INSTRUCT, Type.PSYCHIC, -1, 15, -1, 0, 7)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.BEAK_BLAST, Type.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7)
|
||||
.attr(BeakBlastHeaderAttr)
|
||||
|
@ -8622,6 +8809,7 @@ export function initMoves() {
|
|||
new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7)
|
||||
.attr(RechargeAttr),
|
||||
new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7)
|
||||
.ignoresSubstitute()
|
||||
.partial(),
|
||||
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
|
||||
.ignoresAbilities()
|
||||
|
@ -9289,7 +9477,8 @@ export function initMoves() {
|
|||
.target(MoveTarget.BOTH_SIDES),
|
||||
new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true)
|
||||
.attr(RemoveArenaTrapAttr, true),
|
||||
.attr(RemoveArenaTrapAttr, true)
|
||||
.attr(RemoveAllSubstitutesAttr),
|
||||
new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9)
|
||||
.attr(WeatherChangeAttr, WeatherType.SNOW)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
|
|
|
@ -65,6 +65,7 @@ export enum BattlerTagType {
|
|||
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
|
||||
ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
|
||||
DISABLED = "DISABLED",
|
||||
SUBSTITUTE = "SUBSTITUTE",
|
||||
IGNORE_GHOST = "IGNORE_GHOST",
|
||||
IGNORE_DARK = "IGNORE_DARK",
|
||||
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
export enum PokemonAnimType {
|
||||
/**
|
||||
* Adds a Substitute doll to the field in front of a Pokemon.
|
||||
* The Pokemon then moves "out of focus" and becomes semi-transparent.
|
||||
*/
|
||||
SUBSTITUTE_ADD,
|
||||
/** Brings a Pokemon with a Substitute "into focus" before using a move. */
|
||||
SUBSTITUTE_PRE_MOVE,
|
||||
/** Brings a Pokemon with a Substitute "out of focus" after using a move. */
|
||||
SUBSTITUTE_POST_MOVE,
|
||||
/**
|
||||
* Removes a Pokemon's Substitute doll from the field.
|
||||
* The Pokemon then moves back to its original position.
|
||||
*/
|
||||
SUBSTITUTE_REMOVE
|
||||
}
|
|
@ -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, 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 } 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";
|
||||
|
@ -58,6 +58,7 @@ import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
|||
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { PokemonAnimType } from "#app/enums/pokemon-anim-type";
|
||||
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
|
||||
|
||||
export enum FieldPosition {
|
||||
|
@ -566,6 +567,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
return 1;
|
||||
}
|
||||
|
||||
/** Resets the pokemon's field sprite properties, including position, alpha, and scale */
|
||||
resetSprite(): void {
|
||||
// Resetting properties should not be shown on the field
|
||||
this.setVisible(false);
|
||||
|
||||
// Reset field position
|
||||
this.setFieldPosition(FieldPosition.CENTER);
|
||||
if (this.isOffsetBySubstitute()) {
|
||||
this.x -= this.getSubstituteOffset()[0];
|
||||
this.y -= this.getSubstituteOffset()[1];
|
||||
}
|
||||
|
||||
// Reset sprite display properties
|
||||
this.setAlpha(1);
|
||||
this.setScale(this.getSpriteScale());
|
||||
}
|
||||
|
||||
getHeldItems(): PokemonHeldItemModifier[] {
|
||||
if (!this.scene) {
|
||||
return [];
|
||||
|
@ -640,6 +658,47 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Pokemon's offset from its current field position in the event that
|
||||
* it has a Substitute doll in effect. The offset is returned in `[ x, y ]` format.
|
||||
* @see {@linkcode SubstituteTag}
|
||||
* @see {@linkcode getFieldPositionOffset}
|
||||
*/
|
||||
getSubstituteOffset(): [ number, number ] {
|
||||
return this.isPlayer() ? [-30, 10] : [30, -10];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the Pokemon's position on the field is offset because
|
||||
* the Pokemon has a Substitute active.
|
||||
* @see {@linkcode SubstituteTag}
|
||||
*/
|
||||
isOffsetBySubstitute(): boolean {
|
||||
const substitute = this.getTag(SubstituteTag);
|
||||
if (substitute) {
|
||||
if (substitute.sprite === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// During the Pokemon's MoveEffect phase, the offset is removed to put the Pokemon "in focus"
|
||||
const currentPhase = this.scene.getCurrentPhase();
|
||||
if (currentPhase instanceof MoveEffectPhase && currentPhase.getPokemon() === this) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** If this Pokemon has a Substitute on the field, removes its sprite from the field. */
|
||||
destroySubstitute(): void {
|
||||
const substitute = this.getTag(SubstituteTag);
|
||||
if (substitute && substitute.sprite) {
|
||||
substitute.sprite.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
setFieldPosition(fieldPosition: FieldPosition, duration?: integer): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
if (fieldPosition === this.fieldPosition) {
|
||||
|
@ -1414,6 +1473,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
applyPreDefendAbAttrs(FullHpResistTypeAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier);
|
||||
}
|
||||
|
||||
if (move.category === MoveCategory.STATUS && move.hitsSubstitute(source, this)) {
|
||||
typeMultiplier.value = 0;
|
||||
}
|
||||
|
||||
return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier;
|
||||
}
|
||||
|
||||
|
@ -2385,6 +2448,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
|
||||
|
||||
if (damage.value) {
|
||||
this.lapseTags(BattlerTagLapseType.HIT);
|
||||
|
||||
const substitute = this.getTag(SubstituteTag);
|
||||
if (substitute && move.hitsSubstitute(source, this)) {
|
||||
substitute.hp -= damage.value;
|
||||
damage.value = 0;
|
||||
}
|
||||
if (this.isFullHp()) {
|
||||
applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage);
|
||||
} else if (!this.isPlayer() && damage.value >= this.hp) {
|
||||
|
@ -2407,15 +2477,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
this.scene.gameData.gameStats.highestDamage = damage.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (damage.value > 0) {
|
||||
source.turnData.damageDealt += damage.value;
|
||||
source.turnData.currDamageDealt = damage.value;
|
||||
this.battleData.hitCount++;
|
||||
const attackResult = { move: move.id, result: result as DamageResult, damage: damage.value, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
|
||||
this.turnData.attacksReceived.unshift(attackResult);
|
||||
|
||||
if (source.isPlayer() && !this.isPlayer()) {
|
||||
this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// want to include is.Fainted() in case multi hit move ends early, still want to render message
|
||||
if (source.turnData.hitsLeft === 1 || this.isFainted()) {
|
||||
|
@ -2440,6 +2514,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
// set splice index here, so future scene queues happen before FaintedPhase
|
||||
this.scene.setPhaseQueueSplice();
|
||||
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
|
||||
this.destroySubstitute();
|
||||
this.resetSummonData();
|
||||
}
|
||||
|
||||
|
@ -2452,7 +2527,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
if (!cancelled.value && typeMultiplier === 0) {
|
||||
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
|
||||
}
|
||||
result = (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS;
|
||||
result = (cancelled.value || typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -2499,6 +2574,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
*/
|
||||
this.scene.setPhaseQueueSplice();
|
||||
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure));
|
||||
this.destroySubstitute();
|
||||
this.resetSummonData();
|
||||
}
|
||||
|
||||
|
@ -3124,6 +3200,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
this.summonData[k] = this.summonDataPrimer[k];
|
||||
}
|
||||
}
|
||||
// If this Pokemon has a Substitute when loading in, play an animation to add its sprite
|
||||
if (this.getTag(SubstituteTag)) {
|
||||
this.scene.triggerPokemonBattleAnim(this, PokemonAnimType.SUBSTITUTE_ADD);
|
||||
this.getTag(SubstituteTag)!.sourceInFocus = false;
|
||||
}
|
||||
this.summonDataPrimer = null;
|
||||
}
|
||||
this.updateInfo();
|
||||
|
@ -3503,21 +3584,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
* info container.
|
||||
*/
|
||||
leaveField(clearEffects: boolean = true, hideInfo: boolean = true) {
|
||||
this.resetSprite();
|
||||
this.resetTurnData();
|
||||
if (clearEffects) {
|
||||
this.destroySubstitute();
|
||||
this.resetSummonData();
|
||||
this.resetBattleData();
|
||||
}
|
||||
if (hideInfo) {
|
||||
this.hideInfo();
|
||||
}
|
||||
this.setVisible(false);
|
||||
this.scene.field.remove(this);
|
||||
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.battleInfo?.destroy();
|
||||
this.destroySubstitute();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
|
|
|
@ -70,5 +70,8 @@
|
|||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} hortet {{stockpiledCount}}!",
|
||||
"disabledOnAdd": " {{moveName}} von {{pokemonNameWithAffix}} wurde blockiert!",
|
||||
"disabledLapse": "{{moveName}} von {{pokemonNameWithAffix}} ist nicht länger blockiert!",
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} ist nun schwach gegenüber Feuer-Attacken!"
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} ist nun schwach gegenüber Feuer-Attacken!",
|
||||
"substituteOnAdd": "Ein Delegator von {{pokemonNameWithAffix}} ist erschienen!",
|
||||
"substituteOnHit": "Der Delegator steckt den Schlag für {{pokemonNameWithAffix}} ein!",
|
||||
"substituteOnRemove": "Der Delegator von {{pokemonNameWithAffix}} hört auf zu wirken!"
|
||||
}
|
||||
|
|
|
@ -70,5 +70,8 @@
|
|||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
"disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!",
|
||||
"disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled.",
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!"
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!",
|
||||
"substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!",
|
||||
"substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!",
|
||||
"substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!"
|
||||
}
|
||||
|
|
|
@ -68,5 +68,7 @@
|
|||
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
|
||||
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
|
||||
"safeguard": "{{targetName}} is protected by Safeguard!",
|
||||
"substituteOnOverlap": "{{pokemonName}} already\nhas a substitute!",
|
||||
"substituteNotEnoughHp": "But it does not have enough HP\nleft to make a substitute!",
|
||||
"afterYou": "{{pokemonName}} took the kind offer!"
|
||||
}
|
|
@ -70,5 +70,8 @@
|
|||
"stockpilingOnAdd": "¡{{pokemonNameWithAffix}} ha reservado energía por {{stockpiledCount}}ª vez!",
|
||||
"disabledOnAdd": "¡Se ha anulado el movimiento {{moveName}}\nde {{pokemonNameWithAffix}}!",
|
||||
"disabledLapse": "¡El movimiento {{moveName}} de {{pokemonNameWithAffix}} ya no está anulado!",
|
||||
"tarShotOnAdd": "¡{{pokemonNameWithAffix}} se ha vuelto débil ante el fuego!"
|
||||
"tarShotOnAdd": "¡{{pokemonNameWithAffix}} se ha vuelto débil ante el fuego!",
|
||||
"substituteOnAdd": "¡{{pokemonNameWithAffix}} creó un sustituto!",
|
||||
"substituteOnHit": "¡El sustituto recibe daño en lugar del {{pokemonNameWithAffix}}!",
|
||||
"substituteOnRemove": "¡El sustituto del {{pokemonNameWithAffix}} se debilitó!"
|
||||
}
|
||||
|
|
|
@ -70,5 +70,8 @@
|
|||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} utilise\nla capacité Stockage {{stockpiledCount}} fois !",
|
||||
"disabledOnAdd": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} est mise sous entrave !",
|
||||
"disabledLapse": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} n’est plus sous entrave !",
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} est maintenant\nvulnérable au feu !"
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} est maintenant\nvulnérable au feu !",
|
||||
"substituteOnAdd": "{{pokemonNameWithAffix}}\ncrée un clone !",
|
||||
"substituteOnHit": "Le clone subit les dégâts à la place\nde {{pokemonNameWithAffix}} !",
|
||||
"substituteOnRemove": "Le clone de {{pokemonNameWithAffix}}\ndisparait…"
|
||||
}
|
||||
|
|
|
@ -70,5 +70,8 @@
|
|||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} ha usato Accumulo per la\n{{stockpiledCount}}ª volta!",
|
||||
"disabledOnAdd": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} è stata bloccata!",
|
||||
"disabledLapse": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} non è più bloccata!",
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} è diventato vulnerabile\nal tipo Fuoco!"
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} è diventato vulnerabile\nal tipo Fuoco!",
|
||||
"substituteOnAdd": "Appare un sostituto di {{pokemonNameWithAffix}}!",
|
||||
"substituteOnHit": "Il sostituto viene colpito al posto di {{pokemonNameWithAffix}}!",
|
||||
"substituteOnRemove": "Il sostituto di {{pokemonNameWithAffix}} svanisce!"
|
||||
}
|
||||
|
|
|
@ -70,5 +70,8 @@
|
|||
"stockpilingOnAdd": "{{pokemonNameWithAffix}}[[는]]\n{{stockpiledCount}}개 비축했다!",
|
||||
"disabledOnAdd": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n사용할 수 없다!",
|
||||
"disabledLapse": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n이제 사용할 수 있다.",
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}}[[는]] 불꽃에 약해졌다!"
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}}[[는]] 불꽃에 약해졌다!",
|
||||
"substituteOnAdd": "{{pokemonNameWithAffix}}의\n대타가 나타났다!",
|
||||
"substituteOnHit": "{{pokemonNameWithAffix}}[[를]] 대신하여\n대타가 공격을 받았다!",
|
||||
"substituteOnRemove": "{{pokemonNameWithAffix}}의\n대타는 사라져 버렸다..."
|
||||
}
|
||||
|
|
|
@ -70,5 +70,8 @@
|
|||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} estocou {{stockpiledCount}}!",
|
||||
"disabledOnAdd": "{{moveName}} de {{pokemonNameWithAffix}}\nfoi desabilitado!",
|
||||
"disabledLapse": "{{moveName}} de {{pokemonNameWithAffix}}\nnão está mais desabilitado.",
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} tornou-se mais fraco ao fogo!"
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}} tornou-se mais fraco ao fogo!",
|
||||
"substituteOnAdd": "{{pokemonNameWithAffix}} colocou um substituto!",
|
||||
"substituteOnHit": "O substituto tomou o dano pelo {{pokemonNameWithAffix}}!",
|
||||
"substituteOnRemove": "O substituto de {{pokemonNameWithAffix}} desbotou!"
|
||||
}
|
||||
|
|
|
@ -70,5 +70,8 @@
|
|||
"stockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!",
|
||||
"disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{{moveName}}!",
|
||||
"disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了!",
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n变得怕火了!"
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n变得怕火了!",
|
||||
"substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出现了!",
|
||||
"substituteOnHit": "替身代替{{pokemonNameWithAffix}}\n承受了攻击!",
|
||||
"substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……"
|
||||
}
|
||||
|
|
|
@ -70,5 +70,8 @@
|
|||
"stockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!",
|
||||
"disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{moveName}}!",
|
||||
"disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了!",
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n變得怕火了!"
|
||||
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n變得怕火了!",
|
||||
"substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出現了!",
|
||||
"substituteOnHit": "替身代替{{pokemonNameWithAffix}}承受了攻擊!",
|
||||
"substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……"
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { Mode } from "#app/ui/ui";
|
|||
import i18next from "i18next";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
import { VictoryPhase } from "./victory-phase";
|
||||
import { SubstituteTag } from "#app/data/battler-tags";
|
||||
|
||||
export class AttemptCapturePhase extends PokemonPhase {
|
||||
private pokeballType: PokeballType;
|
||||
|
@ -36,6 +37,11 @@ export class AttemptCapturePhase extends PokemonPhase {
|
|||
return this.end();
|
||||
}
|
||||
|
||||
const substitute = pokemon.getTag(SubstituteTag);
|
||||
if (substitute) {
|
||||
substitute.sprite.setVisible(false);
|
||||
}
|
||||
|
||||
this.scene.pokeballCounts[this.pokeballType]--;
|
||||
|
||||
this.originalY = pokemon.y;
|
||||
|
@ -165,6 +171,11 @@ export class AttemptCapturePhase extends PokemonPhase {
|
|||
pokemon.setVisible(true);
|
||||
pokemon.untint(250, "Sine.easeOut");
|
||||
|
||||
const substitute = pokemon.getTag(SubstituteTag);
|
||||
if (substitute) {
|
||||
substitute.sprite.setVisible(true);
|
||||
}
|
||||
|
||||
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
|
||||
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
||||
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
|
||||
|
|
|
@ -19,7 +19,7 @@ export class CommonAnimPhase extends PokemonPhase {
|
|||
}
|
||||
|
||||
start() {
|
||||
new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, () => {
|
||||
new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, false, () => {
|
||||
this.end();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export class DamagePhase extends PokemonPhase {
|
|||
this.scene.damageNumberHandler.add(this.getPokemon(), this.amount, this.damageResult, this.critical);
|
||||
}
|
||||
|
||||
if (this.damageResult !== HitResult.OTHER) {
|
||||
if (this.damageResult !== HitResult.OTHER && this.amount > 0) {
|
||||
const flashTimer = this.scene.time.addEvent({
|
||||
delay: 100,
|
||||
repeat: 5,
|
||||
|
|
|
@ -139,7 +139,7 @@ export class FaintPhase extends PokemonPhase {
|
|||
y: pokemon.y + 150,
|
||||
ease: "Sine.easeIn",
|
||||
onComplete: () => {
|
||||
pokemon.setVisible(false);
|
||||
pokemon.resetSprite();
|
||||
pokemon.y -= 150;
|
||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
||||
if (pokemon.isPlayer()) {
|
||||
|
|
|
@ -31,7 +31,9 @@ export class MoveAnimTestPhase extends BattlePhase {
|
|||
initMoveAnim(this.scene, moveId).then(() => {
|
||||
loadMoveAnimAssets(this.scene, [moveId], true)
|
||||
.then(() => {
|
||||
new MoveAnim(moveId, player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!, (player !== (allMoves[moveId] instanceof SelfStatusMove) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!).getBattlerIndex()).play(this.scene, () => { // TODO: are the bangs correct here?
|
||||
const user = player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!;
|
||||
const target = (player !== (allMoves[moveId] instanceof SelfStatusMove)) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!;
|
||||
new MoveAnim(moveId, user, target.getBattlerIndex()).play(this.scene, allMoves[moveId].hitsSubstitute(user, target), () => { // TODO: are the bangs correct here?
|
||||
if (player) {
|
||||
this.playMoveAnim(moveQueue, false);
|
||||
} else {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { BattlerIndex } from "#app/battle";
|
|||
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr } from "#app/data/ability";
|
||||
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
||||
import { MoveAnim } from "#app/data/battle-anims";
|
||||
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag } from "#app/data/battler-tags";
|
||||
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
|
||||
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move";
|
||||
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
|
@ -120,7 +120,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||
const applyAttrs: Promise<void>[] = [];
|
||||
|
||||
// Move animation only needs one target
|
||||
new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, () => { // TODO: is the bang correct here?
|
||||
new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => {
|
||||
/** Has the move successfully hit a target (for damage) yet? */
|
||||
let hasHit: boolean = false;
|
||||
for (const target of targets) {
|
||||
|
@ -246,7 +246,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||
* If the move hit, and the target doesn't have Shield Dust,
|
||||
* apply the chance to flinch the target gained from King's Rock
|
||||
*/
|
||||
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) {
|
||||
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
|
||||
const flinched = new Utils.BooleanHolder(false);
|
||||
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||
if (flinched.value) {
|
||||
|
@ -258,14 +258,19 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
|
||||
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
|
||||
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
|
||||
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tags and tokens
|
||||
// Only apply the following effects if the move was not deflected by a substitute
|
||||
if (move.hitsSubstitute(user, target)) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens
|
||||
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
|
||||
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
|
||||
}
|
||||
target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING);
|
||||
if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) {
|
||||
target.lapseTag(BattlerTagType.SHELL_TRAP);
|
||||
}
|
||||
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
|
||||
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
|
||||
}
|
||||
})).then(() => {
|
||||
// Apply the user's post-attack ability effects
|
||||
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {
|
||||
|
@ -306,7 +311,20 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||
}
|
||||
|
||||
// Wait for all move effects to finish applying, then end this phase
|
||||
Promise.allSettled(applyAttrs).then(() => this.end());
|
||||
Promise.allSettled(applyAttrs).then(() => {
|
||||
/**
|
||||
* Remove the target's substitute (if it exists and has expired)
|
||||
* after all targeted effects have applied.
|
||||
* This prevents blocked effects from applying until after this hit resolves.
|
||||
*/
|
||||
targets.forEach(target => {
|
||||
const substitute = target.getTag(SubstituteTag);
|
||||
if (!!substitute && substitute.hp <= 0) {
|
||||
target.lapseTag(BattlerTagType.SUBSTITUTE);
|
||||
}
|
||||
});
|
||||
this.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -167,6 +167,7 @@ export class MovePhase extends BattlePhase {
|
|||
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
|
||||
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||
moveQueue.shift(); // Remove the second turn of charge moves
|
||||
return this.end();
|
||||
}
|
||||
|
@ -186,6 +187,7 @@ export class MovePhase extends BattlePhase {
|
|||
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
|
||||
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||
|
||||
moveQueue.shift();
|
||||
return this.end();
|
||||
|
|
|
@ -31,7 +31,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
|
|||
pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct?
|
||||
}
|
||||
pokemon.updateInfo(true);
|
||||
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, () => {
|
||||
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => {
|
||||
this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined));
|
||||
if (pokemon.status?.isPostTurn()) {
|
||||
this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex));
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
import BattleScene from "#app/battle-scene";
|
||||
import { SubstituteTag } from "#app/data/battler-tags";
|
||||
import { PokemonAnimType } from "#enums/pokemon-anim-type";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import { BattlePhase } from "#app/phases/battle-phase";
|
||||
|
||||
|
||||
|
||||
export class PokemonAnimPhase extends BattlePhase {
|
||||
/** The type of animation to play in this phase */
|
||||
private key: PokemonAnimType;
|
||||
/** The Pokemon to which this animation applies */
|
||||
private pokemon: Pokemon;
|
||||
/** Any other field sprites affected by this animation */
|
||||
private fieldAssets: Phaser.GameObjects.Sprite[];
|
||||
|
||||
constructor(scene: BattleScene, key: PokemonAnimType, pokemon: Pokemon, fieldAssets?: Phaser.GameObjects.Sprite[]) {
|
||||
super(scene);
|
||||
|
||||
this.key = key;
|
||||
this.pokemon = pokemon;
|
||||
this.fieldAssets = fieldAssets ?? [];
|
||||
}
|
||||
|
||||
start(): void {
|
||||
super.start();
|
||||
|
||||
switch (this.key) {
|
||||
case PokemonAnimType.SUBSTITUTE_ADD:
|
||||
this.doSubstituteAddAnim();
|
||||
break;
|
||||
case PokemonAnimType.SUBSTITUTE_PRE_MOVE:
|
||||
this.doSubstitutePreMoveAnim();
|
||||
break;
|
||||
case PokemonAnimType.SUBSTITUTE_POST_MOVE:
|
||||
this.doSubstitutePostMoveAnim();
|
||||
break;
|
||||
case PokemonAnimType.SUBSTITUTE_REMOVE:
|
||||
this.doSubstituteRemoveAnim();
|
||||
break;
|
||||
default:
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
doSubstituteAddAnim(): void {
|
||||
const substitute = this.pokemon.getTag(SubstituteTag);
|
||||
if (substitute === null) {
|
||||
return this.end();
|
||||
}
|
||||
|
||||
const getSprite = () => {
|
||||
const sprite = this.scene.addFieldSprite(
|
||||
this.pokemon.x + this.pokemon.getSprite().x,
|
||||
this.pokemon.y + this.pokemon.getSprite().y,
|
||||
`pkmn${this.pokemon.isPlayer() ? "__back": ""}__sub`
|
||||
);
|
||||
sprite.setOrigin(0.5, 1);
|
||||
this.scene.field.add(sprite);
|
||||
return sprite;
|
||||
};
|
||||
|
||||
const [ subSprite, subTintSprite ] = [ getSprite(), getSprite() ];
|
||||
const subScale = this.pokemon.getSpriteScale() * (this.pokemon.isPlayer() ? 0.5 : 1);
|
||||
|
||||
subSprite.setVisible(false);
|
||||
subSprite.setScale(subScale);
|
||||
subTintSprite.setTintFill(0xFFFFFF);
|
||||
subTintSprite.setScale(0.01);
|
||||
|
||||
if (this.pokemon.isPlayer()) {
|
||||
this.scene.field.bringToTop(this.pokemon);
|
||||
}
|
||||
|
||||
this.scene.playSound("PRSFX- Transform");
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this.pokemon,
|
||||
duration: 500,
|
||||
x: this.pokemon.x + this.pokemon.getSubstituteOffset()[0],
|
||||
y: this.pokemon.y + this.pokemon.getSubstituteOffset()[1],
|
||||
alpha: 0.5,
|
||||
ease: "Sine.easeIn"
|
||||
});
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: subTintSprite,
|
||||
delay: 250,
|
||||
scale: subScale,
|
||||
ease: "Cubic.easeInOut",
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
subSprite.setVisible(true);
|
||||
this.pokemon.scene.tweens.add({
|
||||
targets: subTintSprite,
|
||||
delay: 250,
|
||||
alpha: 0,
|
||||
ease: "Cubic.easeOut",
|
||||
duration: 1000,
|
||||
onComplete: () => {
|
||||
subTintSprite.destroy();
|
||||
substitute.sprite = subSprite;
|
||||
this.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
doSubstitutePreMoveAnim(): void {
|
||||
if (this.fieldAssets.length !== 1) {
|
||||
return this.end();
|
||||
}
|
||||
|
||||
const subSprite = this.fieldAssets[0];
|
||||
if (subSprite === undefined) {
|
||||
return this.end();
|
||||
}
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: subSprite,
|
||||
alpha: 0,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 500
|
||||
});
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this.pokemon,
|
||||
x: subSprite.x,
|
||||
y: subSprite.y,
|
||||
alpha: 1,
|
||||
ease: "Sine.easeInOut",
|
||||
delay: 250,
|
||||
duration: 500,
|
||||
onComplete: () => this.end()
|
||||
});
|
||||
}
|
||||
|
||||
doSubstitutePostMoveAnim(): void {
|
||||
if (this.fieldAssets.length !== 1) {
|
||||
return this.end();
|
||||
}
|
||||
|
||||
const subSprite = this.fieldAssets[0];
|
||||
if (subSprite === undefined) {
|
||||
return this.end();
|
||||
}
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this.pokemon,
|
||||
x: subSprite.x + this.pokemon.getSubstituteOffset()[0],
|
||||
y: subSprite.y + this.pokemon.getSubstituteOffset()[1],
|
||||
alpha: 0.5,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 500
|
||||
});
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: subSprite,
|
||||
alpha: 1,
|
||||
ease: "Sine.easeInOut",
|
||||
delay: 250,
|
||||
duration: 500,
|
||||
onComplete: () => this.end()
|
||||
});
|
||||
}
|
||||
|
||||
doSubstituteRemoveAnim(): void {
|
||||
if (this.fieldAssets.length !== 1) {
|
||||
return this.end();
|
||||
}
|
||||
|
||||
const subSprite = this.fieldAssets[0];
|
||||
if (subSprite === undefined) {
|
||||
return this.end();
|
||||
}
|
||||
|
||||
const getSprite = () => {
|
||||
const sprite = this.scene.addFieldSprite(
|
||||
subSprite.x,
|
||||
subSprite.y,
|
||||
`pkmn${this.pokemon.isPlayer() ? "__back": ""}__sub`
|
||||
);
|
||||
sprite.setOrigin(0.5, 1);
|
||||
this.scene.field.add(sprite);
|
||||
return sprite;
|
||||
};
|
||||
|
||||
const subTintSprite = getSprite();
|
||||
const subScale = this.pokemon.getSpriteScale() * (this.pokemon.isPlayer() ? 0.5 : 1);
|
||||
subTintSprite.setAlpha(0);
|
||||
subTintSprite.setTintFill(0xFFFFFF);
|
||||
subTintSprite.setScale(subScale);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: subTintSprite,
|
||||
alpha: 1,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
subSprite.destroy();
|
||||
const flashTimer = this.scene.time.addEvent({
|
||||
delay: 100,
|
||||
repeat: 7,
|
||||
startAt: 200,
|
||||
callback: () => {
|
||||
this.scene.playSound("PRSFX- Substitute2.wav");
|
||||
|
||||
subTintSprite.setVisible(flashTimer.repeatCount % 2 === 0);
|
||||
if (!flashTimer.repeatCount) {
|
||||
this.scene.tweens.add({
|
||||
targets: subTintSprite,
|
||||
scale: 0.01,
|
||||
ease: "Sine.cubicEaseIn",
|
||||
duration: 500
|
||||
});
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this.pokemon,
|
||||
x: this.pokemon.x - this.pokemon.getSubstituteOffset()[0],
|
||||
y: this.pokemon.y - this.pokemon.getSubstituteOffset()[1],
|
||||
alpha: 1,
|
||||
ease: "Sine.easeInOut",
|
||||
delay: 250,
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
subTintSprite.destroy();
|
||||
this.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
|
|||
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
|
||||
pokemon.updateInfo();
|
||||
}
|
||||
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, () => this.end());
|
||||
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end());
|
||||
} else {
|
||||
this.end();
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ export class ReturnPhase extends SwitchSummonPhase {
|
|||
onEnd(): void {
|
||||
const pokemon = this.getPokemon();
|
||||
|
||||
pokemon.resetSprite();
|
||||
pokemon.resetTurnData();
|
||||
pokemon.resetSummonData();
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ export class ScanIvsPhase extends PokemonPhase {
|
|||
this.scene.ui.setMode(Mode.CONFIRM, () => {
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
this.scene.ui.clearText();
|
||||
new CommonBattleAnim(CommonAnim.LOCK_ON, pokemon, pokemon).play(this.scene, () => {
|
||||
new CommonBattleAnim(CommonAnim.LOCK_ON, pokemon, pokemon).play(this.scene, false, () => {
|
||||
this.scene.ui.getMessageHandler().promptIvs(pokemon.id, pokemon.ivs, this.shownIvs).then(() => this.end());
|
||||
});
|
||||
}, () => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Command } from "#app/ui/command-ui-handler";
|
|||
import i18next from "i18next";
|
||||
import { PostSummonPhase } from "./post-summon-phase";
|
||||
import { SummonPhase } from "./summon-phase";
|
||||
import { SubstituteTag } from "#app/data/battler-tags";
|
||||
|
||||
export class SwitchSummonPhase extends SummonPhase {
|
||||
private slotIndex: integer;
|
||||
|
@ -65,6 +66,16 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||
|
||||
if (!this.batonPass) {
|
||||
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
|
||||
const substitute = pokemon.getTag(SubstituteTag);
|
||||
if (substitute) {
|
||||
this.scene.tweens.add({
|
||||
targets: substitute.sprite,
|
||||
duration: 250,
|
||||
scale: substitute.sprite.scale * 0.5,
|
||||
ease: "Sine.easeIn",
|
||||
onComplete: () => substitute.sprite.destroy()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.ui.showText(this.player ?
|
||||
|
@ -115,8 +126,18 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||
pokemonName: this.getPokemon().getNameToRender()
|
||||
})
|
||||
);
|
||||
// Ensure improperly persisted summon data (such as tags) is cleared upon switching
|
||||
if (!this.batonPass) {
|
||||
/**
|
||||
* If this switch is passing a Substitute, make the switched Pokemon match the returned Pokemon's state as it left.
|
||||
* Otherwise, clear any persisting tags on the returned Pokemon.
|
||||
*/
|
||||
if (this.batonPass) {
|
||||
const substitute = this.lastPokemon.getTag(SubstituteTag);
|
||||
if (substitute) {
|
||||
switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0];
|
||||
switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1];
|
||||
switchedInPokemon.setAlpha(0.5);
|
||||
}
|
||||
} else {
|
||||
switchedInPokemon.resetBattleData();
|
||||
switchedInPokemon.resetSummonData();
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ 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, test } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { BerryPhase } from "#app/phases/berry-phase";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
|
@ -32,37 +34,57 @@ describe("Abilities - Unseen Fist", () => {
|
|||
game.override.enemyLevel(100);
|
||||
});
|
||||
|
||||
test(
|
||||
"ability causes a contact move to ignore Protect",
|
||||
it(
|
||||
"should cause a contact move to ignore Protect",
|
||||
() => testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, true),
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability does not cause a non-contact move to ignore Protect",
|
||||
it(
|
||||
"should not cause a non-contact move to ignore Protect",
|
||||
() => testUnseenFistHitResult(game, Moves.ABSORB, Moves.PROTECT, false),
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability does not apply if the source has Long Reach",
|
||||
it(
|
||||
"should not apply if the source has Long Reach",
|
||||
() => {
|
||||
game.override.passiveAbility(Abilities.LONG_REACH);
|
||||
testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability causes a contact move to ignore Wide Guard",
|
||||
it(
|
||||
"should cause a contact move to ignore Wide Guard",
|
||||
() => testUnseenFistHitResult(game, Moves.BREAKING_SWIPE, Moves.WIDE_GUARD, true),
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"ability does not cause a non-contact move to ignore Wide Guard",
|
||||
it(
|
||||
"should not cause a non-contact move to ignore Wide Guard",
|
||||
() => testUnseenFistHitResult(game, Moves.BULLDOZE, Moves.WIDE_GUARD, false),
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should cause a contact move to ignore Protect, but not Substitute",
|
||||
async () => {
|
||||
game.override.enemyLevel(1);
|
||||
game.override.moveset([Moves.TACKLE]);
|
||||
|
||||
await game.startBattle();
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id);
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(enemyPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
async function testUnseenFistHitResult(game: GameManager, attackMove: Moves, protectMove: Moves, shouldSucceed: boolean = true): Promise<void> {
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Pokemon, { MoveResult, PokemonTurnData, TurnMove, PokemonMove } from "#app/field/pokemon";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { BattlerTagLapseType, SubstituteTag, TrappedTag } from "#app/data/battler-tags";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { PokemonAnimType } from "#app/enums/pokemon-anim-type";
|
||||
import * as messages from "#app/messages";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
|
||||
vi.mock("#app/battle-scene.js");
|
||||
|
||||
const TIMEOUT = 5 * 1000; // 5 sec timeout
|
||||
|
||||
describe("BattlerTag - SubstituteTag", () => {
|
||||
let mockPokemon: Pokemon;
|
||||
|
||||
describe("onAdd behavior", () => {
|
||||
beforeEach(() => {
|
||||
mockPokemon = {
|
||||
scene: new BattleScene(),
|
||||
hp: 101,
|
||||
id: 0,
|
||||
getMaxHp: vi.fn().mockReturnValue(101) as Pokemon["getMaxHp"],
|
||||
findAndRemoveTags: vi.fn().mockImplementation((tagFilter) => {
|
||||
// simulate a Trapped tag set by another Pokemon, then expect the filter to catch it.
|
||||
const trapTag = new TrappedTag(BattlerTagType.TRAPPED, BattlerTagLapseType.CUSTOM, 0, Moves.NONE, 1);
|
||||
expect(tagFilter(trapTag)).toBeTruthy();
|
||||
return true;
|
||||
}) as Pokemon["findAndRemoveTags"]
|
||||
} as Pokemon;
|
||||
|
||||
vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue("");
|
||||
vi.spyOn(mockPokemon.scene, "getPokemonById").mockImplementation(pokemonId => mockPokemon.id === pokemonId ? mockPokemon : null);
|
||||
});
|
||||
|
||||
it(
|
||||
"sets the tag's HP to 1/4 of the source's max HP (rounded down)",
|
||||
async () => {
|
||||
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true);
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
|
||||
|
||||
subject.onAdd(mockPokemon);
|
||||
|
||||
expect(subject.hp).toBe(25);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"triggers on-add effects that bring the source out of focus",
|
||||
async () => {
|
||||
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation(
|
||||
(pokemon, battleAnimType, fieldAssets?, delayed?) => {
|
||||
expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_ADD);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
|
||||
|
||||
subject.onAdd(mockPokemon);
|
||||
|
||||
expect(subject.sourceInFocus).toBeFalsy();
|
||||
expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1);
|
||||
expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"removes effects that trap the source",
|
||||
async () => {
|
||||
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
|
||||
|
||||
subject.onAdd(mockPokemon);
|
||||
expect(mockPokemon.findAndRemoveTags).toHaveBeenCalledTimes(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
describe("onRemove behavior", () => {
|
||||
beforeEach(() => {
|
||||
mockPokemon = {
|
||||
scene: new BattleScene(),
|
||||
hp: 101,
|
||||
id: 0,
|
||||
isFainted: vi.fn().mockReturnValue(false) as Pokemon["isFainted"]
|
||||
} as Pokemon;
|
||||
|
||||
vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue("");
|
||||
});
|
||||
|
||||
it(
|
||||
"triggers on-remove animation and message",
|
||||
async () => {
|
||||
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
|
||||
subject.sourceInFocus = false;
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation(
|
||||
(pokemon, battleAnimType, fieldAssets?, delayed?) => {
|
||||
expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_REMOVE);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
|
||||
|
||||
subject.onRemove(mockPokemon);
|
||||
|
||||
expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1);
|
||||
expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
describe("lapse behavior", () => {
|
||||
beforeEach(() => {
|
||||
mockPokemon = {
|
||||
scene: new BattleScene(),
|
||||
hp: 101,
|
||||
id: 0,
|
||||
turnData: {acted: true} as PokemonTurnData,
|
||||
getLastXMoves: vi.fn().mockReturnValue([{move: Moves.TACKLE, result: MoveResult.SUCCESS} as TurnMove]) as Pokemon["getLastXMoves"],
|
||||
} as Pokemon;
|
||||
|
||||
vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue("");
|
||||
});
|
||||
|
||||
it(
|
||||
"PRE_MOVE lapse triggers pre-move animation",
|
||||
async () => {
|
||||
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation(
|
||||
(pokemon, battleAnimType, fieldAssets?, delayed?) => {
|
||||
expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_PRE_MOVE);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
|
||||
|
||||
expect(subject.lapse(mockPokemon, BattlerTagLapseType.PRE_MOVE)).toBeTruthy();
|
||||
|
||||
expect(subject.sourceInFocus).toBeTruthy();
|
||||
expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1);
|
||||
expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"AFTER_MOVE lapse triggers post-move animation",
|
||||
async () => {
|
||||
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation(
|
||||
(pokemon, battleAnimType, fieldAssets?, delayed?) => {
|
||||
expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_POST_MOVE);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
|
||||
|
||||
expect(subject.lapse(mockPokemon, BattlerTagLapseType.AFTER_MOVE)).toBeTruthy();
|
||||
|
||||
expect(subject.sourceInFocus).toBeFalsy();
|
||||
expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1);
|
||||
expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
/** TODO: Figure out how to mock a MoveEffectPhase correctly for this test */
|
||||
it.skip(
|
||||
"HIT lapse triggers on-hit message",
|
||||
async () => {
|
||||
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true);
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
|
||||
|
||||
const pokemonMove = {
|
||||
getMove: vi.fn().mockReturnValue(allMoves[Moves.TACKLE]) as PokemonMove["getMove"]
|
||||
} as PokemonMove;
|
||||
|
||||
const moveEffectPhase = {
|
||||
move: pokemonMove,
|
||||
getUserPokemon: vi.fn().mockReturnValue(undefined) as MoveEffectPhase["getUserPokemon"]
|
||||
} as MoveEffectPhase;
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "getCurrentPhase").mockReturnValue(moveEffectPhase);
|
||||
vi.spyOn(allMoves[Moves.TACKLE], "hitsSubstitute").mockReturnValue(true);
|
||||
|
||||
expect(subject.lapse(mockPokemon, BattlerTagLapseType.HIT)).toBeTruthy();
|
||||
|
||||
expect(mockPokemon.scene.triggerPokemonBattleAnim).not.toHaveBeenCalled();
|
||||
expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"CUSTOM lapse flags the tag for removal",
|
||||
async () => {
|
||||
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true);
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
|
||||
|
||||
expect(subject.lapse(mockPokemon, BattlerTagLapseType.CUSTOM)).toBeFalsy();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"Unsupported lapse type does nothing",
|
||||
async () => {
|
||||
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
|
||||
|
||||
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true);
|
||||
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
|
||||
|
||||
expect(subject.lapse(mockPokemon, BattlerTagLapseType.TURN_END)).toBeTruthy();
|
||||
|
||||
expect(mockPokemon.scene.triggerPokemonBattleAnim).not.toHaveBeenCalled();
|
||||
expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,515 @@
|
|||
import { SubstituteTag, TrappedTag } from "#app/data/battler-tags";
|
||||
import { allMoves, StealHeldItemChanceAttr } from "#app/data/move";
|
||||
import { StatusEffect } from "#app/data/status-effect";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { BerryType } from "#app/enums/berry-type";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Species } from "#app/enums/species";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import { Command } from "#app/ui/command-ui-handler";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
|
||||
const TIMEOUT = 20 * 1000; // 20 sec timeout
|
||||
|
||||
describe("Moves - Substitute", () => {
|
||||
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")
|
||||
.moveset([Moves.SUBSTITUTE, Moves.SWORDS_DANCE, Moves.TACKLE, Moves.SPLASH])
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyAbility(Abilities.INSOMNIA)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it(
|
||||
"should cause the user to take damage",
|
||||
async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should redirect enemy attack damage to the Substitute doll",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
|
||||
await game.classicMode.startBattle([Species.SKARMORY]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4));
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
const postSubHp = leadPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(postSubHp);
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should fade after redirecting more damage than its remaining HP",
|
||||
async () => {
|
||||
// Giga Impact OHKOs Magikarp if substitute isn't up
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.GIGA_IMPACT));
|
||||
vi.spyOn(allMoves[Moves.GIGA_IMPACT], "accuracy", "get").mockReturnValue(100);
|
||||
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4));
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
const postSubHp = leadPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.hp).toBe(postSubHp);
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeUndefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should block stat changes from status moves",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.CHARM));
|
||||
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
"should be bypassed by sound-based moves",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.ECHOED_VOICE));
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
const postSubHp = leadPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
expect(leadPokemon.hp).toBeLessThan(postSubHp);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should be bypassed by attackers with Infiltrator",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
game.override.enemyAbility(Abilities.INFILTRATOR);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
const postSubHp = leadPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
expect(leadPokemon.hp).toBeLessThan(postSubHp);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"shouldn't block the user's own status moves",
|
||||
async () => {
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SWORDS_DANCE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
|
||||
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should protect the user from flinching",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.FAKE_OUT));
|
||||
game.override.startingLevel(1); // Ensures the Substitute will break
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should protect the user from being trapped",
|
||||
async () => {
|
||||
vi.spyOn(allMoves[Moves.SAND_TOMB], "accuracy", "get").mockReturnValue(100);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.SAND_TOMB));
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getTag(TrappedTag)).toBeUndefined();
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should prevent the user's stats from being lowered",
|
||||
async () => {
|
||||
vi.spyOn(allMoves[Moves.LIQUIDATION], "chance", "get").mockReturnValue(100);
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.LIQUIDATION));
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should protect the user from being afflicted with status effects",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.NUZZLE));
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.status?.effect).not.toBe(StatusEffect.PARALYSIS);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should prevent the user's items from being stolen",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.THIEF));
|
||||
vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate
|
||||
game.override.startingHeldItems([{name: "BERRY", type: BerryType.SITRUS}]);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getHeldItems().length).toBe(1);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should prevent the user's items from being removed",
|
||||
async () => {
|
||||
game.override.moveset([Moves.KNOCK_OFF]);
|
||||
game.override.enemyHeldItems([{name: "BERRY", type: BerryType.SITRUS}]);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id);
|
||||
const enemyNumItems = enemyPokemon.getHeldItems().length;
|
||||
|
||||
game.move.select(Moves.KNOCK_OFF);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
|
||||
expect(enemyPokemon.getHeldItems().length).toBe(enemyNumItems);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"move effect should prevent the user's berries from being stolen and eaten",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.BUG_BITE));
|
||||
game.override.startingHeldItems([{name: "BERRY", type: BerryType.SITRUS}]);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
const enemyPostAttackHp = enemyPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getHeldItems().length).toBe(1);
|
||||
expect(enemyPokemon.hp).toBe(enemyPostAttackHp);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should prevent the user's stats from being reset by Clear Smog",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.CLEAR_SMOG));
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
|
||||
|
||||
game.move.select(Moves.SWORDS_DANCE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should prevent the user from becoming confused",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.MAGICAL_TORQUE));
|
||||
vi.spyOn(allMoves[Moves.MAGICAL_TORQUE], "chance", "get").mockReturnValue(100);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
|
||||
|
||||
game.move.select(Moves.SWORDS_DANCE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(leadPokemon.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
|
||||
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
"should transfer to the switched in Pokemon when the source uses Baton Pass",
|
||||
async () => {
|
||||
game.override.moveset([Moves.SUBSTITUTE, Moves.BATON_PASS]);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE, Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
|
||||
|
||||
// Simulate a Baton switch for the player this turn
|
||||
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.POKEMON, 1, true);
|
||||
});
|
||||
|
||||
await game.phaseInterceptor.to("MovePhase", false);
|
||||
|
||||
const switchedPokemon = game.scene.getPlayerPokemon()!;
|
||||
const subTag = switchedPokemon.getTag(SubstituteTag)!;
|
||||
expect(subTag).toBeDefined();
|
||||
expect(subTag.hp).toBe(Math.floor(leadPokemon.getMaxHp() * 1/4));
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should prevent the source's Rough Skin from activating when hit",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
game.override.ability(Abilities.ROUGH_SKIN);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should prevent the source's Focus Punch from failing when hit",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
game.override.moveset([Moves.FOCUS_PUNCH]);
|
||||
|
||||
// Make Focus Punch 40 power to avoid a KO
|
||||
vi.spyOn(allMoves[Moves.FOCUS_PUNCH], "calculateBattlePower").mockReturnValue(40);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id);
|
||||
|
||||
game.move.select(Moves.FOCUS_PUNCH);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should not allow Shell Trap to activate when attacked",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
game.override.moveset([Moves.SHELL_TRAP]);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id);
|
||||
|
||||
game.move.select(Moves.SHELL_TRAP);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should not allow Beak Blast to burn opponents when hit",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
game.override.moveset([Moves.BEAK_BLAST]);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id);
|
||||
|
||||
game.move.select(Moves.BEAK_BLAST);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.BURN);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should cause incoming attacks to not activate Counter",
|
||||
async() => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
|
||||
game.override.moveset([Moves.COUNTER]);
|
||||
|
||||
await game.classicMode.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id);
|
||||
|
||||
game.move.select(Moves.COUNTER);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
}
|
||||
);
|
||||
});
|
|
@ -8,6 +8,7 @@ import { Species } from "#enums/species";
|
|||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { SubstituteTag } from "#app/data/battler-tags";
|
||||
|
||||
|
||||
describe("Moves - Tidy Up", () => {
|
||||
|
@ -39,7 +40,7 @@ describe("Moves - Tidy Up", () => {
|
|||
it("spikes are cleared", async () => {
|
||||
game.override.moveset([Moves.SPIKES, Moves.TIDY_UP]);
|
||||
game.override.enemyMoveset([Moves.SPIKES, Moves.SPIKES, Moves.SPIKES, Moves.SPIKES]);
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.SPIKES);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
@ -52,7 +53,7 @@ describe("Moves - Tidy Up", () => {
|
|||
it("stealth rocks are cleared", async () => {
|
||||
game.override.moveset([Moves.STEALTH_ROCK, Moves.TIDY_UP]);
|
||||
game.override.enemyMoveset([Moves.STEALTH_ROCK, Moves.STEALTH_ROCK, Moves.STEALTH_ROCK, Moves.STEALTH_ROCK]);
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.STEALTH_ROCK);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
@ -64,7 +65,7 @@ describe("Moves - Tidy Up", () => {
|
|||
it("toxic spikes are cleared", async () => {
|
||||
game.override.moveset([Moves.TOXIC_SPIKES, Moves.TIDY_UP]);
|
||||
game.override.enemyMoveset([Moves.TOXIC_SPIKES, Moves.TOXIC_SPIKES, Moves.TOXIC_SPIKES, Moves.TOXIC_SPIKES]);
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.TOXIC_SPIKES);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
@ -77,7 +78,7 @@ describe("Moves - Tidy Up", () => {
|
|||
game.override.moveset([Moves.STICKY_WEB, Moves.TIDY_UP]);
|
||||
game.override.enemyMoveset([Moves.STICKY_WEB, Moves.STICKY_WEB, Moves.STICKY_WEB, Moves.STICKY_WEB]);
|
||||
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.STICKY_WEB);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
@ -86,21 +87,26 @@ describe("Moves - Tidy Up", () => {
|
|||
expect(game.scene.arena.getTag(ArenaTagType.STICKY_WEB)).toBeUndefined();
|
||||
}, 20000);
|
||||
|
||||
it.skip("substitutes are cleared", async () => {
|
||||
it("substitutes are cleared", async () => {
|
||||
game.override.moveset([Moves.SUBSTITUTE, Moves.TIDY_UP]);
|
||||
game.override.enemyMoveset([Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE]);
|
||||
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
game.move.select(Moves.SUBSTITUTE);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.move.select(Moves.TIDY_UP);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
// TODO: check for subs here once the move is implemented
|
||||
|
||||
const pokemon = [ game.scene.getPlayerPokemon()!, game.scene.getEnemyPokemon()! ];
|
||||
pokemon.forEach(p => {
|
||||
expect(p).toBeDefined();
|
||||
expect(p!.getTag(SubstituteTag)).toBeUndefined();
|
||||
});
|
||||
}, 20000);
|
||||
|
||||
it("user's stats are raised with no traps set", async () => {
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {Button} from "#enums/buttons";
|
|||
import { Moves } from "#enums/moves";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import { ModifierBar } from "#app/modifier/modifier";
|
||||
import { SubstituteTag } from "#app/data/battler-tags";
|
||||
|
||||
export type TargetSelectCallback = (targets: BattlerIndex[]) => void;
|
||||
|
||||
|
@ -111,7 +112,7 @@ export default class TargetSelectUiHandler extends UiHandler {
|
|||
if (this.targetFlashTween) {
|
||||
this.targetFlashTween.stop();
|
||||
for (const pokemon of multipleTargets) {
|
||||
pokemon.setAlpha(1);
|
||||
pokemon.setAlpha(!!pokemon.getTag(SubstituteTag) ? 0.5 : 1);
|
||||
this.highlightItems(pokemon.id, 1);
|
||||
}
|
||||
}
|
||||
|
@ -162,7 +163,7 @@ export default class TargetSelectUiHandler extends UiHandler {
|
|||
}
|
||||
|
||||
for (const pokemon of this.targetsHighlighted) {
|
||||
pokemon.setAlpha(1);
|
||||
pokemon.setAlpha(!!pokemon.getTag(SubstituteTag) ? 0.5 : 1);
|
||||
this.highlightItems(pokemon.id, 1);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue