mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-06 08:22:16 +02:00
Implement Substitute
Squashed commit from working branch
This commit is contained in:
parent
c1595bf2b7
commit
978816b9ba
@ -66,6 +66,8 @@ import { PlayerGender } from "#enums/player-gender";
|
||||
import { Species } from "#enums/species";
|
||||
import { UiTheme } from "#enums/ui-theme";
|
||||
import { TimedEventManager } from "#app/timed-event-manager.js";
|
||||
import { PokemonAnimPhase } from "./pokemon-anim-phase";
|
||||
import { PokemonAnimType } from "./enums/pokemon-anim-type";
|
||||
import i18next from "i18next";
|
||||
import {TrainerType} from "#enums/trainer-type";
|
||||
import { battleSpecDialogue } from "./data/dialogue";
|
||||
@ -950,10 +952,12 @@ export default class BattleScene extends SceneBase {
|
||||
this.enemyModifierBar.removeAll(true);
|
||||
|
||||
for (const p of this.getParty()) {
|
||||
p.destroySubstitute();
|
||||
p.destroy();
|
||||
}
|
||||
this.party = [];
|
||||
for (const p of this.getEnemyParty()) {
|
||||
p.destroySubstitute();
|
||||
p.destroy();
|
||||
}
|
||||
|
||||
@ -2582,6 +2586,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) {
|
||||
|
@ -1611,6 +1611,10 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
|
||||
}
|
||||
|
||||
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
|
||||
if (pokemon !== attacker && !!pokemon.getTag(BattlerTagType.SUBSTITUTE) && !move.canIgnoreSubstitute(attacker)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**Status inflicted by abilities post attacking are also considered additional effects.*/
|
||||
if (!attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && 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)];
|
||||
@ -1965,6 +1969,10 @@ export class PostSummonStatChangeAbAttr extends PostSummonAbAttr {
|
||||
if (this.intimidate) {
|
||||
applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled);
|
||||
applyAbAttrs(PostIntimidateStatChangeAbAttr, opponent, cancelled);
|
||||
|
||||
if (!!opponent.getTag(BattlerTagType.SUBSTITUTE)) {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
if (!cancelled.value) {
|
||||
const statChangePhase = new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels);
|
||||
@ -4303,7 +4311,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) && (!target.getTag(BattlerTagType.SUBSTITUTE) || move.canIgnoreSubstitute(user)) ? 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 {
|
||||
@ -694,7 +695,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>() ],
|
||||
@ -705,12 +706,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;
|
||||
const userInitialY = user.y;
|
||||
const userHalfHeight = user.getSprite().displayHeight / 2;
|
||||
const targetInitialX = target.x;
|
||||
const targetInitialY = target.y;
|
||||
const targetHalfHeight = target.getSprite().displayHeight / 2;
|
||||
|
||||
const targetInitialX = targetSubstitute?.sprite?.x ?? target.x;
|
||||
const targetInitialY = targetSubstitute?.sprite?.y ?? target.y;
|
||||
const targetHalfHeight = (targetSubstitute?.sprite ?? target.getSprite()).displayHeight / 2;
|
||||
|
||||
let g = 0;
|
||||
let u = 0;
|
||||
@ -748,7 +752,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;
|
||||
const target = !isOppAnim ? this.target : this.user;
|
||||
@ -760,8 +764,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]: [],
|
||||
@ -776,9 +782,18 @@ export abstract class BattleAnim {
|
||||
userSprite.setAlpha(1);
|
||||
userSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ];
|
||||
userSprite.setAngle(0);
|
||||
targetSprite.setPosition(0, 0);
|
||||
targetSprite.setScale(1);
|
||||
targetSprite.setAlpha(1);
|
||||
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()) {
|
||||
@ -808,8 +823,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 ];
|
||||
@ -827,7 +842,7 @@ export abstract class BattleAnim {
|
||||
}
|
||||
|
||||
const spriteFrames = anim.frames[f];
|
||||
const frameData = this.getGraphicFrameData(scene, anim.frames[f]);
|
||||
const frameData = this.getGraphicFrameData(scene, anim.frames[f], onSubstitute);
|
||||
let u = 0;
|
||||
let t = 0;
|
||||
let g = 0;
|
||||
@ -840,24 +855,34 @@ export abstract class BattleAnim {
|
||||
const sprites = spriteCache[isUser ? AnimFrameTarget.USER : AnimFrameTarget.TARGET];
|
||||
const spriteSource = isUser ? userSprite : targetSprite;
|
||||
if ((isUser ? u : t) === sprites.length) {
|
||||
const sprite = scene.addPokemonSprite(isUser ? user : target, 0, 0, spriteSource.texture, spriteSource.frame.name, true);
|
||||
[ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = (isUser ? user : target).getSprite().pipelineData[k]);
|
||||
sprite.setPipelineData("spriteKey", (isUser ? user : target).getBattleSpriteKey());
|
||||
sprite.setPipelineData("shiny", (isUser ? user : target).shiny);
|
||||
sprite.setPipelineData("variant", (isUser ? user : target).variant);
|
||||
sprite.setPipelineData("ignoreFieldPos", true);
|
||||
spriteSource.on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame));
|
||||
scene.field.add(sprite);
|
||||
sprites.push(sprite);
|
||||
if (!isUser && !!targetSubstitute) {
|
||||
const sprite = scene.addPokemonSprite(isUser ? user : target, 0, 0, spriteSource.texture, spriteSource.frame.name, true);
|
||||
[ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = (isUser ? user : target).getSprite().pipelineData[k]);
|
||||
sprite.setPipelineData("spriteKey", (isUser ? user : target).getBattleSpriteKey());
|
||||
sprite.setPipelineData("shiny", (isUser ? user : target).shiny);
|
||||
sprite.setPipelineData("variant", (isUser ? user : target).variant);
|
||||
sprite.setPipelineData("ignoreFieldPos", true);
|
||||
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);
|
||||
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);
|
||||
|
||||
|
@ -18,6 +18,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import i18next from "#app/plugins/i18n.js";
|
||||
import { PokemonAnimType } from "#app/enums/pokemon-anim-type.js";
|
||||
|
||||
export enum BattlerTagLapseType {
|
||||
FAINT,
|
||||
@ -26,6 +27,7 @@ export enum BattlerTagLapseType {
|
||||
AFTER_MOVE,
|
||||
MOVE_EFFECT,
|
||||
TURN_END,
|
||||
HIT,
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
@ -126,8 +128,9 @@ export class TrappedTag extends BattlerTag {
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
const isGhost = pokemon.isOfType(Type.GHOST);
|
||||
const isTrapped = pokemon.getTag(BattlerTagType.TRAPPED);
|
||||
const hasSubstitute = pokemon.getTag(BattlerTagType.SUBSTITUTE);
|
||||
|
||||
return !isTrapped && !isGhost;
|
||||
return !isTrapped && !isGhost && !hasSubstitute;
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
@ -765,7 +768,7 @@ export abstract class DamagingTrapTag extends TrappedTag {
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
return !pokemon.isOfType(Type.GHOST) && !pokemon.findTag(t => t instanceof DamagingTrapTag);
|
||||
return !pokemon.isOfType(Type.GHOST) && !pokemon.findTag(t => t instanceof DamagingTrapTag) && !pokemon.getTag(BattlerTagType.SUBSTITUTE);
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
@ -1558,7 +1561,6 @@ export class IceFaceTag extends BattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Battler tag enabling the Stockpile mechanic. This tag handles:
|
||||
* - Stack tracking, including max limit enforcement (which is replicated in Stockpile for redundancy).
|
||||
@ -1587,7 +1589,6 @@ export class StockpilingTag extends BattlerTag {
|
||||
if (defChange) {
|
||||
this.statChangeCounts[BattleStat.DEF]++;
|
||||
}
|
||||
|
||||
if (spDefChange) {
|
||||
this.statChangeCounts[BattleStat.SPDEF]++;
|
||||
}
|
||||
@ -1647,6 +1648,94 @@ export class StockpilingTag 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);
|
||||
}
|
||||
|
||||
/** 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("battle:battlerTagsSubstituteOnAdd", { 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("battle:battlerTagsSubstituteOnRemove", { 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.canIgnoreSubstitute(attacker)) {
|
||||
pokemon.scene.queueMessage(i18next.t("battle:battlerTagsSubstituteOnHit", { 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;
|
||||
// TODO: load this tag's sprite (or generate a new one upon loading a game)
|
||||
}
|
||||
}
|
||||
|
||||
export function getBattlerTag(tagType: BattlerTagType, turnCount: number, sourceMove: Moves, sourceId: number): BattlerTag {
|
||||
switch (tagType) {
|
||||
case BattlerTagType.RECHARGING:
|
||||
@ -1770,6 +1859,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||
return new StockpilingTag(sourceMove);
|
||||
case BattlerTagType.OCTOLOCK:
|
||||
return new OctolockTag(sourceId);
|
||||
case BattlerTagType.SUBSTITUTE:
|
||||
return new SubstituteTag(sourceMove, sourceId);
|
||||
case BattlerTagType.NONE:
|
||||
default:
|
||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||
|
183
src/data/move.ts
183
src/data/move.ts
@ -1,7 +1,7 @@
|
||||
import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims";
|
||||
import { BattleEndPhase, MoveEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases";
|
||||
import { BattleStat, getBattleStatName } from "./battle-stat";
|
||||
import { EncoreTag, HelpingHandTag, SemiInvulnerableTag, StockpilingTag, TypeBoostTag } from "./battler-tags";
|
||||
import { EncoreTag, HelpingHandTag, SemiInvulnerableTag, StockpilingTag, SubstituteTag, TypeBoostTag } from "./battler-tags";
|
||||
import { getPokemonMessage, getPokemonNameWithAffix } from "../messages";
|
||||
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon";
|
||||
import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects} from "./status-effect";
|
||||
@ -91,11 +91,12 @@ export enum MoveFlags {
|
||||
/**
|
||||
* Enables all hits of a multi-hit move to be accuracy checked individually
|
||||
*/
|
||||
CHECK_ALL_HITS = 1 << 17,
|
||||
CHECK_ALL_HITS = 1 << 17,
|
||||
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;
|
||||
@ -310,6 +311,18 @@ export default class Move implements Localizable {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the move can bypass Substitute to directly hit its target
|
||||
* @param user The {@linkcode Pokemon} using this move
|
||||
* @returns `true` if the move can bypass the target's Substitute; `false` otherwise.
|
||||
*/
|
||||
canIgnoreSubstitute(user: Pokemon): boolean {
|
||||
return this.moveTarget === MoveTarget.USER
|
||||
|| 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
|
||||
@ -553,6 +566,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): 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
|
||||
@ -1351,6 +1375,44 @@ 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() || user.getHpRatio() < 0.25) {
|
||||
return -10;
|
||||
}
|
||||
return Math.ceil(user.getHpRatio() * 10);
|
||||
}
|
||||
}
|
||||
|
||||
export enum MultiHitType {
|
||||
_2,
|
||||
_2_TO_5,
|
||||
@ -1865,6 +1927,10 @@ export class StatusEffectAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!this.selfTarget && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance;
|
||||
if (statusCheck) {
|
||||
@ -1956,6 +2022,9 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
return new Promise<boolean>(resolve => {
|
||||
if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) {
|
||||
return resolve(false);
|
||||
}
|
||||
const rand = Phaser.Math.RND.realInRange(0, 1);
|
||||
if (rand >= this.chance) {
|
||||
return resolve(false);
|
||||
@ -2025,6 +2094,10 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft
|
||||
|
||||
@ -2145,6 +2218,9 @@ export class StealEatBerryAttr extends EatBerryAttr {
|
||||
* @returns {boolean} true if the function succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) {
|
||||
return false;
|
||||
}
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // check for abilities that block item theft
|
||||
if (cancelled.value === true) {
|
||||
@ -2195,6 +2271,10 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.selfTarget && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) {
|
||||
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) {
|
||||
@ -2375,7 +2455,7 @@ export class ChargeAttr extends OverrideMoveEffectAttr {
|
||||
const lastMove = user.getLastXMoves().find(() => true);
|
||||
if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && (this.sameTurn || 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);
|
||||
@ -2482,7 +2562,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 });
|
||||
@ -2518,6 +2598,10 @@ export class StatChangeAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.selfTarget && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
|
||||
const levels = this.getLevels(user);
|
||||
@ -2724,6 +2808,10 @@ export class ResetStatsAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let s = 0; s < target.summonData.battleStats.length; s++) {
|
||||
target.summonData.battleStats[s] = 0;
|
||||
}
|
||||
@ -4373,12 +4461,26 @@ export class FlinchAttr extends AddBattlerTagAttr {
|
||||
constructor() {
|
||||
super(BattlerTagType.FLINCHED, false);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!target.getTag(SubstituteTag) || move.canIgnoreSubstitute(user)) {
|
||||
return super.apply(user, target, move, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfuseAttr extends AddBattlerTagAttr {
|
||||
constructor(selfTarget?: boolean) {
|
||||
super(BattlerTagType.CONFUSED, selfTarget, false, 2, 5);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!target.getTag(SubstituteTag) || move.canIgnoreSubstitute(user)) {
|
||||
return super.apply(user, target, move, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class RechargeAttr extends AddBattlerTagAttr {
|
||||
@ -4451,6 +4553,22 @@ export class FaintCountdownAttr extends AddBattlerTagAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveAllSubstitutesAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
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
|
||||
@ -4837,6 +4955,10 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
const switchOutTarget = (this.user ? user : target);
|
||||
const player = switchOutTarget instanceof PlayerPokemon;
|
||||
|
||||
if (!this.user && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr) || target.isMax())) {
|
||||
return false;
|
||||
}
|
||||
@ -6026,6 +6148,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)
|
||||
@ -6115,6 +6238,7 @@ export function initMoves() {
|
||||
.attr(FixedDamageAttr, 20),
|
||||
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1)
|
||||
.attr(DisableMoveAttr)
|
||||
.ignoresSubstitute()
|
||||
.condition(failOnMaxCondition),
|
||||
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
|
||||
.attr(StatChangeAttr, BattleStat.SPDEF, -1)
|
||||
@ -6251,6 +6375,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(StatChangeAttr, BattleStat.DEF, -2)
|
||||
@ -6279,6 +6404,7 @@ export function initMoves() {
|
||||
.attr(AddArenaTagAttr, ArenaTagType.LIGHT_SCREEN, 5, true)
|
||||
.target(MoveTarget.USER_SIDE),
|
||||
new StatusMove(Moves.HAZE, Type.ICE, -1, 30, -1, 0, 1)
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.BOTH_SIDES)
|
||||
.attr(ResetStatsAttr),
|
||||
new StatusMove(Moves.REFLECT, Type.PSYCHIC, -1, 20, -1, 0, 1)
|
||||
@ -6422,14 +6548,15 @@ export function initMoves() {
|
||||
.attr(HighCritAttr)
|
||||
.slicingMove(),
|
||||
new SelfStatusMove(Moves.SUBSTITUTE, Type.NORMAL, -1, 10, -1, 0, 1)
|
||||
.attr(RecoilAttr)
|
||||
.unimplemented(),
|
||||
.attr(AddSubstituteAttr)
|
||||
.condition((user, target, move) => !user.getTag(SubstituteTag) && user.getHpRatio() > 0.25 && user.getMaxHp() > 1),
|
||||
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)
|
||||
@ -6455,12 +6582,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()
|
||||
@ -6472,6 +6601,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)
|
||||
@ -6502,6 +6632,7 @@ export function initMoves() {
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.ballBombMove(),
|
||||
new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2)
|
||||
.ignoresProtect()
|
||||
@ -6559,6 +6690,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)
|
||||
@ -6604,6 +6736,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(),
|
||||
@ -6662,6 +6795,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)
|
||||
@ -6709,6 +6843,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(StatChangeAttr, BattleStat.SPATK, 1)
|
||||
@ -6738,13 +6873,16 @@ export function initMoves() {
|
||||
.attr(StatChangeAttr, BattleStat.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()
|
||||
@ -6777,8 +6915,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)
|
||||
@ -6860,6 +7000,7 @@ export function initMoves() {
|
||||
.attr(StatChangeAttr, BattleStat.SPATK, -2, true)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE),
|
||||
new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3)
|
||||
.attr(StatChangeAttr, BattleStat.SPD, -1)
|
||||
@ -6969,6 +7110,7 @@ export function initMoves() {
|
||||
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
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)
|
||||
@ -7047,6 +7189,7 @@ export function initMoves() {
|
||||
.target(MoveTarget.USER_SIDE)
|
||||
.unimplemented(),
|
||||
new StatusMove(Moves.ME_FIRST, Type.NORMAL, -1, 20, -1, 0, 4)
|
||||
.ignoresSubstitute()
|
||||
.ignoresVirtual()
|
||||
.target(MoveTarget.NEAR_ENEMY)
|
||||
.unimplemented(),
|
||||
@ -7054,8 +7197,10 @@ export function initMoves() {
|
||||
.attr(CopyMoveAttr)
|
||||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.POWER_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new StatusMove(Moves.GUARD_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.PUNISHMENT, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4)
|
||||
.makesContact(true)
|
||||
@ -7070,6 +7215,7 @@ export function initMoves() {
|
||||
.attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES)
|
||||
.target(MoveTarget.ENEMY_SIDE),
|
||||
new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4)
|
||||
.ignoresSubstitute()
|
||||
.attr(SwapStatsAttr),
|
||||
new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
||||
@ -7352,6 +7498,7 @@ export function initMoves() {
|
||||
.attr(AbilityGiveAttr),
|
||||
new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5)
|
||||
.ignoresProtect()
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5)
|
||||
.soundBased()
|
||||
@ -7389,6 +7536,7 @@ export function initMoves() {
|
||||
new AttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
||||
.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(StatChangeAttr, BattleStat.ATK, 1, true)
|
||||
@ -7403,6 +7551,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)
|
||||
.partial(),
|
||||
@ -7411,6 +7560,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),
|
||||
@ -7616,12 +7766,14 @@ 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),
|
||||
new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6)
|
||||
.attr(StatChangeAttr, BattleStat.ATK, -1),
|
||||
.attr(StatChangeAttr, BattleStat.ATK, -1)
|
||||
.ignoresSubstitute(),
|
||||
new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6)
|
||||
.attr(StatChangeAttr, BattleStat.SPATK, -1)
|
||||
.soundBased(),
|
||||
@ -7634,7 +7786,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)
|
||||
@ -7645,6 +7798,7 @@ export function initMoves() {
|
||||
.attr(ProtectAttr, BattlerTagType.SPIKY_SHIELD),
|
||||
new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6)
|
||||
.attr(StatChangeAttr, BattleStat.SPDEF, 1)
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.NEAR_ALLY),
|
||||
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
|
||||
.attr(StatChangeAttr, BattleStat.SPATK, -2),
|
||||
@ -7652,6 +7806,7 @@ export function initMoves() {
|
||||
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK, BattleStat.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)
|
||||
@ -7660,6 +7815,7 @@ export function initMoves() {
|
||||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
|
||||
.attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.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
|
||||
@ -7672,6 +7828,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(StatChangeAttr, BattleStat.ATK, -1),
|
||||
@ -7717,6 +7874,7 @@ export function initMoves() {
|
||||
.attr(StatChangeAttr, [ BattleStat.DEF, BattleStat.SPDEF ], -1, true),
|
||||
new AttackMove(Moves.HYPERSPACE_FURY, Type.DARK, MoveCategory.PHYSICAL, 100, -1, 5, -1, 0, 6)
|
||||
.attr(StatChangeAttr, BattleStat.DEF, -1, true)
|
||||
.ignoresSubstitute()
|
||||
.makesContact(false)
|
||||
.ignoresProtect(),
|
||||
/* Unused */
|
||||
@ -7875,6 +8033,7 @@ export function initMoves() {
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
|
||||
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
|
||||
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.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)
|
||||
@ -7904,6 +8063,7 @@ 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)
|
||||
.ignoresSubstitute()
|
||||
.unimplemented(),
|
||||
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)
|
||||
@ -7921,6 +8081,7 @@ export function initMoves() {
|
||||
new AttackMove(Moves.TROP_KICK, Type.GRASS, MoveCategory.PHYSICAL, 70, 100, 15, 100, 0, 7)
|
||||
.attr(StatChangeAttr, BattleStat.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, 5, 7)
|
||||
.attr(ChargeAttr, ChargeAnim.BEAK_BLAST_CHARGING, i18next.t("moveTriggers:startedHeatingUpBeak", {pokemonName: "{USER}"}), undefined, false, true, -3)
|
||||
@ -7987,6 +8148,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()
|
||||
@ -8646,7 +8808,8 @@ export function initMoves() {
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9)
|
||||
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.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),
|
||||
|
@ -63,5 +63,6 @@ export enum BattlerTagType {
|
||||
ICE_FACE = "ICE_FACE",
|
||||
STOCKPILING = "STOCKPILING",
|
||||
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
|
||||
ALWAYS_GET_HIT = "ALWAYS_GET_HIT"
|
||||
ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
|
||||
SUBSTITUTE = "SUBSTITUTE"
|
||||
}
|
||||
|
16
src/enums/pokemon-anim-type.ts
Normal file
16
src/enums/pokemon-anim-type.ts
Normal file
@ -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
|
||||
}
|
@ -19,7 +19,7 @@ import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEv
|
||||
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
||||
import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase, MoveEndPhase } from "../phases";
|
||||
import { BattleStat } from "../data/battle-stat";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag } from "../data/battler-tags";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag } from "../data/battler-tags";
|
||||
import { WeatherType } from "../data/weather";
|
||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||
import { ArenaTagSide, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||
@ -51,6 +51,7 @@ import { Biome } from "#enums/biome";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { getPokemonNameWithAffix } from "#app/messages.js";
|
||||
import { PokemonAnimType } from "#app/enums/pokemon-anim-type.js";
|
||||
|
||||
export enum FieldPosition {
|
||||
CENTER,
|
||||
@ -547,6 +548,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 [];
|
||||
@ -621,6 +639,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) {
|
||||
@ -2069,6 +2128,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.canIgnoreSubstitute(source)) {
|
||||
substitute.hp -= damage.value;
|
||||
damage.value = 0;
|
||||
}
|
||||
if (this.isFullHp()) {
|
||||
applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, damage);
|
||||
} else if (!this.isPlayer() && damage.value >= this.hp) {
|
||||
@ -2126,6 +2192,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();
|
||||
}
|
||||
|
||||
@ -2139,6 +2206,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (!typeless) {
|
||||
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier);
|
||||
}
|
||||
if (!!this.getTag(SubstituteTag) && !move.canIgnoreSubstitute(source)) {
|
||||
cancelled.value = true;
|
||||
}
|
||||
if (!cancelled.value) {
|
||||
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, typeMultiplier);
|
||||
defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, move, cancelled, typeMultiplier));
|
||||
@ -2193,6 +2263,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();
|
||||
}
|
||||
|
||||
@ -2765,6 +2836,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();
|
||||
|
@ -155,5 +155,8 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} wurde durch {{moveName}} verletzt!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} nimmt einen Teil seiner KP und legt einen Fluch auf {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} wurde durch den Fluch verletzt!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
"battlerTagsSubstituteOnAdd": "Ein Delegator von {{pokemonNameWithAffix}} ist erschienen!",
|
||||
"battlerTagsSubstituteOnHit": "Der Delegator steckt den Schlag für {{pokemonNameWithAffix}} ein!",
|
||||
"battlerTagsSubstituteOnRemove": "Der Delegator von {{pokemonNameWithAffix}} hört auf zu wirken!"
|
||||
} as const;
|
||||
|
@ -155,5 +155,8 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
"battlerTagsSubstituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!",
|
||||
"battlerTagsSubstituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!",
|
||||
"battlerTagsSubstituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!"
|
||||
} as const;
|
||||
|
@ -155,5 +155,8 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
"battlerTagsSubstituteOnAdd": "¡{{pokemonNameWithAffix}} creó un sustituto!",
|
||||
"battlerTagsSubstituteOnHit": "¡El sustituto recibe daño en lugar del {{pokemonNameWithAffix}}!",
|
||||
"battlerTagsSubstituteOnRemove": "¡El sustituto del {{pokemonNameWithAffix}} se debilitó!"
|
||||
} as const;
|
||||
|
@ -155,5 +155,8 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} est blessé\npar la capacité {{moveName}} !",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} sacrifie des PV\net lance une malédiction sur {{pokemonName}} !",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} est touché par la malédiction !",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
"battlerTagsSubstituteOnAdd": "{{pokemonNameWithAffix}}\ncrée un clone !",
|
||||
"battlerTagsSubstituteOnHit": "Le clone subit les dégâts à la place\nde {{pokemonNameWithAffix}} !",
|
||||
"battlerTagsSubstituteOnRemove": "Le clone de {{pokemonNameWithAffix}}\ndisparait…"
|
||||
} as const;
|
||||
|
@ -155,5 +155,8 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} viene colpito da {{moveName}}!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} ha sacrificato metà dei suoi PS per\nlanciare una maledizione su {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} subisce la maledizione!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} ha usato Accumulo per la\n{{stockpiledCount}}ª volta!"
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} ha usato Accumulo per la\n{{stockpiledCount}}ª volta!",
|
||||
"battlerTagsSubstituteOnAdd": "Appare un sostituto di {{pokemonNameWithAffix}}!",
|
||||
"battlerTagsSubstituteOnHit": "Il sostituto viene colpito al posto di {{pokemonNameWithAffix}}!",
|
||||
"battlerTagsSubstituteOnRemove": "Il sostituto di {{pokemonNameWithAffix}} svanisce!"
|
||||
} as const;
|
||||
|
@ -156,4 +156,7 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}}[[는]] 자신의 체력을 깎아서\n{{pokemonName}}에게 저주를 걸었다!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}}[[는]]\n저주받고 있다!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}}[[는]]\n{{stockpiledCount}}개 비축했다!",
|
||||
"battlerTagsSubstituteOnAdd": "{{pokemonNameWithAffix}}의\n대타가 나타났다!",
|
||||
"battlerTagsSubstituteOnHit": "{{pokemonNameWithAffix}}[[를]] 대신하여\n대타가 공격을 받았다!",
|
||||
"battlerTagsSubstituteOnRemove": "{{pokemonNameWithAffix}}의\n대타는 사라져 버렸다..."
|
||||
} as const;
|
||||
|
@ -156,4 +156,7 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} cortou seus PS pela metade e amaldiçoou {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} foi ferido pelo Curse!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} estocou {{stockpiledCount}}!",
|
||||
"battlerTagsSubstituteOnAdd": "{{pokemonNameWithAffix}} colocou um substituto!",
|
||||
"battlerTagsSubstituteOnHit": "O substituto tomou o dano pelo {{pokemonNameWithAffix}}!",
|
||||
"battlerTagsSubstituteOnRemove": "O substituto de {{pokemonNameWithAffix}} desbotou!"
|
||||
} as const;
|
||||
|
@ -147,5 +147,8 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}}\n受到了{{moveName}}的伤害!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}}削减了自己的体力,\n并诅咒了{{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}}\n正受到诅咒!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!"
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!",
|
||||
"battlerTagsSubstituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出现了!",
|
||||
"battlerTagsSubstituteOnHit": "替身代替{{pokemonNameWithAffix}}\n承受了攻击!",
|
||||
"battlerTagsSubstituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……"
|
||||
} as const;
|
||||
|
@ -144,5 +144,8 @@ export const battle: SimpleTranslationEntries = {
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} 受到了{{moveName}}的傷害!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}}削減了自己的體力,並詛咒了{{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}}正受到詛咒!",
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
||||
"battlerTagsStockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||
"battlerTagsSubstituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出現了!",
|
||||
"battlerTagsSubstituteOnHit": "替身代替{{pokemonNameWithAffix}}承受了攻擊!",
|
||||
"battlerTagsSubstituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……"
|
||||
} as const;
|
||||
|
@ -19,7 +19,7 @@ import { biomeLinks, getBiomeName } from "./data/biomes";
|
||||
import { ModifierTier } from "./modifier/modifier-tier";
|
||||
import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type";
|
||||
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
|
||||
import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, TrappedTag } from "./data/battler-tags";
|
||||
import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag, TrappedTag } from "./data/battler-tags";
|
||||
import { getPokemonNameWithAffix } from "./messages";
|
||||
import { Starter } from "./ui/starter-select-ui-handler";
|
||||
import { Gender } from "./data/gender";
|
||||
@ -1601,6 +1601,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 ?
|
||||
@ -1619,7 +1629,7 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
ease: "Sine.easeIn",
|
||||
scale: 0.5,
|
||||
onComplete: () => {
|
||||
pokemon.setVisible(false);
|
||||
pokemon.resetSprite();
|
||||
this.scene.field.remove(pokemon);
|
||||
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||
this.scene.time.delayedCall(750, () => this.switchAndSummon());
|
||||
@ -1653,8 +1663,18 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
pokemonName: getPokemonNameWithAffix(this.getPokemon())
|
||||
})
|
||||
);
|
||||
// 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) {
|
||||
switchedPokemon.x += switchedPokemon.getSubstituteOffset()[0];
|
||||
switchedPokemon.y += switchedPokemon.getSubstituteOffset()[1];
|
||||
switchedPokemon.setAlpha(0.5);
|
||||
}
|
||||
} else {
|
||||
party[this.fieldIndex].resetBattleData();
|
||||
party[this.fieldIndex].resetSummonData();
|
||||
}
|
||||
@ -2571,7 +2591,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();
|
||||
});
|
||||
}
|
||||
@ -2723,6 +2743,8 @@ 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();
|
||||
}
|
||||
@ -2742,6 +2764,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();
|
||||
@ -2952,7 +2975,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, () => {
|
||||
new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()).play(this.scene, !move.canIgnoreSubstitute(user), () => {
|
||||
for (const target of targets) {
|
||||
if (!targetHitChecks[target.getBattlerIndex()]) {
|
||||
this.stopMultiHit(target);
|
||||
@ -2995,7 +3018,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
if (hitResult !== HitResult.NO_EFFECT) {
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
|
||||
if (hitResult < HitResult.NO_EFFECT && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) {
|
||||
if (hitResult < HitResult.NO_EFFECT && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && (!target.getTag(BattlerTagType.SUBSTITUTE) || move.canIgnoreSubstitute(user))) {
|
||||
const flinched = new Utils.BooleanHolder(false);
|
||||
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||
if (flinched.value) {
|
||||
@ -3042,7 +3065,20 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -3203,7 +3239,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, () => {
|
||||
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].canIgnoreSubstitute(user), () => {
|
||||
if (player) {
|
||||
this.playMoveAnim(moveQueue, false);
|
||||
} else {
|
||||
@ -3542,7 +3580,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
|
||||
pokemon.status.cureTurn = this.cureTurn;
|
||||
}
|
||||
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));
|
||||
if (pokemon.status.isPostTurn()) {
|
||||
this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex));
|
||||
@ -3590,7 +3628,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
|
||||
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage, 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();
|
||||
}
|
||||
@ -3696,7 +3734,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,
|
||||
@ -3832,7 +3870,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()) {
|
||||
@ -5495,7 +5533,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());
|
||||
});
|
||||
}, () => {
|
||||
|
237
src/pokemon-anim-phase.ts
Normal file
237
src/pokemon-anim-phase.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import BattleScene from "./battle-scene";
|
||||
import { SubstituteTag } from "./data/battler-tags";
|
||||
import { PokemonAnimType } from "./enums/pokemon-anim-type";
|
||||
import Pokemon from "./field/pokemon";
|
||||
import { BattlePhase } from "./phases";
|
||||
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
234
src/test/battlerTags/substitute.test.ts
Normal file
234
src/test/battlerTags/substitute.test.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Pokemon, { MoveResult, PokemonTurnData, TurnMove, PokemonMove } from "#app/field/pokemon.js";
|
||||
import BattleScene from "#app/battle-scene.js";
|
||||
import { BattlerTagLapseType, SubstituteTag, TrappedTag } from "#app/data/battler-tags.js";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
|
||||
import { Moves } from "#app/enums/moves.js";
|
||||
import { PokemonAnimType } from "#app/enums/pokemon-anim-type.js";
|
||||
import * as messages from "#app/messages.js";
|
||||
import { MoveEffectPhase } from "#app/phases.js";
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
|
||||
jest.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 : undefined);
|
||||
});
|
||||
|
||||
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], "canIgnoreSubstitute").mockReturnValue(false);
|
||||
|
||||
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();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
418
src/test/moves/substitute.test.ts
Normal file
418
src/test/moves/substitute.test.ts
Normal file
@ -0,0 +1,418 @@
|
||||
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import Overrides from "#app/overrides";
|
||||
import { Species } from "#app/enums/species.js";
|
||||
import { Abilities } from "#app/enums/abilities.js";
|
||||
import { Moves } from "#app/enums/moves.js";
|
||||
import { getMovePosition } from "../utils/gameManagerUtils";
|
||||
import { BerryPhase, MoveEffectPhase, MoveEndPhase } from "#app/phases.js";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
|
||||
import { BattleStat } from "#app/data/battle-stat.js";
|
||||
import { allMoves, StealHeldItemChanceAttr } from "#app/data/move.js";
|
||||
import { SubstituteTag, TrappedTag } from "#app/data/battler-tags.js";
|
||||
import { StatusEffect } from "#app/data/status-effect.js";
|
||||
import { BerryType } from "#app/enums/berry-type.js";
|
||||
import { Mode } from "#app/ui/ui.js";
|
||||
import PartyUiHandler from "#app/ui/party-ui-handler.js";
|
||||
import { Button } from "#app/enums/buttons.js";
|
||||
|
||||
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);
|
||||
|
||||
vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue("single");
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SUBSTITUTE, Moves.SWORDS_DANCE, Moves.TACKLE, Moves.SPLASH]);
|
||||
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA);
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.SPLASH));
|
||||
vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||
vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100);
|
||||
});
|
||||
|
||||
it(
|
||||
"should cause the user to take damage",
|
||||
async () => {
|
||||
await game.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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 () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.TACKLE));
|
||||
|
||||
await game.startBattle([Species.SKARMORY]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.GIGA_IMPACT));
|
||||
vi.spyOn(allMoves[Moves.GIGA_IMPACT], "accuracy", "get").mockReturnValue(100);
|
||||
|
||||
await game.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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 () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.CHARM));
|
||||
|
||||
await game.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SUBSTITUTE));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0);
|
||||
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
|
||||
}
|
||||
);
|
||||
|
||||
it(
|
||||
"should be bypassed by sound-based moves",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.ECHOED_VOICE));
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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 () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.TACKLE));
|
||||
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INFILTRATOR);
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SUBSTITUTE));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SWORDS_DANCE));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEndPhase, false);
|
||||
|
||||
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should protect the user from flinching",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.FAKE_OUT));
|
||||
vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(1); // Ensures the Substitute will break
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, leadPokemon.id);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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);
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.SAND_TOMB));
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, leadPokemon.id);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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);
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.LIQUIDATION));
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, leadPokemon.id);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(0);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should protect the user from being afflicted with status effects",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.NUZZLE));
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, leadPokemon.id);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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 () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.THIEF));
|
||||
vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate
|
||||
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "BERRY", type: BerryType.SITRUS}]);
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, leadPokemon.id);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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 () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.KNOCK_OFF]);
|
||||
vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "BERRY", type: BerryType.SITRUS}]);
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
|
||||
enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, enemyPokemon.id);
|
||||
const enemyNumItems = enemyPokemon.getHeldItems().length;
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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 () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.BUG_BITE));
|
||||
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "BERRY", type: BerryType.SITRUS}]);
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, leadPokemon.id);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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 () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.CLEAR_SMOG));
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, leadPokemon.id);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SWORDS_DANCE));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should prevent the user from becoming confused",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(Array(4).fill(Moves.MAGICAL_TORQUE));
|
||||
vi.spyOn(allMoves[Moves.MAGICAL_TORQUE], "chance", "get").mockReturnValue(100);
|
||||
|
||||
await game.startBattle([Species.BLASTOISE]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, leadPokemon.id);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.SWORDS_DANCE));
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
|
||||
expect(leadPokemon.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
|
||||
expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2);
|
||||
}
|
||||
);
|
||||
|
||||
it.skip(
|
||||
"should transfer to the switched in Pokemon when the source uses Baton Pass",
|
||||
async () => {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SUBSTITUTE, Moves.BATON_PASS]);
|
||||
|
||||
await game.startBattle([Species.BLASTOISE, Species.CHARIZARD]);
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon();
|
||||
|
||||
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, null, null, leadPokemon.id);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BATON_PASS));
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
// TODO: Figure out how to navigate out of the Party UI
|
||||
game.onNextPrompt("MoveEffectPhase", Mode.PARTY, () => {
|
||||
const handler = game.scene.ui.getHandler() as PartyUiHandler;
|
||||
handler.setCursor(1);
|
||||
handler.processInput(Button.ACTION);
|
||||
handler.setCursor(0);
|
||||
handler.processInput(Button.ACTION);
|
||||
handler.processInput(Button.ACTION);
|
||||
});
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, 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
|
||||
);
|
||||
});
|
@ -9,6 +9,7 @@ import { Species } from "#enums/species";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { SPLASH_ONLY } from "../utils/testUtils";
|
||||
import { SubstituteTag } from "#app/data/battler-tags.js";
|
||||
|
||||
|
||||
describe("Moves - Tidy Up", () => {
|
||||
@ -90,7 +91,7 @@ describe("Moves - Tidy Up", () => {
|
||||
|
||||
}, 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]);
|
||||
|
||||
@ -100,8 +101,12 @@ describe("Moves - Tidy Up", () => {
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.doAttack(getMovePosition(game.scene, 0, 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() => {
|
||||
|
@ -7,6 +7,7 @@ import { getMoveTargets } from "../data/move";
|
||||
import {Button} from "#enums/buttons";
|
||||
import { Moves } from "#enums/moves";
|
||||
import Pokemon from "#app/field/pokemon.js";
|
||||
import { SubstituteTag } from "#app/data/battler-tags.js";
|
||||
|
||||
export type TargetSelectCallback = (targets: BattlerIndex[]) => void;
|
||||
|
||||
@ -107,7 +108,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,7 +154,7 @@ export default class TargetSelectUiHandler extends UiHandler {
|
||||
this.targetFlashTween = null;
|
||||
}
|
||||
for (const pokemon of this.targetsHighlighted) {
|
||||
pokemon.setAlpha(1);
|
||||
pokemon.setAlpha(!!pokemon.getTag(SubstituteTag) ? 0.5 : 1);
|
||||
}
|
||||
|
||||
if (this.targetBattleInfoMoveTween.length >= 1) {
|
||||
|
Loading…
Reference in New Issue
Block a user