Merge branch 'beta' into main

This commit is contained in:
flx-sta 2024-09-13 10:08:54 -07:00 committed by GitHub
commit f44474dba7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 1923 additions and 209 deletions

View File

@ -21,10 +21,10 @@ jobs:
steps:
- name: Check out Git repository # Step to check out the repository
uses: actions/checkout@v2 # Use the checkout action version 2
uses: actions/checkout@v4 # Use the checkout action version 4
- name: Set up Node.js # Step to set up Node.js environment
uses: actions/setup-node@v1 # Use the setup-node action version 1
uses: actions/setup-node@v4 # Use the setup-node action version 4
with:
node-version: 20 # Specify Node.js version 20

View File

@ -64,6 +64,7 @@ import { PlayerGender } from "#enums/player-gender";
import { Species } from "#enums/species";
import { UiTheme } from "#enums/ui-theme";
import { TimedEventManager } from "#app/timed-event-manager";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
import i18next from "i18next";
import { TrainerType } from "#enums/trainer-type";
import { battleSpecDialogue } from "./data/dialogue";
@ -74,6 +75,7 @@ import { MessagePhase } from "./phases/message-phase";
import { MovePhase } from "./phases/move-phase";
import { NewBiomeEncounterPhase } from "./phases/new-biome-encounter-phase";
import { NextEncounterPhase } from "./phases/next-encounter-phase";
import { PokemonAnimPhase } from "./phases/pokemon-anim-phase";
import { QuietFormChangePhase } from "./phases/quiet-form-change-phase";
import { ReturnPhase } from "./phases/return-phase";
import { SelectBiomePhase } from "./phases/select-biome-phase";
@ -2721,6 +2723,16 @@ export default class BattleScene extends SceneBase {
return false;
}
triggerPokemonBattleAnim(pokemon: Pokemon, battleAnimType: PokemonAnimType, fieldAssets?: Phaser.GameObjects.Sprite[], delayed: boolean = false): boolean {
const phase: Phase = new PokemonAnimPhase(this, battleAnimType, pokemon, fieldAssets);
if (delayed) {
this.pushPhase(phase);
} else {
this.unshiftPhase(phase);
}
return true;
}
validateAchvs(achvType: Constructor<Achv>, ...args: unknown[]): void {
const filteredAchvs = Object.values(achvs).filter(a => a instanceof achvType);
for (const achv of filteredAchvs) {

View File

@ -1706,6 +1706,10 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
}
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (pokemon !== attacker && move.hitsSubstitute(attacker, pokemon)) {
return false;
}
/**Status inflicted by abilities post attacking are also considered additional effects.*/
if (!attacker.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !simulated && pokemon !== attacker && (!this.contactRequired || move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) && pokemon.randSeedInt(100) < this.chance && !pokemon.status) {
const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)];
@ -2064,6 +2068,10 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
if (this.intimidate) {
applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated);
applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated);
if (opponent.getTag(BattlerTagType.SUBSTITUTE)) {
cancelled.value = true;
}
}
if (!cancelled.value) {
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.stages));
@ -2143,7 +2151,6 @@ export class DownloadAbAttr extends PostSummonAbAttr {
private enemyCountTally: integer;
private stats: BattleStat[];
// TODO: Implement the Substitute feature(s) once move is implemented.
/**
* Checks to see if it is the opening turn (starting a new game), if so, Download won't work. This is because Download takes into account
* vitamins and items, so it needs to use the Stat and the stat alone.
@ -4781,7 +4788,7 @@ export const allAbilities = [ new Ability(Abilities.NONE, 3) ];
export function initAbilities() {
allAbilities.push(
new Ability(Abilities.STENCH, 3)
.attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr(FlinchAttr) ? 10 : 0, BattlerTagType.FLINCHED),
.attr(PostAttackApplyBattlerTagAbAttr, false, (user, target, move) => !move.hasAttr(FlinchAttr) && !move.hitsSubstitute(user, target) ? 10 : 0, BattlerTagType.FLINCHED),
new Ability(Abilities.DRIZZLE, 3)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN),

View File

@ -6,6 +6,7 @@ import * as Utils from "../utils";
import { BattlerIndex } from "../battle";
import { Element } from "json-stable-stringify";
import { Moves } from "#enums/moves";
import { SubstituteTag } from "./battler-tags";
//import fs from 'vite-plugin-fs/browser';
export enum AnimFrameTarget {
@ -700,7 +701,7 @@ export abstract class BattleAnim {
return false;
}
private getGraphicFrameData(scene: BattleScene, frames: AnimFrame[]): Map<integer, Map<AnimFrameTarget, GraphicFrameData>> {
private getGraphicFrameData(scene: BattleScene, frames: AnimFrame[], onSubstitute?: boolean): Map<integer, Map<AnimFrameTarget, GraphicFrameData>> {
const ret: Map<integer, Map<AnimFrameTarget, GraphicFrameData>> = new Map([
[AnimFrameTarget.GRAPHIC, new Map<AnimFrameTarget, GraphicFrameData>() ],
[AnimFrameTarget.USER, new Map<AnimFrameTarget, GraphicFrameData>() ],
@ -711,12 +712,15 @@ export abstract class BattleAnim {
const user = !isOppAnim ? this.user : this.target;
const target = !isOppAnim ? this.target : this.user;
const targetSubstitute = (onSubstitute && user !== target) ? target!.getTag(SubstituteTag) : null;
const userInitialX = user!.x; // TODO: is this bang correct?
const userInitialY = user!.y; // TODO: is this bang correct?
const userHalfHeight = user!.getSprite().displayHeight! / 2; // TODO: is this bang correct?
const targetInitialX = target!.x; // TODO: is this bang correct?
const targetInitialY = target!.y; // TODO: is this bang correct?
const targetHalfHeight = target!.getSprite().displayHeight! / 2; // TODO: is this bang correct?
const targetInitialX = targetSubstitute?.sprite?.x ?? target!.x; // TODO: is this bang correct?
const targetInitialY = targetSubstitute?.sprite?.y ?? target!.y; // TODO: is this bang correct?
const targetHalfHeight = (targetSubstitute?.sprite ?? target!.getSprite()).displayHeight! / 2; // TODO: is this bang correct?
let g = 0;
let u = 0;
@ -754,7 +758,7 @@ export abstract class BattleAnim {
return ret;
}
play(scene: BattleScene, callback?: Function) {
play(scene: BattleScene, onSubstitute?: boolean, callback?: Function) {
const isOppAnim = this.isOppAnim();
const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct?
const target = !isOppAnim ? this.target : this.user;
@ -766,8 +770,10 @@ export abstract class BattleAnim {
return;
}
const targetSubstitute = (!!onSubstitute && user !== target) ? target.getTag(SubstituteTag) : null;
const userSprite = user.getSprite();
const targetSprite = target.getSprite();
const targetSprite = targetSubstitute?.sprite ?? target.getSprite();
const spriteCache: SpriteCache = {
[AnimFrameTarget.GRAPHIC]: [],
@ -782,16 +788,34 @@ export abstract class BattleAnim {
userSprite.setAlpha(1);
userSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ];
userSprite.setAngle(0);
if (!targetSubstitute) {
targetSprite.setPosition(0, 0);
targetSprite.setScale(1);
targetSprite.setAlpha(1);
} else {
targetSprite.setPosition(
target.x - target.getSubstituteOffset()[0],
target.y - target.getSubstituteOffset()[1]
);
targetSprite.setScale(target.getSpriteScale() * (target.isPlayer() ? 0.5 : 1));
targetSprite.setAlpha(1);
}
targetSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ];
targetSprite.setAngle(0);
if (!this.isHideUser() && userSprite) {
this.user?.getSprite().setVisible(true); // using this.user to fix context loss due to isOppAnim swap (#481)
/**
* This and `targetSpriteToShow` are used to restore context lost
* from the `isOppAnim` swap. Using these references instead of `this.user`
* and `this.target` prevent the target's Substitute doll from disappearing
* after being the target of an animation.
*/
const userSpriteToShow = !isOppAnim ? userSprite : targetSprite;
const targetSpriteToShow = !isOppAnim ? targetSprite : userSprite;
if (!this.isHideUser() && userSpriteToShow) {
userSpriteToShow.setVisible(true);
}
if (!this.isHideTarget() && (targetSprite !== userSprite || !this.isHideUser())) {
this.target?.getSprite().setVisible(true); // using this.target to fix context loss due to isOppAnim swap (#481)
if (!this.isHideTarget() && (targetSpriteToShow !== userSpriteToShow || !this.isHideUser())) {
targetSpriteToShow.setVisible(true);
}
for (const ms of Object.values(spriteCache).flat()) {
if (ms) {
@ -814,8 +838,8 @@ export abstract class BattleAnim {
const userInitialX = user.x;
const userInitialY = user.y;
const targetInitialX = target.x;
const targetInitialY = target.y;
const targetInitialX = targetSubstitute?.sprite?.x ?? target.x;
const targetInitialY = targetSubstitute?.sprite?.y ?? target.y;
this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ];
this.dstLine = [ userInitialX, userInitialY, targetInitialX, targetInitialY ];
@ -833,7 +857,7 @@ export abstract class BattleAnim {
}
const spriteFrames = anim!.frames[f]; // TODO: is the bang correcT?
const frameData = this.getGraphicFrameData(scene, anim!.frames[f]); // TODO: is the bang correct?
const frameData = this.getGraphicFrameData(scene, anim!.frames[f], onSubstitute); // TODO: is the bang correct?
let u = 0;
let t = 0;
let g = 0;
@ -846,6 +870,7 @@ export abstract class BattleAnim {
const sprites = spriteCache[isUser ? AnimFrameTarget.USER : AnimFrameTarget.TARGET];
const spriteSource = isUser ? userSprite : targetSprite;
if ((isUser ? u : t) === sprites.length) {
if (!isUser && !!targetSubstitute) {
const sprite = scene.addPokemonSprite(isUser ? user! : target, 0, 0, spriteSource!.texture, spriteSource!.frame.name, true); // TODO: are those bangs correct?
[ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = (isUser ? user! : target).getSprite().pipelineData[k]); // TODO: are those bangs correct?
sprite.setPipelineData("spriteKey", (isUser ? user! : target).getBattleSpriteKey());
@ -855,15 +880,24 @@ export abstract class BattleAnim {
spriteSource.on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame));
scene.field.add(sprite);
sprites.push(sprite);
} else {
const sprite = scene.addFieldSprite(spriteSource.x, spriteSource.y, spriteSource.texture);
spriteSource.on("animationupdate", (_anim, frame) => sprite.setFrame(frame.textureFrame));
scene.field.add(sprite);
sprites.push(sprite);
}
}
const spriteIndex = isUser ? u++ : t++;
const pokemonSprite = sprites[spriteIndex];
const graphicFrameData = frameData.get(frame.target)!.get(spriteIndex)!; // TODO: are the bangs correct?
pokemonSprite.setPosition(graphicFrameData.x, graphicFrameData.y - ((spriteSource.height / 2) * (spriteSource.parentContainer.scale - 1)));
const spriteSourceScale = (isUser || !targetSubstitute)
? spriteSource.parentContainer.scale
: target.getSpriteScale() * (target.isPlayer() ? 0.5 : 1);
pokemonSprite.setPosition(graphicFrameData.x, graphicFrameData.y - ((spriteSource.height / 2) * (spriteSourceScale - 1)));
pokemonSprite.setAngle(graphicFrameData.angle);
pokemonSprite.setScale(graphicFrameData.scaleX * spriteSource.parentContainer.scale, graphicFrameData.scaleY * spriteSource.parentContainer.scale);
pokemonSprite.setScale(graphicFrameData.scaleX * spriteSourceScale, graphicFrameData.scaleY * spriteSourceScale);
pokemonSprite.setData("locked", frame.locked);

View File

@ -22,6 +22,7 @@ import { MovePhase } from "#app/phases/move-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase, StatStageChangeCallback } from "#app/phases/stat-stage-change-phase";
import { PokemonAnimType } from "#app/enums/pokemon-anim-type";
export enum BattlerTagLapseType {
FAINT,
@ -30,6 +31,7 @@ export enum BattlerTagLapseType {
AFTER_MOVE,
MOVE_EFFECT,
TURN_END,
HIT,
CUSTOM
}
@ -391,10 +393,12 @@ export class BeakBlastChargingTag extends BattlerTag {
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.CUSTOM) {
const effectPhase = pokemon.scene.getCurrentPhase();
if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) {
if (effectPhase instanceof MoveEffectPhase) {
const attacker = effectPhase.getPokemon();
if (effectPhase.move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) {
attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
}
}
return true;
}
return super.lapse(pokemon, lapseType);
@ -451,10 +455,14 @@ export class TrappedTag extends BattlerTag {
}
canAdd(pokemon: Pokemon): boolean {
const source = pokemon.scene.getPokemonById(this.sourceId!)!;
const move = allMoves[this.sourceMove];
const isGhost = pokemon.isOfType(Type.GHOST);
const isTrapped = pokemon.getTag(TrappedTag);
const hasSubstitute = move.hitsSubstitute(source, pokemon);
return !isTrapped && !isGhost;
return !isTrapped && !isGhost && !hasSubstitute;
}
onAdd(pokemon: Pokemon): void {
@ -1121,7 +1129,7 @@ export abstract class DamagingTrapTag extends TrappedTag {
}
canAdd(pokemon: Pokemon): boolean {
return !pokemon.getTag(TrappedTag);
return !pokemon.getTag(TrappedTag) && !pokemon.getTag(BattlerTagType.SUBSTITUTE);
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -2007,7 +2015,6 @@ export class FormBlockDamageTag extends BattlerTag {
pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger);
}
}
/** Provides the additional weather-based effects of the Ice Face ability */
export class IceFaceBlockDamageTag extends FormBlockDamageTag {
constructor(tagType: BattlerTagType) {
@ -2055,7 +2062,6 @@ export class StockpilingTag extends BattlerTag {
if (defChange) {
this.statChangeCounts[Stat.DEF]++;
}
if (spDefChange) {
this.statChangeCounts[Stat.SPDEF]++;
}
@ -2211,6 +2217,93 @@ export class TarShotTag extends BattlerTag {
}
}
export class SubstituteTag extends BattlerTag {
/** The substitute's remaining HP. If HP is depleted, the Substitute fades. */
public hp: number;
/** A reference to the sprite representing the Substitute doll */
public sprite: Phaser.GameObjects.Sprite;
/** Is the source Pokemon "in focus," i.e. is it fully visible on the field? */
public sourceInFocus: boolean;
constructor(sourceMove: Moves, sourceId: integer) {
super(BattlerTagType.SUBSTITUTE, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE, BattlerTagLapseType.HIT], 0, sourceMove, sourceId, true);
}
/** Sets the Substitute's HP and queues an on-add battle animation that initializes the Substitute's sprite. */
onAdd(pokemon: Pokemon): void {
this.hp = Math.floor(pokemon.scene.getPokemonById(this.sourceId!)!.getMaxHp() / 4);
this.sourceInFocus = false;
// Queue battle animation and message
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_ADD);
pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500);
// Remove any trapping effects from the user
pokemon.findAndRemoveTags(tag => tag instanceof TrappedTag);
}
/** Queues an on-remove battle animation that removes the Substitute's sprite. */
onRemove(pokemon: Pokemon): void {
// Only play the animation if the cause of removal isn't from the source's own move
if (!this.sourceInFocus) {
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_REMOVE, [this.sprite]);
} else {
this.sprite.destroy();
}
pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnRemove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
switch (lapseType) {
case BattlerTagLapseType.PRE_MOVE:
this.onPreMove(pokemon);
break;
case BattlerTagLapseType.AFTER_MOVE:
this.onAfterMove(pokemon);
break;
case BattlerTagLapseType.HIT:
this.onHit(pokemon);
break;
}
return lapseType !== BattlerTagLapseType.CUSTOM; // only remove this tag on custom lapse
}
/** Triggers an animation that brings the Pokemon into focus before it uses a move */
onPreMove(pokemon: Pokemon): void {
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_PRE_MOVE, [this.sprite]);
this.sourceInFocus = true;
}
/** Triggers an animation that brings the Pokemon out of focus after it uses a move */
onAfterMove(pokemon: Pokemon): void {
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_POST_MOVE, [this.sprite]);
this.sourceInFocus = false;
}
/** If the Substitute redirects damage, queue a message to indicate it. */
onHit(pokemon: Pokemon): void {
const moveEffectPhase = pokemon.scene.getCurrentPhase();
if (moveEffectPhase instanceof MoveEffectPhase) {
const attacker = moveEffectPhase.getUserPokemon()!;
const move = moveEffectPhase.move.getMove();
const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft);
if (firstHit && move.hitsSubstitute(attacker, pokemon)) {
pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnHit", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
}
}
/**
* When given a battler tag or json representing one, load the data for it.
* @param {BattlerTag | any} source A battler tag
*/
loadTag(source: BattlerTag | any): void {
super.loadTag(source);
this.hp = source.hp;
}
}
/**
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
*
@ -2370,6 +2463,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new ThroatChoppedTag();
case BattlerTagType.GORILLA_TACTICS:
return new GorillaTacticsTag();
case BattlerTagType.SUBSTITUTE:
return new SubstituteTag(sourceMove, sourceId);
case BattlerTagType.NONE:
default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -1,5 +1,5 @@
import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims";
import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags";
import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, SubstituteTag, TypeBoostTag } from "./battler-tags";
import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon";
import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects } from "./status-effect";
@ -116,8 +116,10 @@ export enum MoveFlags {
IGNORE_ABILITIES = 1 << 16,
/** Enables all hits of a multi-hit move to be accuracy checked individually */
CHECK_ALL_HITS = 1 << 17,
/** Indicates a move is able to bypass its target's Substitute (if the target has one) */
IGNORE_SUBSTITUTE = 1 << 18,
/** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */
REDIRECT_COUNTER = 1 << 18,
REDIRECT_COUNTER = 1 << 19,
}
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
@ -333,6 +335,22 @@ export default class Move implements Localizable {
return false;
}
/**
* Checks if the move would hit its target's Substitute instead of the target itself.
* @param user The {@linkcode Pokemon} using this move
* @param target The {@linkcode Pokemon} targeted by this move
* @returns `true` if the move can bypass the target's Substitute; `false` otherwise.
*/
hitsSubstitute(user: Pokemon, target: Pokemon | null): boolean {
if (this.moveTarget === MoveTarget.USER || !target?.getTag(BattlerTagType.SUBSTITUTE)) {
return false;
}
return !user.hasAbility(Abilities.INFILTRATOR)
&& !this.hasFlag(MoveFlags.SOUND_BASED)
&& !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE);
}
/**
* Adds a move condition to the move
* @param condition {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}, appends to conditions array a new MoveCondition object
@ -576,6 +594,17 @@ export default class Move implements Localizable {
return this;
}
/**
* Sets the {@linkcode MoveFlags.IGNORE_SUBSTITUTE} flag for the calling Move
* @param ignoresSubstitute The value (boolean) to set the flag to
* example: @see {@linkcode Moves.WHIRLWIND}
* @returns The {@linkcode Move} that called this function
*/
ignoresSubstitute(ignoresSubstitute: boolean = true): this {
this.setFlag(MoveFlags.IGNORE_SUBSTITUTE, ignoresSubstitute);
return this;
}
/**
* Sets the {@linkcode MoveFlags.REDIRECT_COUNTER} flag for the calling Move
* @param redirectCounter The value (boolean) to set the flag to
@ -598,7 +627,7 @@ export default class Move implements Localizable {
// special cases below, eg: if the move flag is MAKES_CONTACT, and the user pokemon has an ability that ignores contact (like "Long Reach"), then overrides and move does not make contact
switch (flag) {
case MoveFlags.MAKES_CONTACT:
if (user.hasAbilityWithAttr(IgnoreContactAbAttr)) {
if (user.hasAbilityWithAttr(IgnoreContactAbAttr) || this.hitsSubstitute(user, target)) {
return false;
}
break;
@ -612,8 +641,8 @@ export default class Move implements Localizable {
}
break;
case MoveFlags.IGNORE_PROTECT:
if (user.hasAbilityWithAttr(IgnoreProtectOnContactAbAttr) &&
this.checkFlag(MoveFlags.MAKES_CONTACT, user, target)) {
if (user.hasAbilityWithAttr(IgnoreProtectOnContactAbAttr)
&& this.checkFlag(MoveFlags.MAKES_CONTACT, user, null)) {
return true;
}
break;
@ -1446,6 +1475,58 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
}
}
/**
* Attribute to put in a {@link https://bulbapedia.bulbagarden.net/wiki/Substitute_(doll) | Substitute Doll}
* for the user.
* @extends MoveEffectAttr
* @see {@linkcode apply}
*/
export class AddSubstituteAttr extends MoveEffectAttr {
constructor() {
super(true);
}
/**
* Removes 1/4 of the user's maximum HP (rounded down) to create a substitute for the user
* @param user the {@linkcode Pokemon} that used the move.
* @param target n/a
* @param move the {@linkcode Move} with this attribute.
* @param args n/a
* @returns true if the attribute successfully applies, false otherwise
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
const hpCost = Math.floor(user.getMaxHp() / 4);
user.damageAndUpdate(hpCost, HitResult.OTHER, false, true, true);
user.addTag(BattlerTagType.SUBSTITUTE, 0, move.id, user.id);
return true;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
if (user.isBoss()) {
return -10;
}
return 5;
}
getCondition(): MoveConditionFunc {
return (user, target, move) => !user.getTag(SubstituteTag) && user.hp > Math.floor(user.getMaxHp() / 4) && user.getMaxHp() > 1;
}
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
if (user.getTag(SubstituteTag)) {
return i18next.t("moveTriggers:substituteOnOverlap", { pokemonName: getPokemonNameWithAffix(user) });
} else if (user.hp <= Math.floor(user.getMaxHp() / 4) || user.getMaxHp() === 1) {
return i18next.t("moveTriggers:substituteNotEnoughHp");
} else {
return i18next.t("battle:attackFailed");
}
}
}
export enum MultiHitType {
_2,
_2_TO_5,
@ -1949,6 +2030,10 @@ export class StatusEffectAttr extends MoveEffectAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance;
if (statusCheck) {
@ -2048,6 +2133,9 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise<boolean>(resolve => {
if (move.hitsSubstitute(user, target)) {
return resolve(false);
}
const rand = Phaser.Math.RND.realInRange(0, 1);
if (rand >= this.chance) {
return resolve(false);
@ -2117,6 +2205,10 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
return false;
}
if (move.hitsSubstitute(user, target)) {
return false;
}
const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft
@ -2236,6 +2328,9 @@ export class StealEatBerryAttr extends EatBerryAttr {
* @returns {boolean} true if the function succeeds
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (move.hitsSubstitute(user, target)) {
return false;
}
const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // check for abilities that block item theft
if (cancelled.value === true) {
@ -2286,6 +2381,10 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
return false;
}
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
// Special edge case for shield dust blocking Sparkling Aria curing burn
const moveTargets = getMoveTargets(user, move.id);
if (target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && move.id === Moves.SPARKLING_ARIA && moveTargets.targets.length === 1) {
@ -2463,7 +2562,7 @@ export class ChargeAttr extends OverrideMoveEffectAttr {
const lastMove = user.getLastXMoves().find(() => true);
if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && lastMove.turn !== user.scene.currentBattle.turn)) {
(args[0] as Utils.BooleanHolder).value = true;
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, () => {
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
if (this.tagType) {
user.addTag(this.tagType, 1, move.id, user.id);
@ -2563,7 +2662,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
return new Promise(resolve => {
if (args.length < 2 || !args[1]) {
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, () => {
new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => {
(args[0] as Utils.BooleanHolder).value = true;
user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
@ -2597,6 +2696,10 @@ export class StatStageChangeAttr extends MoveEffectAttr {
return false;
}
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
const stages = this.getLevels(user);
@ -2793,9 +2896,11 @@ export class ResetStatsAttr extends MoveEffectAttr {
activePokemon.forEach(p => promises.push(this.resetStats(p)));
target.scene.queueMessage(i18next.t("moveTriggers:statEliminated"));
} else { // Affects only the single target when Clear Smog is used
if (!move.hitsSubstitute(user, target)) {
promises.push(this.resetStats(target));
target.scene.queueMessage(i18next.t("moveTriggers:resetStats", {pokemonName: getPokemonNameWithAffix(target)}));
}
}
await Promise.all(promises);
return true;
@ -2836,7 +2941,7 @@ export class SwapStatStagesAttr extends MoveEffectAttr {
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any []): boolean {
if (super.apply(user, target, move, args)) {
for (const s of BATTLE_STATS) {
for (const s of this.stats) {
const temp = user.getStatStage(s);
user.setStatStage(s, target.getStatStage(s));
target.setStatStage(s, temp);
@ -4621,6 +4726,13 @@ export class FlinchAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.FLINCHED, false);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!move.hitsSubstitute(user, target)) {
return super.apply(user, target, move, args);
}
return false;
}
}
export class ConfuseAttr extends AddBattlerTagAttr {
@ -4636,8 +4748,11 @@ export class ConfuseAttr extends AddBattlerTagAttr {
return false;
}
if (!move.hitsSubstitute(user, target)) {
return super.apply(user, target, move, args);
}
return false;
}
}
export class RechargeAttr extends AddBattlerTagAttr {
@ -4710,6 +4825,36 @@ export class FaintCountdownAttr extends AddBattlerTagAttr {
}
}
/**
* Attribute to remove all Substitutes from the field.
* @extends MoveEffectAttr
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Tidy_Up_(move) | Tidy Up}
* @see {@linkcode SubstituteTag}
*/
export class RemoveAllSubstitutesAttr extends MoveEffectAttr {
constructor() {
super(true);
}
/**
* Remove's the Substitute Doll effect from all active Pokemon on the field
* @param user {@linkcode Pokemon} the Pokemon using this move
* @param target n/a
* @param move {@linkcode Move} the move applying this effect
* @param args n/a
* @returns `true` if the effect successfully applies
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
user.scene.getField(true).forEach(pokemon =>
pokemon.findAndRemoveTags(tag => tag.tagType === BattlerTagType.SUBSTITUTE));
return true;
}
}
/**
* Attribute used when a move hits a {@linkcode BattlerTagType} for double damage
* @extends MoveAttr
@ -5099,6 +5244,10 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
const switchOutTarget = (this.user ? user : target);
const player = switchOutTarget instanceof PlayerPokemon;
if (!this.user && move.hitsSubstitute(user, target)) {
return false;
}
if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr) || target.isMax())) {
return false;
}
@ -6615,6 +6764,7 @@ export function initMoves() {
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr)
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
.ignoresSubstitute()
.hidesTarget()
.windMove(),
new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
@ -6705,6 +6855,7 @@ export function initMoves() {
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1)
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
.condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined)
.ignoresSubstitute()
.condition(failOnMaxCondition),
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
@ -6842,6 +6993,7 @@ export function initMoves() {
.attr(LevelDamageAttr),
new StatusMove(Moves.MIMIC, Type.NORMAL, -1, 10, -1, 0, 1)
.attr(MovesetCopyMoveAttr)
.ignoresSubstitute()
.ignoresVirtual(),
new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], -2)
@ -6870,6 +7022,7 @@ export function initMoves() {
.attr(AddArenaTagAttr, ArenaTagType.LIGHT_SCREEN, 5, true)
.target(MoveTarget.USER_SIDE),
new SelfStatusMove(Moves.HAZE, Type.ICE, -1, 30, -1, 0, 1)
.ignoresSubstitute()
.attr(ResetStatsAttr, true),
new StatusMove(Moves.REFLECT, Type.PSYCHIC, -1, 20, -1, 0, 1)
.attr(AddArenaTagAttr, ArenaTagType.REFLECT, 5, true)
@ -7012,14 +7165,14 @@ export function initMoves() {
.attr(HighCritAttr)
.slicingMove(),
new SelfStatusMove(Moves.SUBSTITUTE, Type.NORMAL, -1, 10, -1, 0, 1)
.attr(RecoilAttr)
.unimplemented(),
.attr(AddSubstituteAttr),
new AttackMove(Moves.STRUGGLE, Type.NORMAL, MoveCategory.PHYSICAL, 50, -1, 1, -1, 0, 1)
.attr(RecoilAttr, true, 0.25, true)
.attr(TypelessAttr)
.ignoresVirtual()
.target(MoveTarget.RANDOM_NEAR_ENEMY),
new StatusMove(Moves.SKETCH, Type.NORMAL, -1, 1, -1, 0, 2)
.ignoresSubstitute()
.attr(SketchAttr)
.ignoresVirtual(),
new AttackMove(Moves.TRIPLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 10, 90, 10, -1, 0, 2)
@ -7045,12 +7198,14 @@ export function initMoves() {
.soundBased(),
new StatusMove(Moves.CURSE, Type.GHOST, -1, 10, -1, 0, 2)
.attr(CurseAttr)
.ignoresSubstitute()
.ignoresProtect(true)
.target(MoveTarget.CURSE),
new AttackMove(Moves.FLAIL, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2)
.attr(LowHpPowerAttr),
new StatusMove(Moves.CONVERSION_2, Type.NORMAL, -1, 30, -1, 0, 2)
.attr(ResistLastMoveTypeAttr)
.ignoresSubstitute()
.partial(), // Checks the move's original typing and not if its type is changed through some other means
new AttackMove(Moves.AEROBLAST, Type.FLYING, MoveCategory.SPECIAL, 100, 95, 5, -1, 0, 2)
.windMove()
@ -7062,6 +7217,7 @@ export function initMoves() {
new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2)
.attr(LowHpPowerAttr),
new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2)
.ignoresSubstitute()
.attr(ReducePpMoveAttr, 4),
new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2)
.attr(StatusEffectAttr, StatusEffect.FREEZE)
@ -7095,7 +7251,8 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.ballBombMove(),
new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST),
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
.ignoresSubstitute(),
new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2)
.ignoresProtect()
.attr(DestinyBondAttr)
@ -7164,6 +7321,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.INFATUATED)
.ignoresSubstitute()
.condition((user, target, move) => user.isOppositeGender(target)),
new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2)
.attr(BypassSleepAttr)
@ -7209,6 +7367,7 @@ export function initMoves() {
.hidesUser(),
new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
.ignoresSubstitute()
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target)),
new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2)
.partial(),
@ -7267,6 +7426,7 @@ export function initMoves() {
.attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.SPECIAL, 2)
.target(MoveTarget.ATTACKER),
new StatusMove(Moves.PSYCH_UP, Type.NORMAL, -1, 10, -1, 0, 2)
.ignoresSubstitute()
.attr(CopyStatsAttr),
new AttackMove(Moves.EXTREME_SPEED, Type.NORMAL, MoveCategory.PHYSICAL, 80, 100, 5, -1, 2, 2),
new AttackMove(Moves.ANCIENT_POWER, Type.ROCK, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 2)
@ -7315,6 +7475,7 @@ export function initMoves() {
.attr(WeatherChangeAttr, WeatherType.HAIL)
.target(MoveTarget.BOTH_SIDES),
new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3)
.ignoresSubstitute()
.unimplemented(),
new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1)
@ -7345,13 +7506,16 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false),
new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3)
.ignoresSubstitute()
.unimplemented(),
new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3)
.attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND)
.ignoresSubstitute()
.target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.TRICK, Type.PSYCHIC, 100, 10, -1, 0, 3)
.unimplemented(),
new StatusMove(Moves.ROLE_PLAY, Type.PSYCHIC, -1, 10, -1, 0, 3)
.ignoresSubstitute()
.attr(AbilityCopyAttr),
new SelfStatusMove(Moves.WISH, Type.NORMAL, -1, 10, -1, 0, 3)
.triageMove()
@ -7384,8 +7548,10 @@ export function initMoves() {
.attr(HpPowerAttr)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.SKILL_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 3)
.ignoresSubstitute()
.attr(SwitchAbilitiesAttr),
new SelfStatusMove(Moves.IMPRISON, Type.PSYCHIC, -1, 10, -1, 0, 3)
.ignoresSubstitute()
.unimplemented(),
new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3)
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN)
@ -7470,7 +7636,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true)
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE),
new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST),
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
.ignoresSubstitute(),
new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.makesContact(false),
@ -7582,7 +7749,8 @@ export function initMoves() {
.attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5)
.target(MoveTarget.BOTH_SIDES),
new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK),
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
.ignoresSubstitute(),
new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4)
.attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1)
.attr(HealStatusEffectAttr, false, StatusEffect.SLEEP),
@ -7659,6 +7827,7 @@ export function initMoves() {
.attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true)
.target(MoveTarget.USER_SIDE),
new StatusMove(Moves.ME_FIRST, Type.NORMAL, -1, 20, -1, 0, 4)
.ignoresSubstitute()
.ignoresVirtual()
.target(MoveTarget.NEAR_ENEMY)
.unimplemented(),
@ -7666,9 +7835,11 @@ export function initMoves() {
.attr(CopyMoveAttr)
.ignoresVirtual(),
new StatusMove(Moves.POWER_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4)
.attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ]),
.attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ])
.ignoresSubstitute(),
new StatusMove(Moves.GUARD_SWAP, Type.PSYCHIC, -1, 10, 100, 0, 4)
.attr(SwapStatStagesAttr, [ Stat.DEF, Stat.SPDEF ]),
.attr(SwapStatStagesAttr, [ Stat.DEF, Stat.SPDEF ])
.ignoresSubstitute(),
new AttackMove(Moves.PUNISHMENT, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 4)
.makesContact(true)
.attr(PunishmentPowerAttr),
@ -7682,7 +7853,8 @@ export function initMoves() {
.attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES)
.target(MoveTarget.ENEMY_SIDE),
new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4)
.attr(SwapStatStagesAttr, BATTLE_STATS),
.attr(SwapStatStagesAttr, BATTLE_STATS)
.ignoresSubstitute(),
new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4)
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4)
@ -7965,6 +8137,7 @@ export function initMoves() {
.attr(AbilityGiveAttr),
new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5)
.ignoresProtect()
.ignoresSubstitute()
.target(MoveTarget.NEAR_OTHER)
.condition(failIfSingleBattle)
.condition((user, target, move) => !target.turnData.acted)
@ -8007,6 +8180,7 @@ export function initMoves() {
.partial() // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/
.attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, i18next.t("moveTriggers:tookTargetIntoSky", {pokemonName: "{USER}", targetName: "{TARGET}"}), BattlerTagType.FLYING) // TODO: Add 2nd turn message
.condition(failOnGravityCondition)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
.ignoresVirtual(),
new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
@ -8021,6 +8195,7 @@ export function initMoves() {
new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5)
.attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferrable).reduce((v, m) => v + m.stackCount, 0))),
new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5)
.ignoresSubstitute()
.attr(CopyTypeAttr),
new AttackMove(Moves.RETALIATE, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 5, -1, 0, 5)
.attr(MovePowerMultiplierAttr, (user, target, move) => {
@ -8037,6 +8212,7 @@ export function initMoves() {
.attr(SacrificialAttrOnHit),
new StatusMove(Moves.BESTOW, Type.NORMAL, -1, 15, -1, 0, 5)
.ignoresProtect()
.ignoresSubstitute()
.unimplemented(),
new AttackMove(Moves.INFERNO, Type.FIRE, MoveCategory.SPECIAL, 100, 50, 5, 100, 0, 5)
.attr(StatusEffectAttr, StatusEffect.BURN),
@ -8246,13 +8422,15 @@ export function initMoves() {
.soundBased()
.target(MoveTarget.ALL_NEAR_OTHERS),
new StatusMove(Moves.FAIRY_LOCK, Type.FAIRY, -1, 10, -1, 0, 6)
.ignoresSubstitute()
.target(MoveTarget.BOTH_SIDES)
.unimplemented(),
new SelfStatusMove(Moves.KINGS_SHIELD, Type.STEEL, -1, 10, -1, 4, 6)
.attr(ProtectAttr, BattlerTagType.KINGS_SHIELD)
.condition(failIfLastCondition),
new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1),
.attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.ignoresSubstitute(),
new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.soundBased(),
@ -8265,7 +8443,8 @@ export function initMoves() {
.attr(HealStatusEffectAttr, false, StatusEffect.FREEZE)
.attr(StatusEffectAttr, StatusEffect.BURN),
new AttackMove(Moves.HYPERSPACE_HOLE, Type.PSYCHIC, MoveCategory.SPECIAL, 80, -1, 5, -1, 0, 6)
.ignoresProtect(),
.ignoresProtect()
.ignoresSubstitute(),
new AttackMove(Moves.WATER_SHURIKEN, Type.WATER, MoveCategory.SPECIAL, 15, 100, 20, -1, 1, 6)
.attr(MultiHitAttr)
.attr(WaterShurikenPowerAttr)
@ -8277,6 +8456,7 @@ export function initMoves() {
.condition(failIfLastCondition),
new StatusMove(Moves.AROMATIC_MIST, Type.FAIRY, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 1)
.ignoresSubstitute()
.target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2),
@ -8284,6 +8464,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6)
.ignoresSubstitute()
.powderMove()
.unimplemented(),
new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
@ -8292,6 +8473,7 @@ export function initMoves() {
.ignoresVirtual(),
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false)))
.ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES)
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))),
new StatusMove(Moves.HAPPY_HOUR, Type.NORMAL, -1, 30, -1, 0, 6) // No animation
@ -8304,6 +8486,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_ENEMIES),
new SelfStatusMove(Moves.CELEBRATE, Type.NORMAL, -1, 40, -1, 0, 6),
new StatusMove(Moves.HOLD_HANDS, Type.NORMAL, -1, 40, -1, 0, 6)
.ignoresSubstitute()
.target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1),
@ -8349,6 +8532,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true),
new AttackMove(Moves.HYPERSPACE_FURY, Type.DARK, MoveCategory.PHYSICAL, 100, -1, 5, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true)
.ignoresSubstitute()
.makesContact(false)
.ignoresProtect(),
/* Unused */
@ -8508,6 +8692,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS].find(a => target.hasAbility(a, false)))
.ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES)
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS].find(a => p.hasAbility(a, false)))),
new AttackMove(Moves.THROAT_CHOP, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7)
@ -8538,7 +8723,8 @@ export function initMoves() {
user.scene.queueMessage(i18next.t("moveTriggers:burnedItselfOut", {pokemonName: getPokemonNameWithAffix(user)}));
}),
new StatusMove(Moves.SPEED_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 7)
.attr(SwapStatAttr, Stat.SPD),
.attr(SwapStatAttr, Stat.SPD)
.ignoresSubstitute(),
new AttackMove(Moves.SMART_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 7),
new StatusMove(Moves.PURIFY, Type.POISON, -1, 20, -1, 0, 7)
.condition(
@ -8555,6 +8741,7 @@ export function initMoves() {
new AttackMove(Moves.TROP_KICK, Type.GRASS, MoveCategory.PHYSICAL, 70, 100, 15, 100, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new StatusMove(Moves.INSTRUCT, Type.PSYCHIC, -1, 15, -1, 0, 7)
.ignoresSubstitute()
.unimplemented(),
new AttackMove(Moves.BEAK_BLAST, Type.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7)
.attr(BeakBlastHeaderAttr)
@ -8622,6 +8809,7 @@ export function initMoves() {
new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7)
.attr(RechargeAttr),
new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7)
.ignoresSubstitute()
.partial(),
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
.ignoresAbilities()
@ -9289,7 +9477,8 @@ export function initMoves() {
.target(MoveTarget.BOTH_SIDES),
new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true)
.attr(RemoveArenaTrapAttr, true),
.attr(RemoveArenaTrapAttr, true)
.attr(RemoveAllSubstitutesAttr),
new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9)
.attr(WeatherChangeAttr, WeatherType.SNOW)
.target(MoveTarget.BOTH_SIDES),

View File

@ -12,6 +12,7 @@ import { TimeOfDay } from "#enums/time-of-day";
import { getPokemonNameWithAffix } from "#app/messages";
import i18next from "i18next";
import { WeatherType } from "./weather";
import { Challenges } from "#app/enums/challenges";
export enum FormChangeItem {
NONE,
@ -345,6 +346,16 @@ export class SpeciesFormChangePostMoveTrigger extends SpeciesFormChangeMoveTrigg
}
}
export class MeloettaFormChangePostMoveTrigger extends SpeciesFormChangePostMoveTrigger {
override canChange(pokemon: Pokemon): boolean {
if (pokemon.scene.gameMode.hasChallenge(Challenges.SINGLE_TYPE)) {
return false;
} else {
return super.canChange(pokemon);
}
}
}
export class SpeciesDefaultFormMatchTrigger extends SpeciesFormChangeTrigger {
private formKey: string;
@ -759,9 +770,8 @@ export const pokemonFormChanges: PokemonFormChanges = {
new SpeciesFormChange(Species.KELDEO, "resolute", "ordinary", new SpeciesFormChangeMoveLearnedTrigger(Moves.SECRET_SWORD, false))
],
[Species.MELOETTA]: [
new SpeciesFormChange(Species.MELOETTA, "aria", "pirouette", new SpeciesFormChangePostMoveTrigger(Moves.RELIC_SONG), true),
new SpeciesFormChange(Species.MELOETTA, "pirouette", "aria", new SpeciesFormChangePostMoveTrigger(Moves.RELIC_SONG), true),
new SpeciesFormChange(Species.MELOETTA, "pirouette", "aria", new SpeciesFormChangeActiveTrigger(false), true)
new SpeciesFormChange(Species.MELOETTA, "aria", "pirouette", new MeloettaFormChangePostMoveTrigger(Moves.RELIC_SONG), true),
new SpeciesFormChange(Species.MELOETTA, "pirouette", "aria", new MeloettaFormChangePostMoveTrigger(Moves.RELIC_SONG), true)
],
[Species.GENESECT]: [
new SpeciesFormChange(Species.GENESECT, "", "shock", new SpeciesFormChangeItemTrigger(FormChangeItem.SHOCK_DRIVE)),

View File

@ -1893,7 +1893,7 @@ export function initSpecies() {
),
new PokemonSpecies(Species.MELOETTA, 5, false, false, true, "Melody Pokémon", Type.NORMAL, Type.PSYCHIC, 0.6, 6.5, Abilities.SERENE_GRACE, Abilities.NONE, Abilities.NONE, 600, 100, 77, 77, 128, 128, 90, 3, 100, 270, GrowthRate.SLOW, null, false, true,
new PokemonForm("Aria Forme", "aria", Type.NORMAL, Type.PSYCHIC, 0.6, 6.5, Abilities.SERENE_GRACE, Abilities.NONE, Abilities.NONE, 600, 100, 77, 77, 128, 128, 90, 3, 100, 270, false, null, true),
new PokemonForm("Pirouette Forme", "pirouette", Type.NORMAL, Type.FIGHTING, 0.6, 6.5, Abilities.SERENE_GRACE, Abilities.NONE, Abilities.NONE, 600, 100, 128, 90, 77, 77, 128, 3, 100, 270),
new PokemonForm("Pirouette Forme", "pirouette", Type.NORMAL, Type.FIGHTING, 0.6, 6.5, Abilities.SERENE_GRACE, Abilities.NONE, Abilities.NONE, 600, 100, 128, 90, 77, 77, 128, 3, 100, 270, false, null, true),
),
new PokemonSpecies(Species.GENESECT, 5, false, false, true, "Paleozoic Pokémon", Type.BUG, Type.STEEL, 1.5, 82.5, Abilities.DOWNLOAD, Abilities.NONE, Abilities.NONE, 600, 71, 120, 95, 120, 95, 99, 3, 0, 300, GrowthRate.SLOW, null, false, true,
new PokemonForm("Normal", "", Type.BUG, Type.STEEL, 1.5, 82.5, Abilities.DOWNLOAD, Abilities.NONE, Abilities.NONE, 600, 71, 120, 95, 120, 95, 99, 3, 0, 300, false, null, true),

View File

@ -1530,18 +1530,18 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.PIDGEOT], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1;
p.formIndex = 1; // Mega Pidgeot
p.generateAndPopulateMoveset();
p.generateName();
})),
[TrainerType.RED]: new TrainerConfig(++t).initForChampion(signatureSpecies["RED"], true).setBattleBgm("battle_johto_champion").setMixedBattleBgm("battle_johto_champion").setHasDouble("red_blue_double").setDoubleTrainerType(TrainerType.BLUE).setDoubleTitle("champion_double")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([Species.PIKACHU], TrainerSlot.TRAINER, true, p => {
p.formIndex = 8;
p.formIndex = 8; // G-Max Pikachu
p.generateAndPopulateMoveset();
p.generateName();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1;
p.formIndex = 1; // Mega Venusaur, Mega Charizard X, or Mega Blastoise
p.generateAndPopulateMoveset();
p.generateName();
})),
@ -1550,7 +1550,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.LATIAS, Species.LATIOS], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1;
p.formIndex = 1; // Mega Latias or Mega Latios
p.generateAndPopulateMoveset();
p.generateName();
})),
@ -1559,7 +1559,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.METAGROSS], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1;
p.formIndex = 1; // Mega Metagross
p.generateAndPopulateMoveset();
p.generateName();
})),
@ -1569,15 +1569,16 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.SWAMPERT], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1;
p.formIndex = 1; // Mega Swampert
p.generateAndPopulateMoveset();
p.generateName();
})),
[TrainerType.CYNTHIA]: new TrainerConfig(++t).initForChampion(signatureSpecies["CYNTHIA"], false).setBattleBgm("battle_sinnoh_champion").setMixedBattleBgm("battle_sinnoh_champion")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([Species.SPIRITOMB], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.GARCHOMP], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1;
p.formIndex = 1; // Mega Garchomp
p.generateAndPopulateMoveset();
p.generateName();
})),
@ -1590,7 +1591,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.LAPRAS], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1;
p.formIndex = 1; // G-Max Lapras
p.generateAndPopulateMoveset();
p.generateName();
})),
@ -1599,7 +1600,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.GARDEVOIR], TrainerSlot.TRAINER, true, p => {
p.formIndex = 1;
p.formIndex = 1; // Mega Gardevoir
p.generateAndPopulateMoveset();
p.generateName();
})),
@ -1612,7 +1613,7 @@ export const trainerConfigs: TrainerConfigs = {
p.generateAndPopulateMoveset();
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.CHARIZARD], TrainerSlot.TRAINER, true, p => {
p.formIndex = 3;
p.formIndex = 3; // G-Max Charizard
p.generateAndPopulateMoveset();
p.generateName();
})),
@ -1688,7 +1689,7 @@ export const trainerConfigs: TrainerConfigs = {
p.pokeball = PokeballType.MASTER_BALL;
p.shiny = true;
p.variant = 1;
p.formIndex = 1;
p.formIndex = 1; // Mega Rayquaza
p.generateName();
}))
.setGenModifiersFunc(party => {
@ -1706,7 +1707,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Kangaskhan
p.generateName();
})),
[TrainerType.ROCKET_BOSS_GIOVANNI_2]: new TrainerConfig(++t).setName("Giovanni").initForEvilTeamLeader("Rocket Boss", [], true).setMixedBattleBgm("battle_rocket_boss").setVictoryBgm("victory_team_plasma")
@ -1721,7 +1722,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Kangaskhan
p.generateName();
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([Species.GASTRODON, Species.SEISMITOAD]))
@ -1740,7 +1741,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Camerupt
p.generateName();
})),
[TrainerType.MAXIE_2]: new TrainerConfig(++t).setName("Maxie").initForEvilTeamLeader("Magma Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma")
@ -1751,7 +1752,7 @@ export const trainerConfigs: TrainerConfigs = {
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([Species.TORKOAL, Species.NINETALES], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.abilityIndex = 2; // DROUGHT
p.abilityIndex = 2; // Drought
}))
.setPartyMemberFunc(2, getRandomPartyMemberFunc([Species.SHIFTRY, Species.SCOVILLAIN], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -1762,7 +1763,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Camerupt
p.generateName();
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.GROUDON], TrainerSlot.TRAINER, true, p => {
@ -1780,7 +1781,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Sharpedo
p.generateName();
})),
[TrainerType.ARCHIE_2]: new TrainerConfig(++t).setName("Archie").initForEvilTeamLeader("Aqua Boss", [], true).setMixedBattleBgm("battle_aqua_magma_boss").setVictoryBgm("victory_team_plasma")
@ -1805,7 +1806,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Sharpedo
p.generateName();
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.KYOGRE], TrainerSlot.TRAINER, true, p => {
@ -1821,7 +1822,7 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.HOUNDOOM ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Houndoom
p.generateName();
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.WEAVILE], TrainerSlot.TRAINER, true, p => {
@ -1839,7 +1840,7 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(3, getRandomPartyMemberFunc([Species.HOUNDOOM], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Houndoom
p.generateName();
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([Species.WEAVILE, Species.SNEASLER], TrainerSlot.TRAINER, true, p => {
@ -1867,8 +1868,8 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GENESECT ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
p.formIndex = Utils.randSeedInt(5);
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = Utils.randSeedInt(5, 1); // Shock, Burn, Chill, or Douse Drive
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BASCULEGION, Species.JELLICENT ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
@ -1900,7 +1901,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Gyarados
p.generateName();
})),
[TrainerType.LYSANDRE_2]: new TrainerConfig(++t).setName("Lysandre").initForEvilTeamLeader("Flare Boss", [], true).setMixedBattleBgm("battle_flare_boss").setVictoryBgm("victory_team_plasma")
@ -1919,7 +1920,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
p.formIndex = 1;
p.formIndex = 1; // Mega Gyardos
p.generateName();
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([Species.YVELTAL], TrainerSlot.TRAINER, true, p => {
@ -1936,25 +1937,24 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.NIHILEGO ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ROGUE_BALL;
})),
[TrainerType.LUSAMINE_2]: new TrainerConfig(++t).setName("Lusamine").initForEvilTeamLeader("Aether Boss", [], true).setMixedBattleBgm("battle_aether_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLEFABLE ], TrainerSlot.TRAINER, true, p => {
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.NIHILEGO ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ROGUE_BALL;
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.MILOTIC, Species.PRIMARINA ]))
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.PHEROMOSA ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
}))
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.CLEFABLE ]))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.STAKATAKA, Species.CELESTEELA, Species.GUZZLORD ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
p.pokeball = PokeballType.ROGUE_BALL;
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.NIHILEGO ], TrainerSlot.TRAINER, true, p => {
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.PHEROMOSA ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
p.pokeball = PokeballType.ROGUE_BALL;
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.NECROZMA ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
@ -1968,37 +1968,41 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GALVANTULA, Species.VIKAVOLT]))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.formIndex = 1;
p.formIndex = 1; // Mega Pinsir
p.pokeball = PokeballType.ULTRA_BALL;
p.generateName();
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.GOLISOPOD ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
})),
[TrainerType.GUZMA_2]: new TrainerConfig(++t).setName("Guzma").initForEvilTeamLeader("Skull Boss", [], true).setMixedBattleBgm("battle_skull_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.GOLISOPOD ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.abilityIndex = 2; //Anticipation
p.pokeball = PokeballType.ULTRA_BALL;
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.HISUI_SAMUROTT, Species.CRAWDAUNT ], TrainerSlot.TRAINER, true, p => {
p.abilityIndex = 2; //Sharpness, Adaptability
p.abilityIndex = 2; //Sharpness Hisui Samurott, Adaptability Crawdaunt
}))
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.SCIZOR, Species.KLEAVOR ]))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.PINSIR ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.formIndex = 1;
p.generateName();
p.pokeball = PokeballType.ULTRA_BALL;
}))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.BUZZWOLE ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
p.pokeball = PokeballType.ROGUE_BALL;
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.XURKITREE ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
p.pokeball = PokeballType.ROGUE_BALL;
})),
[TrainerType.ROSE]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", []).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ]))
@ -2009,29 +2013,32 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.COPPERAJAH ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.formIndex = 1;
p.formIndex = 1; // G-Max Copperajah
p.generateName();
p.pokeball = PokeballType.ULTRA_BALL;
})),
[TrainerType.ROSE_2]: new TrainerConfig(++t).setName("Rose").initForEvilTeamLeader("Macro Boss", [], true).setMixedBattleBgm("battle_macro_boss").setVictoryBgm("victory_team_plasma")
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCHALUDON ], TrainerSlot.TRAINER, true, p => {
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.MELMETAL ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.ULTRA_BALL;
}))
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.AEGISLASH, Species.GHOLDENGO ]))
.setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.DRACOVISH, Species.DRACOZOLT ], TrainerSlot.TRAINER, true, p => {
p.generateAndPopulateMoveset();
p.abilityIndex = 1; //Strong Jaw, Hustle
p.abilityIndex = 1; //Strong Jaw Dracovish, Hustle Dracozolt
}))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.MELMETAL ]))
.setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.ARCHALUDON ]))
.setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.GALAR_ARTICUNO, Species.GALAR_ZAPDOS, Species.GALAR_MOLTRES ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
p.pokeball = PokeballType.ULTRA_BALL;
}))
.setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.COPPERAJAH ], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 2);
p.generateAndPopulateMoveset();
p.formIndex = 1;
p.formIndex = 1; // G-Max Copperajah
p.generateName();
p.pokeball = PokeballType.ULTRA_BALL;
})),
};

View File

@ -65,6 +65,7 @@ export enum BattlerTagType {
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
DISABLED = "DISABLED",
SUBSTITUTE = "SUBSTITUTE",
IGNORE_GHOST = "IGNORE_GHOST",
IGNORE_DARK = "IGNORE_DARK",
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",

View 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
}

View File

@ -17,7 +17,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
@ -58,6 +58,7 @@ import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase";
import { Challenges } from "#enums/challenges";
import { PokemonAnimType } from "#app/enums/pokemon-anim-type";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
export enum FieldPosition {
@ -566,6 +567,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return 1;
}
/** Resets the pokemon's field sprite properties, including position, alpha, and scale */
resetSprite(): void {
// Resetting properties should not be shown on the field
this.setVisible(false);
// Reset field position
this.setFieldPosition(FieldPosition.CENTER);
if (this.isOffsetBySubstitute()) {
this.x -= this.getSubstituteOffset()[0];
this.y -= this.getSubstituteOffset()[1];
}
// Reset sprite display properties
this.setAlpha(1);
this.setScale(this.getSpriteScale());
}
getHeldItems(): PokemonHeldItemModifier[] {
if (!this.scene) {
return [];
@ -640,6 +658,47 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
/**
* Returns the Pokemon's offset from its current field position in the event that
* it has a Substitute doll in effect. The offset is returned in `[ x, y ]` format.
* @see {@linkcode SubstituteTag}
* @see {@linkcode getFieldPositionOffset}
*/
getSubstituteOffset(): [ number, number ] {
return this.isPlayer() ? [-30, 10] : [30, -10];
}
/**
* Returns whether or not the Pokemon's position on the field is offset because
* the Pokemon has a Substitute active.
* @see {@linkcode SubstituteTag}
*/
isOffsetBySubstitute(): boolean {
const substitute = this.getTag(SubstituteTag);
if (substitute) {
if (substitute.sprite === undefined) {
return false;
}
// During the Pokemon's MoveEffect phase, the offset is removed to put the Pokemon "in focus"
const currentPhase = this.scene.getCurrentPhase();
if (currentPhase instanceof MoveEffectPhase && currentPhase.getPokemon() === this) {
return false;
}
return true;
} else {
return false;
}
}
/** If this Pokemon has a Substitute on the field, removes its sprite from the field. */
destroySubstitute(): void {
const substitute = this.getTag(SubstituteTag);
if (substitute && substitute.sprite) {
substitute.sprite.destroy();
}
}
setFieldPosition(fieldPosition: FieldPosition, duration?: integer): Promise<void> {
return new Promise(resolve => {
if (fieldPosition === this.fieldPosition) {
@ -1414,6 +1473,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyPreDefendAbAttrs(FullHpResistTypeAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier);
}
if (move.category === MoveCategory.STATUS && move.hitsSubstitute(source, this)) {
typeMultiplier.value = 0;
}
return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier;
}
@ -2385,6 +2448,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
if (damage.value) {
this.lapseTags(BattlerTagLapseType.HIT);
const substitute = this.getTag(SubstituteTag);
if (substitute && move.hitsSubstitute(source, this)) {
substitute.hp -= damage.value;
damage.value = 0;
}
if (this.isFullHp()) {
applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage);
} else if (!this.isPlayer() && damage.value >= this.hp) {
@ -2407,15 +2477,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.scene.gameData.gameStats.highestDamage = damage.value;
}
}
if (damage.value > 0) {
source.turnData.damageDealt += damage.value;
source.turnData.currDamageDealt = damage.value;
this.battleData.hitCount++;
const attackResult = { move: move.id, result: result as DamageResult, damage: damage.value, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
this.turnData.attacksReceived.unshift(attackResult);
if (source.isPlayer() && !this.isPlayer()) {
this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, damage);
}
}
}
// want to include is.Fainted() in case multi hit move ends early, still want to render message
if (source.turnData.hitsLeft === 1 || this.isFainted()) {
@ -2440,6 +2514,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// set splice index here, so future scene queues happen before FaintedPhase
this.scene.setPhaseQueueSplice();
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
this.destroySubstitute();
this.resetSummonData();
}
@ -2452,7 +2527,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!cancelled.value && typeMultiplier === 0) {
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
}
result = (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS;
result = (cancelled.value || typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS;
break;
}
@ -2499,6 +2574,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
*/
this.scene.setPhaseQueueSplice();
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure));
this.destroySubstitute();
this.resetSummonData();
}
@ -3124,6 +3200,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.summonData[k] = this.summonDataPrimer[k];
}
}
// If this Pokemon has a Substitute when loading in, play an animation to add its sprite
if (this.getTag(SubstituteTag)) {
this.scene.triggerPokemonBattleAnim(this, PokemonAnimType.SUBSTITUTE_ADD);
this.getTag(SubstituteTag)!.sourceInFocus = false;
}
this.summonDataPrimer = null;
}
this.updateInfo();
@ -3503,21 +3584,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* info container.
*/
leaveField(clearEffects: boolean = true, hideInfo: boolean = true) {
this.resetSprite();
this.resetTurnData();
if (clearEffects) {
this.destroySubstitute();
this.resetSummonData();
this.resetBattleData();
}
if (hideInfo) {
this.hideInfo();
}
this.setVisible(false);
this.scene.field.remove(this);
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true);
}
destroy(): void {
this.battleInfo?.destroy();
this.destroySubstitute();
super.destroy();
}
@ -3901,6 +3984,9 @@ export class PlayerPokemon extends Pokemon {
this.fusionVariant = pokemon.variant;
this.fusionGender = pokemon.gender;
this.fusionLuck = pokemon.luck;
if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) {
this.pauseEvolutions = true;
}
this.scene.validateAchv(achvs.SPLICE);
this.scene.gameData.gameStats.pokemonFused++;

View File

@ -70,5 +70,8 @@
"stockpilingOnAdd": "{{pokemonNameWithAffix}} hortet {{stockpiledCount}}!",
"disabledOnAdd": " {{moveName}} von {{pokemonNameWithAffix}} wurde blockiert!",
"disabledLapse": "{{moveName}} von {{pokemonNameWithAffix}} ist nicht länger blockiert!",
"tarShotOnAdd": "{{pokemonNameWithAffix}} ist nun schwach gegenüber Feuer-Attacken!"
"tarShotOnAdd": "{{pokemonNameWithAffix}} ist nun schwach gegenüber Feuer-Attacken!",
"substituteOnAdd": "Ein Delegator von {{pokemonNameWithAffix}} ist erschienen!",
"substituteOnHit": "Der Delegator steckt den Schlag für {{pokemonNameWithAffix}} ein!",
"substituteOnRemove": "Der Delegator von {{pokemonNameWithAffix}} hört auf zu wirken!"
}

View File

@ -70,5 +70,8 @@
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
"disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!",
"disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled.",
"tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!"
"tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!",
"substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!",
"substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!",
"substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!"
}

View File

@ -68,5 +68,7 @@
"swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!",
"exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!",
"safeguard": "{{targetName}} is protected by Safeguard!",
"substituteOnOverlap": "{{pokemonName}} already\nhas a substitute!",
"substituteNotEnoughHp": "But it does not have enough HP\nleft to make a substitute!",
"afterYou": "{{pokemonName}} took the kind offer!"
}

View File

@ -70,5 +70,8 @@
"stockpilingOnAdd": "¡{{pokemonNameWithAffix}} ha reservado energía por {{stockpiledCount}}ª vez!",
"disabledOnAdd": "¡Se ha anulado el movimiento {{moveName}}\nde {{pokemonNameWithAffix}}!",
"disabledLapse": "¡El movimiento {{moveName}} de {{pokemonNameWithAffix}} ya no está anulado!",
"tarShotOnAdd": "¡{{pokemonNameWithAffix}} se ha vuelto débil ante el fuego!"
"tarShotOnAdd": "¡{{pokemonNameWithAffix}} se ha vuelto débil ante el fuego!",
"substituteOnAdd": "¡{{pokemonNameWithAffix}} creó un sustituto!",
"substituteOnHit": "¡El sustituto recibe daño en lugar del {{pokemonNameWithAffix}}!",
"substituteOnRemove": "¡El sustituto del {{pokemonNameWithAffix}} se debilitó!"
}

View File

@ -70,5 +70,8 @@
"stockpilingOnAdd": "{{pokemonNameWithAffix}} utilise\nla capacité Stockage {{stockpiledCount}} fois !",
"disabledOnAdd": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} est mise sous entrave !",
"disabledLapse": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} nest plus sous entrave !",
"tarShotOnAdd": "{{pokemonNameWithAffix}} est maintenant\nvulnérable au feu !"
"tarShotOnAdd": "{{pokemonNameWithAffix}} est maintenant\nvulnérable au feu !",
"substituteOnAdd": "{{pokemonNameWithAffix}}\ncrée un clone !",
"substituteOnHit": "Le clone subit les dégâts à la place\nde {{pokemonNameWithAffix}} !",
"substituteOnRemove": "Le clone de {{pokemonNameWithAffix}}\ndisparait…"
}

View File

@ -1,6 +1,7 @@
{
"title": "Paramètres du Challenge",
"illegalEvolution": "{{pokemon}} est devenu\ninéligible pour ce challenge !",
"noneSelected": "Aucun sélectionné",
"singleGeneration": {
"name": "Monogénération",
"desc": "Vous ne pouvez choisir que des Pokémon de {{gen}} génération.",

View File

@ -2,9 +2,9 @@
"intro": "Bienvenue dans PokéRogue, un fangame axé sur les combats Pokémon avec des éléments roguelite !\n$Ce jeu nest pas monétisé et nous ne prétendons à la propriété daucun élément sous copyright utilisé.\n$Bien quen développement permanent, PokéRogue reste entièrement jouable.\n$Tout signalement de bugs et derreurs quelconques passe par le serveur Discord.\n$Si le jeu est lent, vérifiez que lAccélération Matérielle est activée dans les paramètres du navigateur.",
"accessMenu": "Accédez au menu avec M ou Échap lors de lattente dune\naction.\n$Il contient les paramètres et diverses fonctionnalités.",
"menu": "Vous pouvez accéder aux paramètres depuis ce menu.\n$Vous pouvez entre autres y changer la vitesse du jeu ou le style de fenêtre…\n$Mais également des tonnes dautres fonctionnalités, jetez-y un œil !",
"starterSelect": "Choisissez vos starters depuis cet écran avec Z ou Espace.\nIls formeront votre équipe de départ.\n$Chacun possède une valeur. Votre équipe peut avoir jusquà 6 membres, sans dépasser un cout de 10.\n$Vous pouvez aussi choisir le sexe, le talent et la forme en\nfonction des variants déjà capturés ou éclos.\n$Les IV dun starter sont les meilleurs de tous ceux de son espèce déjà possédés. Obtenez-en plusieurs !",
"starterSelect": "Choisissez vos starters depuis cet écran avec Z ou Espace.\nIls formeront votre équipe de départ.\n$Chacun possède une valeur. Votre équipe peut avoir\njusquà 6 membres, sans dépasser un cout de 10.\n$Vous pouvez aussi choisir le sexe, le talent et la forme en\nfonction des variants déjà capturés ou éclos.\n$Les IV dun starter sont les meilleurs de tous ceux de\nson espèce déjà possédés. Obtenez-en plusieurs !",
"pokerus": "Chaque jour, 3 starters tirés aléatoirement ont un contour violet.\n$Si un starter que vous possédez la, essayez de lajouter à votre équipe. Vérifiez bien son résumé !",
"statChange": "Les changements de stats persistent à travers les combats tant que le Pokémon nest pas rappelé.\n$Vos Pokémon sont rappelés avant un combat de Dresseur et avant dentrer dans un nouveau biome.\n$Vous pouvez voir en combat les changements de stats dun Pokémon en maintenant C ou Maj.\n$Vous pouvez également voir les capacités de ladversaire en maintenant V.\n$Seules les capacités que le Pokémon a utilisées dans ce combat sont consultables.",
"selectItem": "Après chaque combat, vous avez le choix entre 3 objets\ntirés au sort. Vous ne pouvez en prendre quun.\n$Cela peut être des objets consommables, des objets à\nfaire tenir, ou des objets passifs aux effets permanents.\n$La plupart des effets des objets non-consommables se cumuleront de diverses manières.\n$Certains objets napparaitront que sils ont une utilité immédiate, comme les objets dévolution.\n$Vous pouvez aussi transférer des objets tenus entre Pokémon en utilisant loption de transfert.\n$Loption de transfert apparait en bas à droite dès quun Pokémon de léquipe porte un objet.\n$Vous pouvez acheter des consommables avec de largent.\nPlus vous progressez, plus le choix sera large.\n$Choisir un des objets gratuits déclenchera le prochain combat, donc faites bien tous vos achats avant.",
"statChange": "Les changements de stats persistent à travers\nles combats tant que le Pokémon nest pas rappelé.\n$Vos Pokémon sont rappelés avant un combat de\nDresseur et avant dentrer dans un nouveau biome.\n$Vous pouvez voir en combat les changements de stats\ndun Pokémon en maintenant C ou Maj.\n$Vous pouvez également voir les capacités de ladversaire\nen maintenant V.\n$Seules les capacités que le Pokémon a utilisées dans\nce combat sont consultables.",
"selectItem": "Après chaque combat, vous avez le choix entre 3 objets\ntirés au sort. Vous ne pouvez en prendre quun.\n$Cela peut être des objets consommables, des objets à\nfaire tenir, ou des objets passifs aux effets permanents.\n$La plupart des effets des objets non-consommables se cumuleront de diverses manières.\n$Certains objets napparaitront que sils ont une utilité immédiate, comme les objets dévolution.\n$Vous pouvez aussi transférer des objets tenus entre\nPokémon en utilisant loption de transfert.\n$Loption de transfert apparait en bas à droite dès\nquun Pokémon de léquipe porte un objet.\n$Vous pouvez acheter des consommables avec de\nlargent. Plus vous progressez, plus le choix sera large.\n$Choisir un des objets gratuits déclenchera le prochain\ncombat, donc faites bien tous vos achats avant.",
"eggGacha": "Depuis cet écran, vous pouvez utiliser vos coupons\npour recevoir Œufs de Pokémon au hasard.\n$Les Œufs éclosent après avoir remporté un certain nombre de combats. Plus ils sont rares, plus ils mettent de temps.\n$Les Pokémon éclos ne rejoindront pas votre équipe, mais seront ajoutés à vos starters.\n$Les Pokémon issus dŒufs ont généralement de meilleurs IV que les Pokémon sauvages.\n$Certains Pokémon ne peuvent être obtenus que dans des Œufs.\n$Il y a 3 différentes machines à actionner avec différents\nbonus, prenez celle qui vous convient le mieux !"
}

View File

@ -70,5 +70,8 @@
"stockpilingOnAdd": "{{pokemonNameWithAffix}} ha usato Accumulo per la\n{{stockpiledCount}}ª volta!",
"disabledOnAdd": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} è stata bloccata!",
"disabledLapse": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} non è più bloccata!",
"tarShotOnAdd": "{{pokemonNameWithAffix}} è diventato vulnerabile\nal tipo Fuoco!"
"tarShotOnAdd": "{{pokemonNameWithAffix}} è diventato vulnerabile\nal tipo Fuoco!",
"substituteOnAdd": "Appare un sostituto di {{pokemonNameWithAffix}}!",
"substituteOnHit": "Il sostituto viene colpito al posto di {{pokemonNameWithAffix}}!",
"substituteOnRemove": "Il sostituto di {{pokemonNameWithAffix}} svanisce!"
}

View File

@ -70,5 +70,8 @@
"stockpilingOnAdd": "{{pokemonNameWithAffix}}[[는]]\n{{stockpiledCount}}개 비축했다!",
"disabledOnAdd": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n사용할 수 없다!",
"disabledLapse": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n이제 사용할 수 있다.",
"tarShotOnAdd": "{{pokemonNameWithAffix}}[[는]] 불꽃에 약해졌다!"
"tarShotOnAdd": "{{pokemonNameWithAffix}}[[는]] 불꽃에 약해졌다!",
"substituteOnAdd": "{{pokemonNameWithAffix}}의\n대타가 나타났다!",
"substituteOnHit": "{{pokemonNameWithAffix}}[[를]] 대신하여\n대타가 공격을 받았다!",
"substituteOnRemove": "{{pokemonNameWithAffix}}의\n대타는 사라져 버렸다..."
}

View File

@ -70,5 +70,8 @@
"stockpilingOnAdd": "{{pokemonNameWithAffix}} estocou {{stockpiledCount}}!",
"disabledOnAdd": "{{moveName}} de {{pokemonNameWithAffix}}\nfoi desabilitado!",
"disabledLapse": "{{moveName}} de {{pokemonNameWithAffix}}\nnão está mais desabilitado.",
"tarShotOnAdd": "{{pokemonNameWithAffix}} tornou-se mais fraco ao fogo!"
"tarShotOnAdd": "{{pokemonNameWithAffix}} tornou-se mais fraco ao fogo!",
"substituteOnAdd": "{{pokemonNameWithAffix}} colocou um substituto!",
"substituteOnHit": "O substituto tomou o dano pelo {{pokemonNameWithAffix}}!",
"substituteOnRemove": "O substituto de {{pokemonNameWithAffix}} desbotou!"
}

View File

@ -70,5 +70,8 @@
"stockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!",
"disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{{moveName}}",
"disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了",
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n变得怕火了"
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n变得怕火了",
"substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出现了",
"substituteOnHit": "替身代替{{pokemonNameWithAffix}}\n承受了攻击",
"substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……"
}

View File

@ -70,5 +70,8 @@
"stockpilingOnAdd": "{{pokemonNameWithAffix}}蓄力了{{stockpiledCount}}次!",
"disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{moveName}}",
"disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了",
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n變得怕火了"
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n變得怕火了",
"substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出現了",
"substituteOnHit": "替身代替{{pokemonNameWithAffix}}承受了攻擊!",
"substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……"
}

View File

@ -15,6 +15,7 @@ import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
import { VictoryPhase } from "./victory-phase";
import { SubstituteTag } from "#app/data/battler-tags";
export class AttemptCapturePhase extends PokemonPhase {
private pokeballType: PokeballType;
@ -36,6 +37,11 @@ export class AttemptCapturePhase extends PokemonPhase {
return this.end();
}
const substitute = pokemon.getTag(SubstituteTag);
if (substitute) {
substitute.sprite.setVisible(false);
}
this.scene.pokeballCounts[this.pokeballType]--;
this.originalY = pokemon.y;
@ -165,6 +171,11 @@ export class AttemptCapturePhase extends PokemonPhase {
pokemon.setVisible(true);
pokemon.untint(250, "Sine.easeOut");
const substitute = pokemon.getTag(SubstituteTag);
if (substitute) {
substitute.sprite.setVisible(true);
}
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));

View File

@ -19,7 +19,7 @@ export class CommonAnimPhase extends PokemonPhase {
}
start() {
new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, () => {
new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, false, () => {
this.end();
});
}

View File

@ -57,7 +57,7 @@ export class DamagePhase extends PokemonPhase {
this.scene.damageNumberHandler.add(this.getPokemon(), this.amount, this.damageResult, this.critical);
}
if (this.damageResult !== HitResult.OTHER) {
if (this.damageResult !== HitResult.OTHER && this.amount > 0) {
const flashTimer = this.scene.time.addEvent({
delay: 100,
repeat: 5,

View File

@ -139,7 +139,7 @@ export class FaintPhase extends PokemonPhase {
y: pokemon.y + 150,
ease: "Sine.easeIn",
onComplete: () => {
pokemon.setVisible(false);
pokemon.resetSprite();
pokemon.y -= 150;
pokemon.trySetStatus(StatusEffect.FAINT);
if (pokemon.isPlayer()) {

View File

@ -31,7 +31,9 @@ export class MoveAnimTestPhase extends BattlePhase {
initMoveAnim(this.scene, moveId).then(() => {
loadMoveAnimAssets(this.scene, [moveId], true)
.then(() => {
new MoveAnim(moveId, player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!, (player !== (allMoves[moveId] instanceof SelfStatusMove) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!).getBattlerIndex()).play(this.scene, () => { // TODO: are the bangs correct here?
const user = player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!;
const target = (player !== (allMoves[moveId] instanceof SelfStatusMove)) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!;
new MoveAnim(moveId, user, target.getBattlerIndex()).play(this.scene, allMoves[moveId].hitsSubstitute(user, target), () => { // TODO: are the bangs correct here?
if (player) {
this.playMoveAnim(moveQueue, false);
} else {

View File

@ -3,7 +3,7 @@ import { BattlerIndex } from "#app/battle";
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr } from "#app/data/ability";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag } from "#app/data/battler-tags";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type";
@ -120,7 +120,7 @@ export class MoveEffectPhase extends PokemonPhase {
const applyAttrs: Promise<void>[] = [];
// Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, () => { // TODO: is the bang correct here?
new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => {
/** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false;
for (const target of targets) {
@ -246,7 +246,7 @@ export class MoveEffectPhase extends PokemonPhase {
* If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock
*/
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) {
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
@ -258,14 +258,19 @@ export class MoveEffectPhase extends PokemonPhase {
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tags and tokens
// Only apply the following effects if the move was not deflected by a substitute
if (move.hitsSubstitute(user, target)) {
return resolve();
}
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING);
if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) {
target.lapseTag(BattlerTagType.SHELL_TRAP);
}
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
})).then(() => {
// Apply the user's post-attack ability effects
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {
@ -306,7 +311,20 @@ export class MoveEffectPhase extends PokemonPhase {
}
// Wait for all move effects to finish applying, then end this phase
Promise.allSettled(applyAttrs).then(() => this.end());
Promise.allSettled(applyAttrs).then(() => {
/**
* Remove the target's substitute (if it exists and has expired)
* after all targeted effects have applied.
* This prevents blocked effects from applying until after this hit resolves.
*/
targets.forEach(target => {
const substitute = target.getTag(SubstituteTag);
if (!!substitute && substitute.hp <= 0) {
target.lapseTag(BattlerTagType.SUBSTITUTE);
}
});
this.end();
});
});
});
}

View File

@ -167,6 +167,7 @@ export class MovePhase extends BattlePhase {
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
moveQueue.shift(); // Remove the second turn of charge moves
return this.end();
}
@ -186,6 +187,7 @@ export class MovePhase extends BattlePhase {
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
moveQueue.shift();
return this.end();

View File

@ -31,7 +31,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct?
}
pokemon.updateInfo(true);
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, () => {
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => {
this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined));
if (pokemon.status?.isPostTurn()) {
this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex));

View File

@ -0,0 +1,237 @@
import BattleScene from "#app/battle-scene";
import { SubstituteTag } from "#app/data/battler-tags";
import { PokemonAnimType } from "#enums/pokemon-anim-type";
import Pokemon from "#app/field/pokemon";
import { BattlePhase } from "#app/phases/battle-phase";
export class PokemonAnimPhase extends BattlePhase {
/** The type of animation to play in this phase */
private key: PokemonAnimType;
/** The Pokemon to which this animation applies */
private pokemon: Pokemon;
/** Any other field sprites affected by this animation */
private fieldAssets: Phaser.GameObjects.Sprite[];
constructor(scene: BattleScene, key: PokemonAnimType, pokemon: Pokemon, fieldAssets?: Phaser.GameObjects.Sprite[]) {
super(scene);
this.key = key;
this.pokemon = pokemon;
this.fieldAssets = fieldAssets ?? [];
}
start(): void {
super.start();
switch (this.key) {
case PokemonAnimType.SUBSTITUTE_ADD:
this.doSubstituteAddAnim();
break;
case PokemonAnimType.SUBSTITUTE_PRE_MOVE:
this.doSubstitutePreMoveAnim();
break;
case PokemonAnimType.SUBSTITUTE_POST_MOVE:
this.doSubstitutePostMoveAnim();
break;
case PokemonAnimType.SUBSTITUTE_REMOVE:
this.doSubstituteRemoveAnim();
break;
default:
this.end();
}
}
doSubstituteAddAnim(): void {
const substitute = this.pokemon.getTag(SubstituteTag);
if (substitute === null) {
return this.end();
}
const getSprite = () => {
const sprite = this.scene.addFieldSprite(
this.pokemon.x + this.pokemon.getSprite().x,
this.pokemon.y + this.pokemon.getSprite().y,
`pkmn${this.pokemon.isPlayer() ? "__back": ""}__sub`
);
sprite.setOrigin(0.5, 1);
this.scene.field.add(sprite);
return sprite;
};
const [ subSprite, subTintSprite ] = [ getSprite(), getSprite() ];
const subScale = this.pokemon.getSpriteScale() * (this.pokemon.isPlayer() ? 0.5 : 1);
subSprite.setVisible(false);
subSprite.setScale(subScale);
subTintSprite.setTintFill(0xFFFFFF);
subTintSprite.setScale(0.01);
if (this.pokemon.isPlayer()) {
this.scene.field.bringToTop(this.pokemon);
}
this.scene.playSound("PRSFX- Transform");
this.scene.tweens.add({
targets: this.pokemon,
duration: 500,
x: this.pokemon.x + this.pokemon.getSubstituteOffset()[0],
y: this.pokemon.y + this.pokemon.getSubstituteOffset()[1],
alpha: 0.5,
ease: "Sine.easeIn"
});
this.scene.tweens.add({
targets: subTintSprite,
delay: 250,
scale: subScale,
ease: "Cubic.easeInOut",
duration: 500,
onComplete: () => {
subSprite.setVisible(true);
this.pokemon.scene.tweens.add({
targets: subTintSprite,
delay: 250,
alpha: 0,
ease: "Cubic.easeOut",
duration: 1000,
onComplete: () => {
subTintSprite.destroy();
substitute.sprite = subSprite;
this.end();
}
});
}
});
}
doSubstitutePreMoveAnim(): void {
if (this.fieldAssets.length !== 1) {
return this.end();
}
const subSprite = this.fieldAssets[0];
if (subSprite === undefined) {
return this.end();
}
this.scene.tweens.add({
targets: subSprite,
alpha: 0,
ease: "Sine.easeInOut",
duration: 500
});
this.scene.tweens.add({
targets: this.pokemon,
x: subSprite.x,
y: subSprite.y,
alpha: 1,
ease: "Sine.easeInOut",
delay: 250,
duration: 500,
onComplete: () => this.end()
});
}
doSubstitutePostMoveAnim(): void {
if (this.fieldAssets.length !== 1) {
return this.end();
}
const subSprite = this.fieldAssets[0];
if (subSprite === undefined) {
return this.end();
}
this.scene.tweens.add({
targets: this.pokemon,
x: subSprite.x + this.pokemon.getSubstituteOffset()[0],
y: subSprite.y + this.pokemon.getSubstituteOffset()[1],
alpha: 0.5,
ease: "Sine.easeInOut",
duration: 500
});
this.scene.tweens.add({
targets: subSprite,
alpha: 1,
ease: "Sine.easeInOut",
delay: 250,
duration: 500,
onComplete: () => this.end()
});
}
doSubstituteRemoveAnim(): void {
if (this.fieldAssets.length !== 1) {
return this.end();
}
const subSprite = this.fieldAssets[0];
if (subSprite === undefined) {
return this.end();
}
const getSprite = () => {
const sprite = this.scene.addFieldSprite(
subSprite.x,
subSprite.y,
`pkmn${this.pokemon.isPlayer() ? "__back": ""}__sub`
);
sprite.setOrigin(0.5, 1);
this.scene.field.add(sprite);
return sprite;
};
const subTintSprite = getSprite();
const subScale = this.pokemon.getSpriteScale() * (this.pokemon.isPlayer() ? 0.5 : 1);
subTintSprite.setAlpha(0);
subTintSprite.setTintFill(0xFFFFFF);
subTintSprite.setScale(subScale);
this.scene.tweens.add({
targets: subTintSprite,
alpha: 1,
ease: "Sine.easeInOut",
duration: 500,
onComplete: () => {
subSprite.destroy();
const flashTimer = this.scene.time.addEvent({
delay: 100,
repeat: 7,
startAt: 200,
callback: () => {
this.scene.playSound("PRSFX- Substitute2.wav");
subTintSprite.setVisible(flashTimer.repeatCount % 2 === 0);
if (!flashTimer.repeatCount) {
this.scene.tweens.add({
targets: subTintSprite,
scale: 0.01,
ease: "Sine.cubicEaseIn",
duration: 500
});
this.scene.tweens.add({
targets: this.pokemon,
x: this.pokemon.x - this.pokemon.getSubstituteOffset()[0],
y: this.pokemon.y - this.pokemon.getSubstituteOffset()[1],
alpha: 1,
ease: "Sine.easeInOut",
delay: 250,
duration: 500,
onComplete: () => {
subTintSprite.destroy();
this.end();
}
});
}
}
});
}
});
}
}

View File

@ -42,7 +42,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
pokemon.updateInfo();
}
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, () => this.end());
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end());
} else {
this.end();
}

View File

@ -16,6 +16,7 @@ export class ReturnPhase extends SwitchSummonPhase {
onEnd(): void {
const pokemon = this.getPokemon();
pokemon.resetSprite();
pokemon.resetTurnData();
pokemon.resetSummonData();

View File

@ -53,7 +53,7 @@ export class ScanIvsPhase extends PokemonPhase {
this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.clearText();
new CommonBattleAnim(CommonAnim.LOCK_ON, pokemon, pokemon).play(this.scene, () => {
new CommonBattleAnim(CommonAnim.LOCK_ON, pokemon, pokemon).play(this.scene, false, () => {
this.scene.ui.getMessageHandler().promptIvs(pokemon.id, pokemon.ivs, this.shownIvs).then(() => this.end());
});
}, () => {

View File

@ -11,6 +11,7 @@ import { Command } from "#app/ui/command-ui-handler";
import i18next from "i18next";
import { PostSummonPhase } from "./post-summon-phase";
import { SummonPhase } from "./summon-phase";
import { SubstituteTag } from "#app/data/battler-tags";
export class SwitchSummonPhase extends SummonPhase {
private slotIndex: integer;
@ -65,6 +66,16 @@ export class SwitchSummonPhase extends SummonPhase {
if (!this.batonPass) {
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
const substitute = pokemon.getTag(SubstituteTag);
if (substitute) {
this.scene.tweens.add({
targets: substitute.sprite,
duration: 250,
scale: substitute.sprite.scale * 0.5,
ease: "Sine.easeIn",
onComplete: () => substitute.sprite.destroy()
});
}
}
this.scene.ui.showText(this.player ?
@ -115,8 +126,18 @@ export class SwitchSummonPhase extends SummonPhase {
pokemonName: this.getPokemon().getNameToRender()
})
);
// Ensure improperly persisted summon data (such as tags) is cleared upon switching
if (!this.batonPass) {
/**
* If this switch is passing a Substitute, make the switched Pokemon match the returned Pokemon's state as it left.
* Otherwise, clear any persisting tags on the returned Pokemon.
*/
if (this.batonPass) {
const substitute = this.lastPokemon.getTag(SubstituteTag);
if (substitute) {
switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0];
switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1];
switchedInPokemon.setAlpha(0.5);
}
} else {
switchedInPokemon.resetBattleData();
switchedInPokemon.resetSummonData();
}

View File

@ -4,7 +4,9 @@ import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { BerryPhase } from "#app/phases/berry-phase";
const TIMEOUT = 20 * 1000;
@ -32,37 +34,57 @@ describe("Abilities - Unseen Fist", () => {
game.override.enemyLevel(100);
});
test(
"ability causes a contact move to ignore Protect",
it(
"should cause a contact move to ignore Protect",
() => testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, true),
TIMEOUT
);
test(
"ability does not cause a non-contact move to ignore Protect",
it(
"should not cause a non-contact move to ignore Protect",
() => testUnseenFistHitResult(game, Moves.ABSORB, Moves.PROTECT, false),
TIMEOUT
);
test(
"ability does not apply if the source has Long Reach",
it(
"should not apply if the source has Long Reach",
() => {
game.override.passiveAbility(Abilities.LONG_REACH);
testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false);
}, TIMEOUT
);
test(
"ability causes a contact move to ignore Wide Guard",
it(
"should cause a contact move to ignore Wide Guard",
() => testUnseenFistHitResult(game, Moves.BREAKING_SWIPE, Moves.WIDE_GUARD, true),
TIMEOUT
);
test(
"ability does not cause a non-contact move to ignore Wide Guard",
it(
"should not cause a non-contact move to ignore Wide Guard",
() => testUnseenFistHitResult(game, Moves.BULLDOZE, Moves.WIDE_GUARD, false),
TIMEOUT
);
it(
"should cause a contact move to ignore Protect, but not Substitute",
async () => {
game.override.enemyLevel(1);
game.override.moveset([Moves.TACKLE]);
await game.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!;
enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id);
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(BerryPhase, false);
expect(enemyPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeUndefined();
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
}, TIMEOUT
);
});
async function testUnseenFistHitResult(game: GameManager, attackMove: Moves, protectMove: Moves, shouldSucceed: boolean = true): Promise<void> {

View File

@ -0,0 +1,234 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import Pokemon, { MoveResult, PokemonTurnData, TurnMove, PokemonMove } from "#app/field/pokemon";
import BattleScene from "#app/battle-scene";
import { BattlerTagLapseType, SubstituteTag, TrappedTag } from "#app/data/battler-tags";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves";
import { PokemonAnimType } from "#app/enums/pokemon-anim-type";
import * as messages from "#app/messages";
import { allMoves } from "#app/data/move";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
vi.mock("#app/battle-scene.js");
const TIMEOUT = 5 * 1000; // 5 sec timeout
describe("BattlerTag - SubstituteTag", () => {
let mockPokemon: Pokemon;
describe("onAdd behavior", () => {
beforeEach(() => {
mockPokemon = {
scene: new BattleScene(),
hp: 101,
id: 0,
getMaxHp: vi.fn().mockReturnValue(101) as Pokemon["getMaxHp"],
findAndRemoveTags: vi.fn().mockImplementation((tagFilter) => {
// simulate a Trapped tag set by another Pokemon, then expect the filter to catch it.
const trapTag = new TrappedTag(BattlerTagType.TRAPPED, BattlerTagLapseType.CUSTOM, 0, Moves.NONE, 1);
expect(tagFilter(trapTag)).toBeTruthy();
return true;
}) as Pokemon["findAndRemoveTags"]
} as Pokemon;
vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue("");
vi.spyOn(mockPokemon.scene, "getPokemonById").mockImplementation(pokemonId => mockPokemon.id === pokemonId ? mockPokemon : null);
});
it(
"sets the tag's HP to 1/4 of the source's max HP (rounded down)",
async () => {
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true);
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
subject.onAdd(mockPokemon);
expect(subject.hp).toBe(25);
}, TIMEOUT
);
it(
"triggers on-add effects that bring the source out of focus",
async () => {
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation(
(pokemon, battleAnimType, fieldAssets?, delayed?) => {
expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_ADD);
return true;
}
);
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
subject.onAdd(mockPokemon);
expect(subject.sourceInFocus).toBeFalsy();
expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1);
expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1);
}, TIMEOUT
);
it(
"removes effects that trap the source",
async () => {
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
subject.onAdd(mockPokemon);
expect(mockPokemon.findAndRemoveTags).toHaveBeenCalledTimes(1);
}, TIMEOUT
);
});
describe("onRemove behavior", () => {
beforeEach(() => {
mockPokemon = {
scene: new BattleScene(),
hp: 101,
id: 0,
isFainted: vi.fn().mockReturnValue(false) as Pokemon["isFainted"]
} as Pokemon;
vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue("");
});
it(
"triggers on-remove animation and message",
async () => {
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
subject.sourceInFocus = false;
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation(
(pokemon, battleAnimType, fieldAssets?, delayed?) => {
expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_REMOVE);
return true;
}
);
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
subject.onRemove(mockPokemon);
expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1);
expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1);
}, TIMEOUT
);
});
describe("lapse behavior", () => {
beforeEach(() => {
mockPokemon = {
scene: new BattleScene(),
hp: 101,
id: 0,
turnData: {acted: true} as PokemonTurnData,
getLastXMoves: vi.fn().mockReturnValue([{move: Moves.TACKLE, result: MoveResult.SUCCESS} as TurnMove]) as Pokemon["getLastXMoves"],
} as Pokemon;
vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue("");
});
it(
"PRE_MOVE lapse triggers pre-move animation",
async () => {
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation(
(pokemon, battleAnimType, fieldAssets?, delayed?) => {
expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_PRE_MOVE);
return true;
}
);
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
expect(subject.lapse(mockPokemon, BattlerTagLapseType.PRE_MOVE)).toBeTruthy();
expect(subject.sourceInFocus).toBeTruthy();
expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1);
expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled();
}, TIMEOUT
);
it(
"AFTER_MOVE lapse triggers post-move animation",
async () => {
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockImplementation(
(pokemon, battleAnimType, fieldAssets?, delayed?) => {
expect(battleAnimType).toBe(PokemonAnimType.SUBSTITUTE_POST_MOVE);
return true;
}
);
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
expect(subject.lapse(mockPokemon, BattlerTagLapseType.AFTER_MOVE)).toBeTruthy();
expect(subject.sourceInFocus).toBeFalsy();
expect(mockPokemon.scene.triggerPokemonBattleAnim).toHaveBeenCalledTimes(1);
expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled();
}, TIMEOUT
);
/** TODO: Figure out how to mock a MoveEffectPhase correctly for this test */
it.skip(
"HIT lapse triggers on-hit message",
async () => {
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true);
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
const pokemonMove = {
getMove: vi.fn().mockReturnValue(allMoves[Moves.TACKLE]) as PokemonMove["getMove"]
} as PokemonMove;
const moveEffectPhase = {
move: pokemonMove,
getUserPokemon: vi.fn().mockReturnValue(undefined) as MoveEffectPhase["getUserPokemon"]
} as MoveEffectPhase;
vi.spyOn(mockPokemon.scene, "getCurrentPhase").mockReturnValue(moveEffectPhase);
vi.spyOn(allMoves[Moves.TACKLE], "hitsSubstitute").mockReturnValue(true);
expect(subject.lapse(mockPokemon, BattlerTagLapseType.HIT)).toBeTruthy();
expect(mockPokemon.scene.triggerPokemonBattleAnim).not.toHaveBeenCalled();
expect(mockPokemon.scene.queueMessage).toHaveBeenCalledTimes(1);
}, TIMEOUT
);
it(
"CUSTOM lapse flags the tag for removal",
async () => {
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true);
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
expect(subject.lapse(mockPokemon, BattlerTagLapseType.CUSTOM)).toBeFalsy();
}, TIMEOUT
);
it(
"Unsupported lapse type does nothing",
async () => {
const subject = new SubstituteTag(Moves.SUBSTITUTE, mockPokemon.id);
vi.spyOn(mockPokemon.scene, "triggerPokemonBattleAnim").mockReturnValue(true);
vi.spyOn(mockPokemon.scene, "queueMessage").mockReturnValue();
expect(subject.lapse(mockPokemon, BattlerTagLapseType.TURN_END)).toBeTruthy();
expect(mockPokemon.scene.triggerPokemonBattleAnim).not.toHaveBeenCalled();
expect(mockPokemon.scene.queueMessage).not.toHaveBeenCalled();
}
);
});
});

View File

@ -1,16 +1,17 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import { Species } from "#enums/species";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Stat } from "#enums/stat";
import { Stat, BATTLE_STATS } from "#enums/stat";
import { Abilities } from "#enums/abilities";
import { MoveEndPhase } from "#app/phases/move-end-phase";
describe("Moves - Guard Swap", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -27,37 +28,42 @@ describe("Moves - Guard Swap", () => {
game.override
.battleType("single")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([Moves.SHELL_SMASH])
.enemySpecies(Species.MEW)
.enemyMoveset(Moves.SPLASH)
.enemySpecies(Species.INDEEDEE)
.enemyLevel(200)
.moveset([ Moves.GUARD_SWAP ])
.ability(Abilities.NONE);
});
it("should swap the user's DEF AND SPDEF stat stages with the target's", async () => {
await game.startBattle([
it("should swap the user's DEF and SPDEF stat stages with the target's", async () => {
await game.classicMode.startBattle([
Species.INDEEDEE
]);
// Should start with no stat stages
const player = game.scene.getPlayerPokemon()!;
// After Shell Smash, should have +2 in ATK and SPATK, -1 in DEF and SPDEF
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(1));
game.move.select(Moves.GUARD_SWAP);
await game.phaseInterceptor.to(MoveEndPhase);
expect(player.getStatStage(Stat.DEF)).toBe(0);
expect(player.getStatStage(Stat.SPDEF)).toBe(0);
expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
expect(enemy.getStatStage(Stat.SPDEF)).toBe(-1);
for (const s of BATTLE_STATS) {
expect(player.getStatStage(s)).toBe(0);
expect(enemy.getStatStage(s)).toBe(1);
}
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStatStage(Stat.DEF)).toBe(-1);
expect(player.getStatStage(Stat.SPDEF)).toBe(-1);
expect(enemy.getStatStage(Stat.DEF)).toBe(0);
expect(enemy.getStatStage(Stat.SPDEF)).toBe(0);
}, 20000);
for (const s of BATTLE_STATS) {
if (s === Stat.DEF || s === Stat.SPDEF) {
expect(player.getStatStage(s)).toBe(1);
expect(enemy.getStatStage(s)).toBe(0);
} else {
expect(player.getStatStage(s)).toBe(0);
expect(enemy.getStatStage(s)).toBe(1);
}
}
}, TIMEOUT);
});

View File

@ -0,0 +1,64 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import { Species } from "#enums/species";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { BATTLE_STATS } from "#enums/stat";
import { Abilities } from "#enums/abilities";
import { MoveEndPhase } from "#app/phases/move-end-phase";
describe("Moves - Heart Swap", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.enemySpecies(Species.INDEEDEE)
.enemyLevel(200)
.moveset([ Moves.HEART_SWAP ])
.ability(Abilities.NONE);
});
it("should swap all of the user's stat stages with the target's", async () => {
await game.classicMode.startBattle([
Species.MANAPHY
]);
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(1));
game.move.select(Moves.HEART_SWAP);
await game.phaseInterceptor.to(MoveEndPhase);
for (const s of BATTLE_STATS) {
expect(player.getStatStage(s)).toBe(0);
expect(enemy.getStatStage(s)).toBe(1);
}
await game.phaseInterceptor.to(TurnEndPhase);
for (const s of BATTLE_STATS) {
expect(enemy.getStatStage(s)).toBe(0);
expect(player.getStatStage(s)).toBe(1);
}
}, TIMEOUT);
});

View File

@ -1,16 +1,17 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import { Species } from "#enums/species";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Stat } from "#enums/stat";
import { Stat, BATTLE_STATS } from "#enums/stat";
import { Abilities } from "#enums/abilities";
import { MoveEndPhase } from "#app/phases/move-end-phase";
describe("Moves - Power Swap", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -27,36 +28,42 @@ describe("Moves - Power Swap", () => {
game.override
.battleType("single")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([Moves.SHELL_SMASH])
.enemySpecies(Species.MEW)
.enemyMoveset(Moves.SPLASH)
.enemySpecies(Species.INDEEDEE)
.enemyLevel(200)
.moveset([ Moves.POWER_SWAP ])
.ability(Abilities.NONE);
});
it("should swap the user's ATK AND SPATK stat stages with the target's", async () => {
await game.startBattle([
it("should swap the user's ATK and SPATK stat stages with the target's", async () => {
await game.classicMode.startBattle([
Species.INDEEDEE
]);
// Should start with no stat stages
const player = game.scene.getPlayerPokemon()!;
// After Shell Smash, should have +2 in ATK and SPATK, -1 in DEF and SPDEF
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy.summonData, "statStages", "get").mockReturnValue(new Array(BATTLE_STATS.length).fill(1));
game.move.select(Moves.POWER_SWAP);
await game.phaseInterceptor.to(MoveEndPhase);
expect(player.getStatStage(Stat.ATK)).toBe(0);
expect(player.getStatStage(Stat.SPATK)).toBe(0);
expect(enemy.getStatStage(Stat.ATK)).toBe(2);
expect(enemy.getStatStage(Stat.SPATK)).toBe(2);
for (const s of BATTLE_STATS) {
expect(player.getStatStage(s)).toBe(0);
expect(enemy.getStatStage(s)).toBe(1);
}
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStatStage(Stat.ATK)).toBe(2);
expect(player.getStatStage(Stat.SPATK)).toBe(2);
expect(enemy.getStatStage(Stat.ATK)).toBe(0);
expect(enemy.getStatStage(Stat.SPATK)).toBe(0);
}, 20000);
for (const s of BATTLE_STATS) {
if (s === Stat.ATK || s === Stat.SPATK) {
expect(player.getStatStage(s)).toBe(1);
expect(enemy.getStatStage(s)).toBe(0);
} else {
expect(player.getStatStage(s)).toBe(0);
expect(enemy.getStatStage(s)).toBe(1);
}
}
}, TIMEOUT);
});

View File

@ -0,0 +1,81 @@
import { Type } from "#app/data/type";
import { Challenges } from "#app/enums/challenges";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Relic Song", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.RELIC_SONG, Moves.SPLASH])
.battleType("single")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.enemySpecies(Species.MAGIKARP)
.enemyLevel(100);
});
it("swaps Meloetta's form between Aria and Pirouette", async () => {
await game.classicMode.startBattle([Species.MELOETTA]);
const meloetta = game.scene.getPlayerPokemon()!;
game.move.select(Moves.RELIC_SONG);
await game.toNextTurn();
expect(meloetta.formIndex).toBe(1);
game.move.select(Moves.RELIC_SONG);
await game.phaseInterceptor.to("BerryPhase");
expect(meloetta.formIndex).toBe(0);
}, TIMEOUT);
it("doesn't swap Meloetta's form during a mono-type challenge", async () => {
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.PSYCHIC + 1, 0);
await game.challengeMode.startBattle([Species.MELOETTA]);
const meloetta = game.scene.getPlayerPokemon()!;
expect(meloetta.formIndex).toBe(0);
game.move.select(Moves.RELIC_SONG);
await game.phaseInterceptor.to("BerryPhase");
await game.toNextTurn();
expect(meloetta.formIndex).toBe(0);
}, TIMEOUT);
it("doesn't swap Meloetta's form during biome change (arena reset)", async () => {
game.override
.starterForms({[Species.MELOETTA]: 1})
.startingWave(10);
await game.classicMode.startBattle([Species.MELOETTA]);
const meloetta = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.toNextWave();
expect(meloetta.formIndex).toBe(1);
}, TIMEOUT);
});

View File

@ -0,0 +1,515 @@
import { SubstituteTag, TrappedTag } from "#app/data/battler-tags";
import { allMoves, StealHeldItemChanceAttr } from "#app/data/move";
import { StatusEffect } from "#app/data/status-effect";
import { Abilities } from "#app/enums/abilities";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { BerryType } from "#app/enums/berry-type";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import { Stat } from "#app/enums/stat";
import { MoveResult } from "#app/field/pokemon";
import { CommandPhase } from "#app/phases/command-phase";
import GameManager from "#app/test/utils/gameManager";
import { Command } from "#app/ui/command-ui-handler";
import { Mode } from "#app/ui/ui";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const TIMEOUT = 20 * 1000; // 20 sec timeout
describe("Moves - Substitute", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.moveset([Moves.SUBSTITUTE, Moves.SWORDS_DANCE, Moves.TACKLE, Moves.SPLASH])
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.INSOMNIA)
.enemyMoveset(Moves.SPLASH)
.startingLevel(100)
.enemyLevel(100);
});
it(
"should cause the user to take damage",
async () => {
await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("MoveEndPhase", false);
expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4));
}, TIMEOUT
);
it(
"should redirect enemy attack damage to the Substitute doll",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
await game.classicMode.startBattle([Species.SKARMORY]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("MoveEndPhase", false);
expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4));
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
const postSubHp = leadPokemon.hp;
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.hp).toBe(postSubHp);
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
}, TIMEOUT
);
it(
"should fade after redirecting more damage than its remaining HP",
async () => {
// Giga Impact OHKOs Magikarp if substitute isn't up
game.override.enemyMoveset(Array(4).fill(Moves.GIGA_IMPACT));
vi.spyOn(allMoves[Moves.GIGA_IMPACT], "accuracy", "get").mockReturnValue(100);
await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("MoveEndPhase", false);
expect(leadPokemon.hp).toBe(Math.ceil(leadPokemon.getMaxHp() * 3/4));
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
const postSubHp = leadPokemon.hp;
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.hp).toBe(postSubHp);
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeUndefined();
}, TIMEOUT
);
it(
"should block stat changes from status moves",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.CHARM));
await game.classicMode.startBattle([Species.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
}
);
it(
"should be bypassed by sound-based moves",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.ECHOED_VOICE));
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("MoveEndPhase");
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
const postSubHp = leadPokemon.hp;
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
expect(leadPokemon.hp).toBeLessThan(postSubHp);
}, TIMEOUT
);
it(
"should be bypassed by attackers with Infiltrator",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.enemyAbility(Abilities.INFILTRATOR);
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("MoveEndPhase");
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
const postSubHp = leadPokemon.hp;
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getTag(BattlerTagType.SUBSTITUTE)).toBeDefined();
expect(leadPokemon.hp).toBeLessThan(postSubHp);
}, TIMEOUT
);
it(
"shouldn't block the user's own status moves",
async () => {
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("MoveEndPhase");
await game.toNextTurn();
game.move.select(Moves.SWORDS_DANCE);
await game.phaseInterceptor.to("MoveEndPhase", false);
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
}, TIMEOUT
);
it(
"should protect the user from flinching",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.FAKE_OUT));
game.override.startingLevel(1); // Ensures the Substitute will break
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
}, TIMEOUT
);
it(
"should protect the user from being trapped",
async () => {
vi.spyOn(allMoves[Moves.SAND_TOMB], "accuracy", "get").mockReturnValue(100);
game.override.enemyMoveset(Array(4).fill(Moves.SAND_TOMB));
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getTag(TrappedTag)).toBeUndefined();
}, TIMEOUT
);
it(
"should prevent the user's stats from being lowered",
async () => {
vi.spyOn(allMoves[Moves.LIQUIDATION], "chance", "get").mockReturnValue(100);
game.override.enemyMoveset(Array(4).fill(Moves.LIQUIDATION));
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getStatStage(Stat.DEF)).toBe(0);
}, TIMEOUT
);
it(
"should protect the user from being afflicted with status effects",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.NUZZLE));
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.status?.effect).not.toBe(StatusEffect.PARALYSIS);
}, TIMEOUT
);
it(
"should prevent the user's items from being stolen",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.THIEF));
vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([new StealHeldItemChanceAttr(1.0)]); // give Thief 100% steal rate
game.override.startingHeldItems([{name: "BERRY", type: BerryType.SITRUS}]);
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getHeldItems().length).toBe(1);
}, TIMEOUT
);
it(
"should prevent the user's items from being removed",
async () => {
game.override.moveset([Moves.KNOCK_OFF]);
game.override.enemyHeldItems([{name: "BERRY", type: BerryType.SITRUS}]);
await game.classicMode.startBattle([Species.BLASTOISE]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id);
const enemyNumItems = enemyPokemon.getHeldItems().length;
game.move.select(Moves.KNOCK_OFF);
await game.phaseInterceptor.to("MoveEndPhase", false);
expect(enemyPokemon.getHeldItems().length).toBe(enemyNumItems);
}, TIMEOUT
);
it(
"move effect should prevent the user's berries from being stolen and eaten",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.BUG_BITE));
game.override.startingHeldItems([{name: "BERRY", type: BerryType.SITRUS}]);
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("MoveEndPhase", false);
const enemyPostAttackHp = enemyPokemon.hp;
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getHeldItems().length).toBe(1);
expect(enemyPokemon.hp).toBe(enemyPostAttackHp);
}, TIMEOUT
);
it(
"should prevent the user's stats from being reset by Clear Smog",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.CLEAR_SMOG));
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
game.move.select(Moves.SWORDS_DANCE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
}, TIMEOUT
);
it(
"should prevent the user from becoming confused",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.MAGICAL_TORQUE));
vi.spyOn(allMoves[Moves.MAGICAL_TORQUE], "chance", "get").mockReturnValue(100);
await game.classicMode.startBattle([Species.BLASTOISE]);
const leadPokemon = game.scene.getPlayerPokemon()!;
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
game.move.select(Moves.SWORDS_DANCE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(leadPokemon.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
expect(leadPokemon.getStatStage(Stat.ATK)).toBe(2);
}
);
it(
"should transfer to the switched in Pokemon when the source uses Baton Pass",
async () => {
game.override.moveset([Moves.SUBSTITUTE, Moves.BATON_PASS]);
await game.classicMode.startBattle([Species.BLASTOISE, Species.CHARIZARD]);
const leadPokemon = game.scene.getPlayerPokemon()!;
leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id);
// Simulate a Baton switch for the player this turn
game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.POKEMON, 1, true);
});
await game.phaseInterceptor.to("MovePhase", false);
const switchedPokemon = game.scene.getPlayerPokemon()!;
const subTag = switchedPokemon.getTag(SubstituteTag)!;
expect(subTag).toBeDefined();
expect(subTag.hp).toBe(Math.floor(leadPokemon.getMaxHp() * 1/4));
}, TIMEOUT
);
it(
"should prevent the source's Rough Skin from activating when hit",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.ability(Abilities.ROUGH_SKIN);
await game.classicMode.startBattle([Species.BLASTOISE]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
}, TIMEOUT
);
it(
"should prevent the source's Focus Punch from failing when hit",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.moveset([Moves.FOCUS_PUNCH]);
// Make Focus Punch 40 power to avoid a KO
vi.spyOn(allMoves[Moves.FOCUS_PUNCH], "calculateBattlePower").mockReturnValue(40);
await game.classicMode.startBattle([Species.BLASTOISE]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id);
game.move.select(Moves.FOCUS_PUNCH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
}, TIMEOUT
);
it(
"should not allow Shell Trap to activate when attacked",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.moveset([Moves.SHELL_TRAP]);
await game.classicMode.startBattle([Species.BLASTOISE]);
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id);
game.move.select(Moves.SHELL_TRAP);
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
}, TIMEOUT
);
it(
"should not allow Beak Blast to burn opponents when hit",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.moveset([Moves.BEAK_BLAST]);
await game.classicMode.startBattle([Species.BLASTOISE]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id);
game.move.select(Moves.BEAK_BLAST);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.BURN);
}, TIMEOUT
);
it(
"should cause incoming attacks to not activate Counter",
async() => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
game.override.moveset([Moves.COUNTER]);
await game.classicMode.startBattle([Species.BLASTOISE]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
playerPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, playerPokemon.id);
game.move.select(Moves.COUNTER);
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
}
);
});

View File

@ -8,6 +8,7 @@ import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SubstituteTag } from "#app/data/battler-tags";
describe("Moves - Tidy Up", () => {
@ -39,7 +40,7 @@ describe("Moves - Tidy Up", () => {
it("spikes are cleared", async () => {
game.override.moveset([Moves.SPIKES, Moves.TIDY_UP]);
game.override.enemyMoveset([Moves.SPIKES, Moves.SPIKES, Moves.SPIKES, Moves.SPIKES]);
await game.startBattle();
await game.classicMode.startBattle();
game.move.select(Moves.SPIKES);
await game.phaseInterceptor.to(TurnEndPhase);
@ -52,7 +53,7 @@ describe("Moves - Tidy Up", () => {
it("stealth rocks are cleared", async () => {
game.override.moveset([Moves.STEALTH_ROCK, Moves.TIDY_UP]);
game.override.enemyMoveset([Moves.STEALTH_ROCK, Moves.STEALTH_ROCK, Moves.STEALTH_ROCK, Moves.STEALTH_ROCK]);
await game.startBattle();
await game.classicMode.startBattle();
game.move.select(Moves.STEALTH_ROCK);
await game.phaseInterceptor.to(TurnEndPhase);
@ -64,7 +65,7 @@ describe("Moves - Tidy Up", () => {
it("toxic spikes are cleared", async () => {
game.override.moveset([Moves.TOXIC_SPIKES, Moves.TIDY_UP]);
game.override.enemyMoveset([Moves.TOXIC_SPIKES, Moves.TOXIC_SPIKES, Moves.TOXIC_SPIKES, Moves.TOXIC_SPIKES]);
await game.startBattle();
await game.classicMode.startBattle();
game.move.select(Moves.TOXIC_SPIKES);
await game.phaseInterceptor.to(TurnEndPhase);
@ -77,7 +78,7 @@ describe("Moves - Tidy Up", () => {
game.override.moveset([Moves.STICKY_WEB, Moves.TIDY_UP]);
game.override.enemyMoveset([Moves.STICKY_WEB, Moves.STICKY_WEB, Moves.STICKY_WEB, Moves.STICKY_WEB]);
await game.startBattle();
await game.classicMode.startBattle();
game.move.select(Moves.STICKY_WEB);
await game.phaseInterceptor.to(TurnEndPhase);
@ -86,21 +87,26 @@ describe("Moves - Tidy Up", () => {
expect(game.scene.arena.getTag(ArenaTagType.STICKY_WEB)).toBeUndefined();
}, 20000);
it.skip("substitutes are cleared", async () => {
it("substitutes are cleared", async () => {
game.override.moveset([Moves.SUBSTITUTE, Moves.TIDY_UP]);
game.override.enemyMoveset([Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE, Moves.SUBSTITUTE]);
await game.startBattle();
await game.classicMode.startBattle();
game.move.select(Moves.SUBSTITUTE);
await game.phaseInterceptor.to(TurnEndPhase);
game.move.select(Moves.TIDY_UP);
await game.phaseInterceptor.to(MoveEndPhase);
// TODO: check for subs here once the move is implemented
const pokemon = [ game.scene.getPlayerPokemon()!, game.scene.getEnemyPokemon()! ];
pokemon.forEach(p => {
expect(p).toBeDefined();
expect(p!.getTag(SubstituteTag)).toBeUndefined();
});
}, 20000);
it("user's stats are raised with no traps set", async () => {
await game.startBattle();
await game.classicMode.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;

View File

@ -877,7 +877,7 @@ export default class PartyUiHandler extends MessageUiHandler {
this.options.push(PartyOption.SUMMARY);
this.options.push(PartyOption.RENAME);
if (pokemon.pauseEvolutions && pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId)) {
if (pokemon.pauseEvolutions && (pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) || (pokemon.isFusion() && pokemon.fusionSpecies && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId)))) {
this.options.push(PartyOption.UNPAUSE_EVOLUTION);
}

View File

@ -995,15 +995,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
delete starterAttributes.shiny;
}
if (starterAttributes.variant !== undefined && !isNaN(starterAttributes.variant)) {
if (starterAttributes.variant !== undefined) {
const unlockedVariants = [
hasNonShiny,
hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT,
hasShiny && caughtAttr & DexAttr.VARIANT_2,
hasShiny && caughtAttr & DexAttr.VARIANT_3
];
if (!unlockedVariants[starterAttributes.variant + 1]) { // add 1 as -1 = non-shiny
// requested variant wasn't unlocked, purging setting
if (isNaN(starterAttributes.variant) || starterAttributes.variant < 0 || !unlockedVariants[starterAttributes.variant]) {
// variant value is invalid or requested variant wasn't unlocked, purging setting
delete starterAttributes.variant;
}
}

View File

@ -8,6 +8,7 @@ import {Button} from "#enums/buttons";
import { Moves } from "#enums/moves";
import Pokemon from "#app/field/pokemon";
import { ModifierBar } from "#app/modifier/modifier";
import { SubstituteTag } from "#app/data/battler-tags";
export type TargetSelectCallback = (targets: BattlerIndex[]) => void;
@ -111,7 +112,7 @@ export default class TargetSelectUiHandler extends UiHandler {
if (this.targetFlashTween) {
this.targetFlashTween.stop();
for (const pokemon of multipleTargets) {
pokemon.setAlpha(1);
pokemon.setAlpha(!!pokemon.getTag(SubstituteTag) ? 0.5 : 1);
this.highlightItems(pokemon.id, 1);
}
}
@ -162,7 +163,7 @@ export default class TargetSelectUiHandler extends UiHandler {
}
for (const pokemon of this.targetsHighlighted) {
pokemon.setAlpha(1);
pokemon.setAlpha(!!pokemon.getTag(SubstituteTag) ? 0.5 : 1);
this.highlightItems(pokemon.id, 1);
}