mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-19 14:52:19 +02:00
merge conflicts
This commit is contained in:
commit
ada362e84f
@ -1 +1 @@
|
||||
Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef
|
||||
Subproject commit 3ccef8472dd7cc7c362538489954cb8fdad27e5f
|
@ -1387,7 +1387,7 @@ export default class BattleScene extends SceneBase {
|
||||
case Species.ZYGARDE:
|
||||
return Utils.randSeedInt(4);
|
||||
case Species.MINIOR:
|
||||
return Utils.randSeedInt(6);
|
||||
return Utils.randSeedInt(7);
|
||||
case Species.ALCREMIE:
|
||||
return Utils.randSeedInt(9);
|
||||
case Species.MEOWSTIC:
|
||||
|
@ -7,7 +7,7 @@ import { Weather, WeatherType } from "./weather";
|
||||
import { BattlerTag, GroundedTag } from "./battler-tags";
|
||||
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
||||
import { Gender } from "./gender";
|
||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
|
||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
|
||||
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||
import { TerrainType } from "./terrain";
|
||||
@ -1139,7 +1139,9 @@ export class MoveEffectChanceMultiplierAbAttr extends AbAttr {
|
||||
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||
// Disable showAbility during getTargetBenefitScore
|
||||
this.showAbility = args[4];
|
||||
if ((args[0] as Utils.NumberHolder).value <= 0 || (args[1] as Move).id === Moves.ORDER_UP) {
|
||||
|
||||
const exceptMoves = [ Moves.ORDER_UP, Moves.ELECTRO_SHOT ];
|
||||
if ((args[0] as Utils.NumberHolder).value <= 0 || exceptMoves.includes((args[1] as Move).id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1329,7 +1331,6 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
|
||||
*/
|
||||
const exceptAttrs: Constructor<MoveAttr>[] = [
|
||||
MultiHitAttr,
|
||||
ChargeAttr,
|
||||
SacrificialAttr,
|
||||
SacrificialAttrOnHit
|
||||
];
|
||||
@ -1345,6 +1346,7 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
|
||||
|
||||
/** Also check if this move is an Attack move and if it's only targeting one Pokemon */
|
||||
return numTargets === 1
|
||||
&& !move.isChargingMove()
|
||||
&& !exceptAttrs.some(attr => move.hasAttr(attr))
|
||||
&& !exceptMoves.some(id => move.id === id)
|
||||
&& move.category !== MoveCategory.STATUS;
|
||||
@ -2431,11 +2433,12 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
|
||||
super(true);
|
||||
}
|
||||
|
||||
applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
|
||||
async applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): Promise<boolean> {
|
||||
const targets = pokemon.getOpponents();
|
||||
if (simulated || !targets.length) {
|
||||
return simulated;
|
||||
}
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
let target: Pokemon = targets[0];
|
||||
if (targets.length > 1) {
|
||||
@ -2447,6 +2450,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
|
||||
if (target.battleData.illusion.active) {
|
||||
return false;
|
||||
}
|
||||
target = target!;
|
||||
pokemon.summonData.speciesForm = target.getSpeciesForm();
|
||||
pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
|
||||
pokemon.summonData.ability = target.getAbility().id;
|
||||
@ -2463,18 +2467,23 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
|
||||
pokemon.setStatStage(s, target.getStatStage(s));
|
||||
}
|
||||
|
||||
pokemon.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId ?? Moves.NONE, m?.ppUsed, m?.ppUp));
|
||||
pokemon.summonData.moveset = target.getMoveset().map(m => {
|
||||
const pp = m?.getMove().pp ?? 0;
|
||||
// if PP value is less than 5, do nothing. If greater, we need to reduce the value to 5 using a negative ppUp value.
|
||||
const ppUp = pp <= 5 ? 0 : (5 - pp) / Math.max(Math.floor(pp / 5), 1);
|
||||
return new PokemonMove(m?.moveId ?? Moves.NONE, 0, ppUp);
|
||||
});
|
||||
pokemon.summonData.types = target.getTypes();
|
||||
promises.push(pokemon.updateInfo());
|
||||
|
||||
|
||||
pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target!.name, }));
|
||||
pokemon.scene.playSound("battle_anims/PRSFX- Transform");
|
||||
|
||||
pokemon.loadAssets(false).then(() => {
|
||||
promises.push(pokemon.loadAssets(false).then(() => {
|
||||
pokemon.playAnim();
|
||||
pokemon.updateInfo();
|
||||
});
|
||||
}));
|
||||
|
||||
pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target.name, }));
|
||||
await Promise.all(promises);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -4213,6 +4222,11 @@ export class RedirectTypeMoveAbAttr extends RedirectMoveAbAttr {
|
||||
|
||||
export class BlockRedirectAbAttr extends AbAttr { }
|
||||
|
||||
/**
|
||||
* Used by Early Bird, makes the pokemon wake up faster
|
||||
* @param statusEffect - The {@linkcode StatusEffect} to check for
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class ReduceStatusEffectDurationAbAttr extends AbAttr {
|
||||
private statusEffect: StatusEffect;
|
||||
|
||||
@ -4222,9 +4236,19 @@ export class ReduceStatusEffectDurationAbAttr extends AbAttr {
|
||||
this.statusEffect = statusEffect;
|
||||
}
|
||||
|
||||
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||
/**
|
||||
* Reduces the number of sleep turns remaining by an extra 1 when applied
|
||||
* @param args - The args passed to the `AbAttr`:
|
||||
* - `[0]` - The {@linkcode StatusEffect} of the Pokemon
|
||||
* - `[1]` - The number of turns remaining until the status is healed
|
||||
* @returns `true` if the ability was applied
|
||||
*/
|
||||
apply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||
if (!(args[1] instanceof Utils.NumberHolder)) {
|
||||
return false;
|
||||
}
|
||||
if (args[0] === this.statusEffect) {
|
||||
(args[1] as Utils.IntegerHolder).value = Utils.toDmgValue((args[1] as Utils.IntegerHolder).value / 2);
|
||||
args[1].value -= 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -970,6 +970,9 @@ export class GravityTag extends ArenaTag {
|
||||
if (pokemon !== null) {
|
||||
pokemon.removeTag(BattlerTagType.FLOATING);
|
||||
pokemon.removeTag(BattlerTagType.TELEKINESIS);
|
||||
if (pokemon.getTag(BattlerTagType.FLYING)) {
|
||||
pokemon.addTag(BattlerTagType.INTERRUPTED);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
//import { battleAnimRawData } from "./battle-anim-raw-data";
|
||||
import BattleScene from "../battle-scene";
|
||||
import { AttackMove, BeakBlastHeaderAttr, ChargeAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move";
|
||||
import { AttackMove, BeakBlastHeaderAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move";
|
||||
import Pokemon from "../field/pokemon";
|
||||
import * as Utils from "../utils";
|
||||
import { BattlerIndex } from "../battle";
|
||||
@ -476,8 +476,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
|
||||
} else {
|
||||
const loadedCheckTimer = setInterval(() => {
|
||||
if (moveAnims.get(move) !== null) {
|
||||
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] || allMoves[move].getAttrs(DelayedAttackAttr)[0];
|
||||
if (chargeAttr && chargeAnims.get(chargeAttr.chargeAnim) === null) {
|
||||
const chargeAnimSource = (allMoves[move].isChargingMove())
|
||||
? allMoves[move]
|
||||
: (allMoves[move].getAttrs(DelayedAttackAttr)[0]
|
||||
?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]);
|
||||
if (chargeAnimSource && chargeAnims.get(chargeAnimSource.chargeAnim) === null) {
|
||||
return;
|
||||
}
|
||||
clearInterval(loadedCheckTimer);
|
||||
@ -507,11 +510,12 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
|
||||
} else {
|
||||
populateMoveAnim(move, ba);
|
||||
}
|
||||
const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0]
|
||||
|| allMoves[move].getAttrs(DelayedAttackAttr)[0]
|
||||
|| allMoves[move].getAttrs(BeakBlastHeaderAttr)[0];
|
||||
if (chargeAttr) {
|
||||
initMoveChargeAnim(scene, chargeAttr.chargeAnim).then(() => resolve());
|
||||
const chargeAnimSource = (allMoves[move].isChargingMove())
|
||||
? allMoves[move]
|
||||
: (allMoves[move].getAttrs(DelayedAttackAttr)[0]
|
||||
?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]);
|
||||
if (chargeAnimSource) {
|
||||
initMoveChargeAnim(scene, chargeAnimSource.chargeAnim).then(() => resolve());
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
@ -638,11 +642,12 @@ export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLo
|
||||
return new Promise(resolve => {
|
||||
const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat();
|
||||
for (const moveId of moveIds) {
|
||||
const chargeAttr = allMoves[moveId].getAttrs(ChargeAttr)[0]
|
||||
|| allMoves[moveId].getAttrs(DelayedAttackAttr)[0]
|
||||
|| allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0];
|
||||
if (chargeAttr) {
|
||||
const moveChargeAnims = chargeAnims.get(chargeAttr.chargeAnim);
|
||||
const chargeAnimSource = (allMoves[moveId].isChargingMove())
|
||||
? allMoves[moveId]
|
||||
: (allMoves[moveId].getAttrs(DelayedAttackAttr)[0]
|
||||
?? allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]);
|
||||
if (chargeAnimSource) {
|
||||
const moveChargeAnims = chargeAnims.get(chargeAnimSource.chargeAnim);
|
||||
moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct?
|
||||
if (Array.isArray(moveChargeAnims)) {
|
||||
moveAnimations.push(moveChargeAnims[1]);
|
||||
|
@ -11,7 +11,6 @@ import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "#app/d
|
||||
import Move, {
|
||||
allMoves,
|
||||
applyMoveAttrs,
|
||||
ChargeAttr,
|
||||
ConsecutiveUseDoublePowerAttr,
|
||||
HealOnAllyAttr,
|
||||
MoveCategory,
|
||||
@ -949,10 +948,6 @@ export class EncoreTag extends BattlerTag {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (allMoves[repeatableMove.move].hasAttr(ChargeAttr) && repeatableMove.result === MoveResult.OTHER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.moveId = repeatableMove.move;
|
||||
|
||||
return true;
|
||||
@ -2591,7 +2586,7 @@ export class TormentTag extends MoveRestrictionBattlerTag {
|
||||
// This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY
|
||||
// Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts
|
||||
const moveObj = allMoves[lastMove.move];
|
||||
const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr);
|
||||
const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY);
|
||||
const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS);
|
||||
if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) {
|
||||
return true;
|
||||
|
429
src/data/move.ts
429
src/data/move.ts
@ -289,10 +289,9 @@ export default class Move implements Localizable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter function that returns if the move targets itself or an ally
|
||||
* Getter function that returns if the move targets the user or its ally
|
||||
* @returns boolean
|
||||
*/
|
||||
|
||||
isAllyTarget(): boolean {
|
||||
switch (this.moveTarget) {
|
||||
case MoveTarget.USER:
|
||||
@ -306,6 +305,10 @@ export default class Move implements Localizable {
|
||||
return false;
|
||||
}
|
||||
|
||||
isChargingMove(): this is ChargingMove {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the move is immune to certain types.
|
||||
* Currently looks at cases of Grass types with powder moves and Dark types with moves affected by Prankster.
|
||||
@ -893,6 +896,85 @@ export class SelfStatusMove extends Move {
|
||||
}
|
||||
}
|
||||
|
||||
type SubMove = new (...args: any[]) => Move;
|
||||
|
||||
function ChargeMove<TBase extends SubMove>(Base: TBase) {
|
||||
return class extends Base {
|
||||
/** The animation to play during the move's charging phase */
|
||||
public readonly chargeAnim: ChargeAnim = ChargeAnim[`${Moves[this.id]}_CHARGING`];
|
||||
/** The message to show during the move's charging phase */
|
||||
private _chargeText: string;
|
||||
|
||||
/** Move attributes that apply during the move's charging phase */
|
||||
public chargeAttrs: MoveAttr[] = [];
|
||||
|
||||
override isChargingMove(): this is ChargingMove {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text to be displayed during this move's charging phase.
|
||||
* References to the user Pokemon should be written as "{USER}", and
|
||||
* references to the target Pokemon should be written as "{TARGET}".
|
||||
* @param chargeText the text to set
|
||||
* @returns this {@linkcode Move} (for chaining API purposes)
|
||||
*/
|
||||
chargeText(chargeText: string): this {
|
||||
this._chargeText = chargeText;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the charge text to display to the player
|
||||
* @param user the {@linkcode Pokemon} using this move
|
||||
* @param target the {@linkcode Pokemon} targeted by this move (optional)
|
||||
*/
|
||||
showChargeText(user: Pokemon, target?: Pokemon): void {
|
||||
user.scene.queueMessage(this._chargeText
|
||||
.replace("{USER}", getPokemonNameWithAffix(user))
|
||||
.replace("{TARGET}", getPokemonNameWithAffix(target))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all charge attributes of the given attribute type.
|
||||
* @param attrType any attribute that extends {@linkcode MoveAttr}
|
||||
* @returns Array of attributes that match `attrType`, or an empty array if
|
||||
* no matches are found.
|
||||
*/
|
||||
getChargeAttrs<T extends MoveAttr>(attrType: Constructor<T>): T[] {
|
||||
return this.chargeAttrs.filter((attr): attr is T => attr instanceof attrType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this move has an attribute of the given type.
|
||||
* @param attrType any attribute that extends {@linkcode MoveAttr}
|
||||
* @returns `true` if a matching attribute is found; `false` otherwise
|
||||
*/
|
||||
hasChargeAttr<T extends MoveAttr>(attrType: Constructor<T>): boolean {
|
||||
return this.chargeAttrs.some((attr) => attr instanceof attrType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an attribute to this move to be applied during the move's charging phase
|
||||
* @param ChargeAttrType the type of {@linkcode MoveAttr} being added
|
||||
* @param args the parameters to construct the given {@linkcode MoveAttr} with
|
||||
* @returns this {@linkcode Move} (for chaining API purposes)
|
||||
*/
|
||||
chargeAttr<T extends Constructor<MoveAttr>>(ChargeAttrType: T, ...args: ConstructorParameters<T>): this {
|
||||
const chargeAttr = new ChargeAttrType(...args);
|
||||
this.chargeAttrs.push(chargeAttr);
|
||||
|
||||
return this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class ChargingAttackMove extends ChargeMove(AttackMove) {}
|
||||
export class ChargingSelfStatusMove extends ChargeMove(SelfStatusMove) {}
|
||||
|
||||
export type ChargingMove = ChargingAttackMove | ChargingSelfStatusMove;
|
||||
|
||||
/**
|
||||
* Base class defining all {@linkcode Move} Attributes
|
||||
* @abstract
|
||||
@ -2050,15 +2132,15 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
|
||||
|
||||
export class StatusEffectAttr extends MoveEffectAttr {
|
||||
public effect: StatusEffect;
|
||||
public cureTurn: integer | null;
|
||||
public overrideStatus: boolean;
|
||||
public turnsRemaining?: number;
|
||||
public overrideStatus: boolean = false;
|
||||
|
||||
constructor(effect: StatusEffect, selfTarget?: boolean, cureTurn?: integer, overrideStatus?: boolean) {
|
||||
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
|
||||
super(selfTarget, MoveEffectTrigger.HIT);
|
||||
|
||||
this.effect = effect;
|
||||
this.cureTurn = cureTurn!; // TODO: is this bang correct?
|
||||
this.overrideStatus = !!overrideStatus;
|
||||
this.turnsRemaining = turnsRemaining;
|
||||
this.overrideStatus = overrideStatus;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
@ -2085,7 +2167,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0))
|
||||
&& pokemon.trySetStatus(this.effect, true, user, this.cureTurn)) {
|
||||
&& pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining)) {
|
||||
applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect);
|
||||
return true;
|
||||
}
|
||||
@ -2102,8 +2184,8 @@ export class StatusEffectAttr extends MoveEffectAttr {
|
||||
export class MultiStatusEffectAttr extends StatusEffectAttr {
|
||||
public effects: StatusEffect[];
|
||||
|
||||
constructor(effects: StatusEffect[], selfTarget?: boolean, cureTurn?: integer, overrideStatus?: boolean) {
|
||||
super(effects[0], selfTarget, cureTurn, overrideStatus);
|
||||
constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) {
|
||||
super(effects[0], selfTarget, turnsRemaining, overrideStatus);
|
||||
this.effects = effects;
|
||||
}
|
||||
|
||||
@ -2574,6 +2656,63 @@ export class OneHitKOAttr extends MoveAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute that allows charge moves to resolve in 1 turn under a given condition.
|
||||
* Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`.
|
||||
* @extends MoveAttr
|
||||
*/
|
||||
export class InstantChargeAttr extends MoveAttr {
|
||||
/** The condition in which the move with this attribute instantly charges */
|
||||
protected readonly condition: UserMoveConditionFunc;
|
||||
|
||||
constructor(condition: UserMoveConditionFunc) {
|
||||
super(true);
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags the move with this attribute as instantly charged if this attribute's condition is met.
|
||||
* @param user the {@linkcode Pokemon} using the move
|
||||
* @param target n/a
|
||||
* @param move the {@linkcode Move} associated with this attribute
|
||||
* @param args
|
||||
* - `[0]` a {@linkcode Utils.BooleanHolder | BooleanHolder} for the "instant charge" flag
|
||||
* @returns `true` if the instant charge condition is met; `false` otherwise.
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon | null, move: Move, args: any[]): boolean {
|
||||
const instantCharge = args[0];
|
||||
if (!(instantCharge instanceof Utils.BooleanHolder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.condition(user, move)) {
|
||||
instantCharge.value = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute that allows charge moves to resolve in 1 turn while specific {@linkcode WeatherType | Weather}
|
||||
* is active. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`.
|
||||
* @extends InstantChargeAttr
|
||||
*/
|
||||
export class WeatherInstantChargeAttr extends InstantChargeAttr {
|
||||
constructor(weatherTypes: WeatherType[]) {
|
||||
super((user, move) => {
|
||||
const currentWeather = user.scene.arena.weather;
|
||||
|
||||
if (Utils.isNullOrUndefined(currentWeather?.weatherType)) {
|
||||
return false;
|
||||
} else {
|
||||
return !currentWeather?.isEffectSuppressed(user.scene)
|
||||
&& weatherTypes.includes(currentWeather?.weatherType);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class OverrideMoveEffectAttr extends MoveAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise<boolean> {
|
||||
//const overridden = args[0] as Utils.BooleanHolder;
|
||||
@ -2582,111 +2721,6 @@ export class OverrideMoveEffectAttr extends MoveAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export class ChargeAttr extends OverrideMoveEffectAttr {
|
||||
public chargeAnim: ChargeAnim;
|
||||
private chargeText: string;
|
||||
private tagType: BattlerTagType | null;
|
||||
private chargeEffect: boolean;
|
||||
public followUpPriority: integer | null;
|
||||
|
||||
constructor(chargeAnim: ChargeAnim, chargeText: string, tagType?: BattlerTagType | null, chargeEffect: boolean = false) {
|
||||
super();
|
||||
|
||||
this.chargeAnim = chargeAnim;
|
||||
this.chargeText = chargeText;
|
||||
this.tagType = tagType!; // TODO: is this bang correct?
|
||||
this.chargeEffect = chargeEffect;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
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, 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);
|
||||
}
|
||||
if (this.chargeEffect) {
|
||||
applyMoveAttrs(MoveEffectAttr, user, target, move);
|
||||
}
|
||||
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
|
||||
user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true });
|
||||
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
|
||||
resolve(true);
|
||||
});
|
||||
} else {
|
||||
user.lapseTag(BattlerTagType.CHARGING);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
usedChargeEffect(user: Pokemon, target: Pokemon | null, move: Move): boolean {
|
||||
if (!this.chargeEffect) {
|
||||
return false;
|
||||
}
|
||||
// Account for move history being populated when this function is called
|
||||
const lastMoves = user.getLastXMoves(2);
|
||||
return lastMoves.length === 2 && lastMoves[1].move === move.id && lastMoves[1].result === MoveResult.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
export class SunlightChargeAttr extends ChargeAttr {
|
||||
constructor(chargeAnim: ChargeAnim, chargeText: string) {
|
||||
super(chargeAnim, chargeText);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const weatherType = user.scene.arena.weather?.weatherType;
|
||||
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN)) {
|
||||
resolve(false);
|
||||
} else {
|
||||
super.apply(user, target, move, args).then(result => resolve(result));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ElectroShotChargeAttr extends ChargeAttr {
|
||||
private statIncreaseApplied: boolean;
|
||||
constructor() {
|
||||
super(ChargeAnim.ELECTRO_SHOT_CHARGING, i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }), null, true);
|
||||
// Add a flag because ChargeAttr skills use themselves twice instead of once over one-to-two turns
|
||||
this.statIncreaseApplied = false;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const weatherType = user.scene.arena.weather?.weatherType;
|
||||
if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.RAIN || weatherType === WeatherType.HEAVY_RAIN)) {
|
||||
// Apply the SPATK increase every call when used in the rain
|
||||
const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true);
|
||||
statChangeAttr.apply(user, target, move, args);
|
||||
// After the SPATK is raised, execute the move resolution e.g. deal damage
|
||||
resolve(false);
|
||||
} else {
|
||||
if (!this.statIncreaseApplied) {
|
||||
// Apply the SPATK increase only if it hasn't been applied before e.g. on the first turn charge up animation
|
||||
const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true);
|
||||
statChangeAttr.apply(user, target, move, args);
|
||||
// Set the flag to true so that on the following turn it doesn't raise SPATK a second time
|
||||
this.statIncreaseApplied = true;
|
||||
}
|
||||
super.apply(user, target, move, args).then(result => {
|
||||
if (!result) {
|
||||
// On the second turn, reset the statIncreaseApplied flag without applying the SPATK increase
|
||||
this.statIncreaseApplied = false;
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||
public tagType: ArenaTagType;
|
||||
public chargeAnim: ChargeAnim;
|
||||
@ -4878,6 +4912,37 @@ export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move)
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attribute that grants {@link https://bulbapedia.bulbagarden.net/wiki/Semi-invulnerable_turn | semi-invulnerability} to the user during
|
||||
* the associated move's charging phase. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`.
|
||||
* @extends MoveEffectAttr
|
||||
*/
|
||||
export class SemiInvulnerableAttr extends MoveEffectAttr {
|
||||
/** The type of {@linkcode SemiInvulnerableTag} to grant to the user */
|
||||
public tagType: BattlerTagType;
|
||||
|
||||
constructor(tagType: BattlerTagType) {
|
||||
super(true);
|
||||
this.tagType = tagType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants a {@linkcode SemiInvulnerableTag} to the associated move's user.
|
||||
* @param user the {@linkcode Pokemon} using the move
|
||||
* @param target n/a
|
||||
* @param move the {@linkcode Move} being used
|
||||
* @param args n/a
|
||||
* @returns `true` if semi-invulnerability was successfully granted; `false` otherwise.
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return user.addTag(this.tagType, 1, move.id, user.id);
|
||||
}
|
||||
}
|
||||
|
||||
export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
public tagType: BattlerTagType;
|
||||
public turnCountMin: integer;
|
||||
@ -6138,7 +6203,7 @@ const lastMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (allMoves[copiableMove].hasAttr(ChargeAttr)) {
|
||||
if (allMoves[copiableMove].isChargingMove()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -6286,7 +6351,7 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (allMoves[copiableMove.move].hasAttr(ChargeAttr) && copiableMove.result === MoveResult.OTHER) {
|
||||
if (allMoves[copiableMove.move].isChargingMove() && copiableMove.result === MoveResult.OTHER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -6579,13 +6644,13 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
export class TransformAttr extends MoveEffectAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
|
||||
if (!super.apply(user, target, move, args) || target.battleData.illusion.active || user.battleData.illusion.active) {
|
||||
user.scene.queueMessage(i18next.t("battle:attackFailed"));
|
||||
return resolve(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
user.summonData.speciesForm = target.getSpeciesForm();
|
||||
user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm();
|
||||
user.summonData.ability = target.getAbility().id;
|
||||
@ -6605,17 +6670,24 @@ export class TransformAttr extends MoveEffectAttr {
|
||||
user.setStatStage(s, target.getStatStage(s));
|
||||
}
|
||||
|
||||
user.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId!, m?.ppUsed, m?.ppUp)); // TODO: is this bang correct?
|
||||
user.summonData.moveset = target.getMoveset().map(m => {
|
||||
const pp = m?.getMove().pp ?? 0;
|
||||
// if PP value is less than 5, do nothing. If greater, we need to reduce the value to 5 using a negative ppUp value.
|
||||
const ppUp = pp <= 5 ? 0 : (5 - pp) / Math.max(Math.floor(pp / 5), 1);
|
||||
return new PokemonMove(m?.moveId!, 0, ppUp);
|
||||
});
|
||||
user.summonData.types = target.getTypes();
|
||||
promises.push(user.updateInfo());
|
||||
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
user.loadAssets(false).then(() => {
|
||||
promises.push(user.loadAssets(false).then(() => {
|
||||
user.playAnim();
|
||||
user.updateInfo();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
await Promise.all(promises);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6987,6 +7059,20 @@ function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null
|
||||
});
|
||||
}
|
||||
|
||||
function applyMoveChargeAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, args: any[]): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const chargeAttrPromises: Promise<boolean>[] = [];
|
||||
const chargeMoveAttrs = move.chargeAttrs.filter(a => attrFilter(a));
|
||||
for (const attr of chargeMoveAttrs) {
|
||||
const result = attr.apply(user, target, move, args);
|
||||
if (result instanceof Promise) {
|
||||
chargeAttrPromises.push(result);
|
||||
}
|
||||
}
|
||||
Promise.allSettled(chargeAttrPromises).then(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
export function applyMoveAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: Move, ...args: any[]): Promise<void> {
|
||||
return applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
|
||||
}
|
||||
@ -6995,6 +7081,10 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon
|
||||
return applyMoveAttrsInternal(attrFilter, user, target, move, args);
|
||||
}
|
||||
|
||||
export function applyMoveChargeAttrs(attrType: Constructor<MoveAttr>, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, ...args: any[]): Promise<void> {
|
||||
return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args);
|
||||
}
|
||||
|
||||
export class MoveCondition {
|
||||
protected func: MoveConditionFunc;
|
||||
|
||||
@ -7239,8 +7329,8 @@ export function initMoves() {
|
||||
new AttackMove(Moves.GUILLOTINE, Type.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
|
||||
.attr(OneHitKOAttr)
|
||||
.attr(OneHitKOAccuracyAttr),
|
||||
new AttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1)
|
||||
.attr(ChargeAttr, ChargeAnim.RAZOR_WIND_CHARGING, i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" }))
|
||||
new ChargingAttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1)
|
||||
.chargeText(i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" }))
|
||||
.attr(HighCritAttr)
|
||||
.windMove()
|
||||
.ignoresVirtual()
|
||||
@ -7260,8 +7350,9 @@ export function initMoves() {
|
||||
.hidesTarget()
|
||||
.windMove()
|
||||
.partial(), // Should force random switches
|
||||
new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
|
||||
.attr(ChargeAttr, ChargeAnim.FLY_CHARGING, i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }), BattlerTagType.FLYING)
|
||||
new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
|
||||
.chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
||||
.condition(failOnGravityCondition)
|
||||
.ignoresVirtual(),
|
||||
new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
|
||||
@ -7410,8 +7501,9 @@ export function initMoves() {
|
||||
.makesContact(false)
|
||||
.slicingMove()
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1)
|
||||
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BEAM_CHARGING, i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" }))
|
||||
new ChargingAttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1)
|
||||
.chargeText(i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
|
||||
.attr(AntiSunlightPowerDecreaseAttr)
|
||||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1)
|
||||
@ -7460,8 +7552,9 @@ export function initMoves() {
|
||||
.attr(OneHitKOAccuracyAttr)
|
||||
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND)
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
|
||||
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" }), BattlerTagType.UNDERGROUND)
|
||||
new ChargingAttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
|
||||
.chargeText(i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND)
|
||||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.TOXIC)
|
||||
@ -7557,9 +7650,9 @@ export function initMoves() {
|
||||
.attr(TrapAttr, BattlerTagType.CLAMP),
|
||||
new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1)
|
||||
.attr(ChargeAttr, ChargeAnim.SKULL_BASH_CHARGING, i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" }), null, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true)
|
||||
new ChargingAttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1)
|
||||
.chargeText(i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(StatStageChangeAttr, [ Stat.DEF ], 1, true)
|
||||
.ignoresVirtual(),
|
||||
new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1)
|
||||
.attr(MultiHitAttr)
|
||||
@ -7596,8 +7689,8 @@ export function initMoves() {
|
||||
.triageMove(),
|
||||
new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.SLEEP),
|
||||
new AttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1)
|
||||
.attr(ChargeAttr, ChargeAnim.SKY_ATTACK_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
|
||||
new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1)
|
||||
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
|
||||
.attr(HighCritAttr)
|
||||
.attr(FlinchAttr)
|
||||
.makesContact(false)
|
||||
@ -8062,9 +8155,10 @@ export function initMoves() {
|
||||
new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3)
|
||||
.makesContact(false)
|
||||
.attr(SecretPowerAttr),
|
||||
new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3)
|
||||
.attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true)
|
||||
.attr(GulpMissileTagAttr)
|
||||
new ChargingAttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3)
|
||||
.chargeText(i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERWATER)
|
||||
.chargeAttr(GulpMissileTagAttr)
|
||||
.ignoresVirtual(),
|
||||
new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3)
|
||||
.attr(MultiHitAttr),
|
||||
@ -8197,8 +8291,9 @@ export function initMoves() {
|
||||
.attr(RechargeAttr),
|
||||
new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true),
|
||||
new AttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3)
|
||||
.attr(ChargeAttr, ChargeAnim.BOUNCE_CHARGING, i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }), BattlerTagType.FLYING)
|
||||
new ChargingAttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3)
|
||||
.chargeText(i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.condition(failOnGravityCondition)
|
||||
.ignoresVirtual(),
|
||||
@ -8553,8 +8648,9 @@ export function initMoves() {
|
||||
new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
||||
.windMove(),
|
||||
new AttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4)
|
||||
.attr(ChargeAttr, ChargeAnim.SHADOW_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN)
|
||||
new ChargingAttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4)
|
||||
.chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
|
||||
.ignoresProtect()
|
||||
.ignoresVirtual(),
|
||||
new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5)
|
||||
@ -8677,12 +8773,13 @@ export function initMoves() {
|
||||
.attr(
|
||||
MovePowerMultiplierAttr,
|
||||
(user, target, move) => target.status || target.hasAbility(Abilities.COMATOSE) ? 2 : 1),
|
||||
new AttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
||||
.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
|
||||
new ChargingAttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
||||
.chargeText(i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }))
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
||||
.condition(failOnGravityCondition)
|
||||
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
|
||||
.ignoresVirtual(),
|
||||
.ignoresVirtual()
|
||||
.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/
|
||||
new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
|
||||
@ -8832,12 +8929,12 @@ export function initMoves() {
|
||||
new AttackMove(Moves.FIERY_DANCE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 50, 0, 5)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
|
||||
.danceMove(),
|
||||
new AttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5)
|
||||
.attr(ChargeAttr, ChargeAnim.FREEZE_SHOCK_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" }))
|
||||
new ChargingAttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5)
|
||||
.chargeText(i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" }))
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5)
|
||||
.attr(ChargeAttr, ChargeAnim.ICE_BURN_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" }))
|
||||
new ChargingAttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5)
|
||||
.chargeText(i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" }))
|
||||
.attr(StatusEffectAttr, StatusEffect.BURN)
|
||||
.ignoresVirtual(),
|
||||
new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
|
||||
@ -8879,8 +8976,9 @@ export function initMoves() {
|
||||
.target(MoveTarget.ENEMY_SIDE),
|
||||
new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6)
|
||||
.attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ),
|
||||
new AttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||
.attr(ChargeAttr, ChargeAnim.PHANTOM_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN)
|
||||
new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||
.chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
|
||||
.ignoresProtect()
|
||||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6)
|
||||
@ -8990,8 +9088,8 @@ export function initMoves() {
|
||||
.ignoresSubstitute()
|
||||
.powderMove()
|
||||
.unimplemented(),
|
||||
new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
|
||||
.attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" }))
|
||||
new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
|
||||
.chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" }))
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true)
|
||||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
|
||||
@ -9200,8 +9298,9 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -1)
|
||||
.condition((user, target, move) => target.getStatStage(Stat.ATK) > -6)
|
||||
.triageMove(),
|
||||
new AttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7)
|
||||
.attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
|
||||
new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7)
|
||||
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
|
||||
.attr(AntiSunlightPowerDecreaseAttr)
|
||||
.slicingMove(),
|
||||
new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7)
|
||||
@ -9627,9 +9726,9 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
|
||||
.attr(MultiHitAttr)
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8)
|
||||
.attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }), null, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
|
||||
new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8)
|
||||
.chargeText(i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
|
||||
.ignoresVirtual(),
|
||||
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
|
||||
.attr(ShellSideArmCategoryAttr)
|
||||
@ -10081,8 +10180,10 @@ export function initMoves() {
|
||||
.attr(IvyCudgelTypeAttr)
|
||||
.attr(HighCritAttr)
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9)
|
||||
.attr(ElectroShotChargeAttr)
|
||||
new ChargingAttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9)
|
||||
.chargeText(i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
|
||||
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ])
|
||||
.ignoresVirtual(),
|
||||
new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
|
||||
.attr(TeraMoveCategoryAttr)
|
||||
|
@ -172,7 +172,8 @@ export const DarkDealEncounter: MysteryEncounter =
|
||||
isBoss: true,
|
||||
modifierConfigs: bossModifiers.map(m => {
|
||||
return {
|
||||
modifier: m
|
||||
modifier: m,
|
||||
stackCount: m.getStackCount(),
|
||||
};
|
||||
})
|
||||
};
|
||||
|
@ -2580,11 +2580,11 @@ export function initSpecies() {
|
||||
new PokemonSpecies(Species.VAROOM, 9, false, false, false, "Single-Cyl Pokémon", Type.STEEL, Type.POISON, 1, 35, Abilities.OVERCOAT, Abilities.NONE, Abilities.SLOW_START, 300, 45, 70, 63, 30, 45, 47, 190, 50, 60, GrowthRate.MEDIUM_FAST, 50, false),
|
||||
new PokemonSpecies(Species.REVAVROOM, 9, false, false, false, "Multi-Cyl Pokémon", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, false,
|
||||
new PokemonForm("Normal", "", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, false, null, true),
|
||||
new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
|
||||
new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
|
||||
new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
|
||||
new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
|
||||
new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175),
|
||||
new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
|
||||
new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
|
||||
new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
|
||||
new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
|
||||
new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175),
|
||||
),
|
||||
new PokemonSpecies(Species.CYCLIZAR, 9, false, false, false, "Mount Pokémon", Type.DRAGON, Type.NORMAL, 1.6, 63, Abilities.SHED_SKIN, Abilities.NONE, Abilities.REGENERATOR, 501, 70, 95, 65, 85, 65, 121, 190, 50, 175, GrowthRate.MEDIUM_SLOW, 50, false),
|
||||
new PokemonSpecies(Species.ORTHWORM, 9, false, false, false, "Earthworm Pokémon", Type.STEEL, null, 2.5, 310, Abilities.EARTH_EATER, Abilities.NONE, Abilities.SAND_VEIL, 480, 70, 85, 145, 60, 55, 65, 25, 50, 240, GrowthRate.SLOW, 50, false),
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as Utils from "../utils";
|
||||
import { randIntRange } from "#app/utils";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import i18next, { ParseKeys } from "i18next";
|
||||
|
||||
@ -6,17 +6,21 @@ export { StatusEffect };
|
||||
|
||||
export class Status {
|
||||
public effect: StatusEffect;
|
||||
public turnCount: integer;
|
||||
public cureTurn: integer | null;
|
||||
/** Toxic damage is `1/16 max HP * toxicTurnCount` */
|
||||
public toxicTurnCount: number = 0;
|
||||
public sleepTurnsRemaining?: number;
|
||||
|
||||
constructor(effect: StatusEffect, turnCount: integer = 0, cureTurn?: integer) {
|
||||
constructor(effect: StatusEffect, toxicTurnCount: number = 0, sleepTurnsRemaining?: number) {
|
||||
this.effect = effect;
|
||||
this.turnCount = turnCount === undefined ? 0 : turnCount;
|
||||
this.cureTurn = cureTurn!; // TODO: is this bang correct?
|
||||
this.toxicTurnCount = toxicTurnCount;
|
||||
this.sleepTurnsRemaining = sleepTurnsRemaining;
|
||||
}
|
||||
|
||||
incrementTurn(): void {
|
||||
this.turnCount++;
|
||||
this.toxicTurnCount++;
|
||||
if (this.sleepTurnsRemaining) {
|
||||
this.sleepTurnsRemaining--;
|
||||
}
|
||||
}
|
||||
|
||||
isPostTurn(): boolean {
|
||||
@ -107,7 +111,7 @@ export function getStatusEffectCatchRateMultiplier(statusEffect: StatusEffect):
|
||||
* Returns a random non-volatile StatusEffect
|
||||
*/
|
||||
export function generateRandomStatusEffect(): StatusEffect {
|
||||
return Utils.randIntRange(1, 6);
|
||||
return randIntRange(1, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,7 +127,7 @@ export function getRandomStatusEffect(statusEffectA: StatusEffect, statusEffectB
|
||||
return statusEffectA;
|
||||
}
|
||||
|
||||
return Utils.randIntRange(0, 2) ? statusEffectA : statusEffectB;
|
||||
return randIntRange(0, 2) ? statusEffectA : statusEffectB;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,7 +144,7 @@ export function getRandomStatus(statusA: Status | null, statusB: Status | null):
|
||||
}
|
||||
|
||||
|
||||
return Utils.randIntRange(0, 2) ? statusA : statusB;
|
||||
return randIntRange(0, 2) ? statusA : statusB;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene";
|
||||
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
||||
import { variantData } from "#app/data/variant";
|
||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move";
|
||||
import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
|
||||
import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
|
||||
import { starterPassiveAbilities } from "#app/data/balance/passives";
|
||||
@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
|
||||
import { WeatherType } from "#app/data/weather";
|
||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/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, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr } from "#app/data/ability";
|
||||
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, 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, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr } from "#app/data/ability";
|
||||
import PokemonData from "#app/system/pokemon-data";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
@ -2314,7 +2314,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
// Trainers get a weight bump to stat buffing moves
|
||||
movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1) ]);
|
||||
// Trainers get a weight decrease to multiturn moves
|
||||
movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]);
|
||||
movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]);
|
||||
}
|
||||
|
||||
// Weight towards higher power moves, by reducing the power of moves below the highest power.
|
||||
@ -3623,7 +3623,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return true;
|
||||
}
|
||||
|
||||
trySetStatus(effect: StatusEffect | undefined, asPhase: boolean = false, sourcePokemon: Pokemon | null = null, cureTurn: integer | null = 0, sourceText: string | null = null): boolean {
|
||||
trySetStatus(effect?: StatusEffect, asPhase: boolean = false, sourcePokemon: Pokemon | null = null, turnsRemaining: number = 0, sourceText: string | null = null): boolean {
|
||||
if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) {
|
||||
return false;
|
||||
}
|
||||
@ -3637,15 +3637,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
if (asPhase) {
|
||||
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon));
|
||||
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, turnsRemaining, sourceText, sourcePokemon));
|
||||
return true;
|
||||
}
|
||||
|
||||
let statusCureTurn: Utils.IntegerHolder;
|
||||
let sleepTurnsRemaining: Utils.NumberHolder;
|
||||
|
||||
if (effect === StatusEffect.SLEEP) {
|
||||
statusCureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4));
|
||||
applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, false, effect, statusCureTurn);
|
||||
sleepTurnsRemaining = new Utils.NumberHolder(this.randSeedIntRange(2, 4));
|
||||
|
||||
this.setFrameRate(4);
|
||||
|
||||
@ -3665,9 +3664,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
}
|
||||
|
||||
statusCureTurn = statusCureTurn!; // tell TS compiler it's defined
|
||||
sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined
|
||||
effect = effect!; // If `effect` is undefined then `trySetStatus()` will have already returned early via the `canSetStatus()` call
|
||||
this.status = new Status(effect, 0, statusCureTurn?.value);
|
||||
this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
|
||||
|
||||
if (effect !== StatusEffect.FAINT) {
|
||||
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true);
|
||||
@ -4197,7 +4196,7 @@ export class PlayerPokemon extends Pokemon {
|
||||
super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource);
|
||||
|
||||
if (Overrides.STATUS_OVERRIDE) {
|
||||
this.status = new Status(Overrides.STATUS_OVERRIDE);
|
||||
this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4);
|
||||
}
|
||||
|
||||
if (Overrides.SHINY_OVERRIDE) {
|
||||
@ -4677,7 +4676,7 @@ export class EnemyPokemon extends Pokemon {
|
||||
}
|
||||
|
||||
if (Overrides.OPP_STATUS_OVERRIDE) {
|
||||
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE);
|
||||
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE, 0, 4);
|
||||
}
|
||||
|
||||
if (Overrides.OPP_GENDER_OVERRIDE) {
|
||||
@ -4686,9 +4685,11 @@ export class EnemyPokemon extends Pokemon {
|
||||
|
||||
const speciesId = this.species.speciesId;
|
||||
|
||||
if (speciesId in Overrides.OPP_FORM_OVERRIDES
|
||||
if (
|
||||
speciesId in Overrides.OPP_FORM_OVERRIDES
|
||||
&& Overrides.OPP_FORM_OVERRIDES[speciesId]
|
||||
&& this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]]) {
|
||||
&& this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]]
|
||||
) {
|
||||
this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId] ?? 0;
|
||||
}
|
||||
|
||||
|
@ -75,6 +75,8 @@ class DefaultOverrides {
|
||||
readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = [];
|
||||
/** Set to `true` to show all tutorials */
|
||||
readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false;
|
||||
/** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */
|
||||
readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null;
|
||||
|
||||
// ----------------
|
||||
// PLAYER OVERRIDES
|
||||
|
@ -30,6 +30,15 @@ export class CommandPhase extends FieldPhase {
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
const commandUiHandler = this.scene.ui.handlers[Mode.COMMAND];
|
||||
if (commandUiHandler) {
|
||||
if (this.scene.currentBattle.turn === 1 || commandUiHandler.getCursor() === Command.POKEMON) {
|
||||
commandUiHandler.setCursor(Command.FIGHT);
|
||||
} else {
|
||||
commandUiHandler.setCursor(commandUiHandler.getCursor());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.fieldIndex) {
|
||||
// If we somehow are attempting to check the right pokemon but there's only one pokemon out
|
||||
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching
|
||||
|
84
src/phases/move-charge-phase.ts
Normal file
84
src/phases/move-charge-phase.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { MoveChargeAnim } from "#app/data/battle-anims";
|
||||
import { applyMoveChargeAttrs, MoveEffectAttr, InstantChargeAttr } from "#app/data/move";
|
||||
import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon";
|
||||
import { BooleanHolder } from "#app/utils";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
|
||||
/**
|
||||
* Phase for the "charging turn" of two-turn moves (e.g. Dig).
|
||||
* @extends {@linkcode PokemonPhase}
|
||||
*/
|
||||
export class MoveChargePhase extends PokemonPhase {
|
||||
/** The move instance that this phase applies */
|
||||
public move: PokemonMove;
|
||||
/** The field index targeted by the move (Charging moves assume single target) */
|
||||
public targetIndex: BattlerIndex;
|
||||
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove) {
|
||||
super(scene, battlerIndex);
|
||||
this.move = move;
|
||||
this.targetIndex = targetIndex;
|
||||
}
|
||||
|
||||
public override start() {
|
||||
super.start();
|
||||
|
||||
const user = this.getUserPokemon();
|
||||
const target = this.getTargetPokemon();
|
||||
const move = this.move.getMove();
|
||||
|
||||
// If the target is somehow not defined, or the move is somehow not a ChargingMove,
|
||||
// immediately end this phase.
|
||||
if (!target || !(move.isChargingMove())) {
|
||||
console.warn("Invalid parameters for MoveChargePhase");
|
||||
return super.end();
|
||||
}
|
||||
|
||||
new MoveChargeAnim(move.chargeAnim, move.id, user).play(this.scene, false, () => {
|
||||
move.showChargeText(user, target);
|
||||
|
||||
applyMoveChargeAttrs(MoveEffectAttr, user, target, move).then(() => {
|
||||
user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id);
|
||||
this.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Checks the move's instant charge conditions, then ends this phase. */
|
||||
public override end() {
|
||||
const user = this.getUserPokemon();
|
||||
const move = this.move.getMove();
|
||||
|
||||
if (move.isChargingMove()) {
|
||||
const instantCharge = new BooleanHolder(false);
|
||||
|
||||
applyMoveChargeAttrs(InstantChargeAttr, user, null, move, instantCharge);
|
||||
|
||||
if (instantCharge.value) {
|
||||
// this MoveEndPhase will be duplicated by the queued MovePhase if not removed
|
||||
this.scene.tryRemovePhase((phase) => phase instanceof MoveEndPhase && phase.getPokemon() === user);
|
||||
// queue a new MovePhase for this move's attack phase
|
||||
this.scene.unshiftPhase(new MovePhase(this.scene, user, [ this.targetIndex ], this.move, false));
|
||||
} else {
|
||||
user.getMoveQueue().push({ move: move.id, targets: [ this.targetIndex ]});
|
||||
}
|
||||
|
||||
// Add this move's charging phase to the user's move history
|
||||
user.pushMoveHistory({ move: this.move.moveId, targets: [ this.targetIndex ], result: MoveResult.OTHER });
|
||||
}
|
||||
super.end();
|
||||
}
|
||||
|
||||
public getUserPokemon(): Pokemon {
|
||||
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
|
||||
}
|
||||
|
||||
public getTargetPokemon(): Pokemon | undefined {
|
||||
return this.scene.getField(true).find((p) => this.targetIndex === p.getBattlerIndex());
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
|
||||
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
||||
import { MoveAnim } from "#app/data/battle-anims";
|
||||
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
|
||||
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move";
|
||||
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move";
|
||||
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
@ -108,7 +108,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* (and not random target) and failed the hit check against its target (MISS), log the move
|
||||
* as FAILed or MISSed (depending on the conditions above) and end this phase.
|
||||
*/
|
||||
|
||||
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
|
||||
this.stopMultiHit();
|
||||
if (hasActiveTargets) {
|
||||
@ -241,15 +240,13 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
user, target, move).then(() => {
|
||||
// All other effects require the move to not have failed or have been cancelled to trigger
|
||||
if (hitResult !== HitResult.FAIL) {
|
||||
/** Are the move's effects tied to the first turn of a charge move? */
|
||||
const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget() ?? null, move));
|
||||
/**
|
||||
* If the invoked move's effects are meant to trigger during the move's "charge turn,"
|
||||
* ignore all effects after this point.
|
||||
* Otherwise, apply all self-targeted POST_APPLY effects.
|
||||
*/
|
||||
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => {
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => {
|
||||
// All effects past this point require the move to have hit the target
|
||||
if (hitResult !== HitResult.NO_EFFECT) {
|
||||
// Apply all non-self-targeted POST_APPLY effects
|
||||
@ -267,7 +264,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
}
|
||||
// If the move was not protected against, apply all HIT effects
|
||||
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
|
||||
Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
|
||||
&& (!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(() => {
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr } from "#app/data/ability";
|
||||
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability";
|
||||
import { CommonAnim } from "#app/data/battle-anims";
|
||||
import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags";
|
||||
import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, ChargeAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move";
|
||||
import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move";
|
||||
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms";
|
||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
|
||||
import { Type } from "#app/data/type";
|
||||
import { getTerrainBlockMessage } from "#app/data/weather";
|
||||
import { MoveUsedEvent } from "#app/events/battle-scene";
|
||||
import Pokemon, { MoveResult, PokemonMove, TurnMove } from "#app/field/pokemon";
|
||||
import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import Overrides from "#app/overrides";
|
||||
import { BattlePhase } from "#app/phases/battle-phase";
|
||||
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
@ -22,6 +23,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import i18next from "i18next";
|
||||
import { MoveChargePhase } from "#app/phases/move-charge-phase";
|
||||
|
||||
export class MovePhase extends BattlePhase {
|
||||
protected _pokemon: Pokemon;
|
||||
@ -134,6 +136,8 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
if (this.cancelled || this.failed) {
|
||||
this.handlePreMoveFailures();
|
||||
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
|
||||
this.chargeMove();
|
||||
} else {
|
||||
this.useMove();
|
||||
}
|
||||
@ -168,25 +172,31 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
switch (this.pokemon.status.effect) {
|
||||
case StatusEffect.PARALYSIS:
|
||||
if (!this.pokemon.randSeedInt(4)) {
|
||||
activated = true;
|
||||
this.cancelled = true;
|
||||
}
|
||||
activated = (!this.pokemon.randSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
|
||||
break;
|
||||
case StatusEffect.SLEEP:
|
||||
applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove());
|
||||
healed = this.pokemon.status.turnCount === this.pokemon.status.cureTurn;
|
||||
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
|
||||
applyAbAttrs(ReduceStatusEffectDurationAbAttr, this.pokemon, null, false, this.pokemon.status.effect, turnsRemaining);
|
||||
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
|
||||
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
|
||||
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
|
||||
this.cancelled = activated;
|
||||
break;
|
||||
case StatusEffect.FREEZE:
|
||||
healed = !!this.move.getMove().findAttr(attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE)) || !this.pokemon.randSeedInt(5);
|
||||
healed =
|
||||
!!this.move.getMove().findAttr((attr) =>
|
||||
attr instanceof HealStatusEffectAttr
|
||||
&& attr.selfTarget
|
||||
&& attr.isOfEffect(StatusEffect.FREEZE))
|
||||
|| (!this.pokemon.randSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true)
|
||||
|| Overrides.STATUS_ACTIVATION_OVERRIDE === false;
|
||||
|
||||
activated = !healed;
|
||||
this.cancelled = activated;
|
||||
break;
|
||||
}
|
||||
|
||||
if (activated) {
|
||||
this.cancel();
|
||||
this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)));
|
||||
this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1)));
|
||||
} else if (healed) {
|
||||
@ -219,12 +229,15 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
this.showMoveText();
|
||||
|
||||
// TODO: Clean up implementation of two-turn moves.
|
||||
if (moveQueue.length > 0) {
|
||||
// Using .shift here clears out two turn moves once they've been used
|
||||
this.ignorePp = moveQueue.shift()?.ignorePP ?? false;
|
||||
}
|
||||
|
||||
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
||||
this.pokemon.lapseTag(BattlerTagType.CHARGING);
|
||||
}
|
||||
|
||||
// "commit" to using the move, deducting PP.
|
||||
if (!this.ignorePp) {
|
||||
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
|
||||
@ -288,6 +301,9 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
this.showFailedText(failedText);
|
||||
|
||||
// Remove the user from its semi-invulnerable state (if applicable)
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
}
|
||||
|
||||
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
|
||||
@ -299,6 +315,35 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
}
|
||||
|
||||
/** Queues a {@linkcode MoveChargePhase} for this phase's invoked move. */
|
||||
protected chargeMove() {
|
||||
const move = this.move.getMove();
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
|
||||
if (move.applyConditions(this.pokemon, targets[0], move)) {
|
||||
// Protean and Libero apply on the charging turn of charge moves
|
||||
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
|
||||
|
||||
this.showMoveText();
|
||||
this.scene.unshiftPhase(new MoveChargePhase(this.scene, this.pokemon.getBattlerIndex(), this.targets[0], this.move));
|
||||
} else {
|
||||
this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual });
|
||||
|
||||
let failedText: string | undefined;
|
||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false));
|
||||
|
||||
if (failureMessage) {
|
||||
failedText = failureMessage;
|
||||
}
|
||||
|
||||
this.showMoveText();
|
||||
this.showFailedText(failedText);
|
||||
|
||||
// Remove the user from its semi-invulnerable state (if applicable)
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a {@linkcode MoveEndPhase} if the move wasn't a {@linkcode followUp} and {@linkcode canMove()} returns `true`,
|
||||
* then ends the phase.
|
||||
@ -412,8 +457,6 @@ export class MovePhase extends BattlePhase {
|
||||
* - Lapses `AFTER_MOVE` tags:
|
||||
* - This handles the effects of {@link Moves.SUBSTITUTE Substitute}
|
||||
* - Removes the second turn of charge moves
|
||||
*
|
||||
* TODO: handle charge moves more gracefully
|
||||
*/
|
||||
protected handlePreMoveFailures(): void {
|
||||
if (this.cancelled || this.failed) {
|
||||
@ -445,18 +488,7 @@ export class MovePhase extends BattlePhase {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.move.getMove().hasAttr(ChargeAttr)) {
|
||||
const lastMove = this.pokemon.getLastXMoves() as TurnMove[];
|
||||
if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) {
|
||||
this.scene.queueMessage(i18next.t("battle:useMove", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
|
||||
moveName: this.move.getName()
|
||||
}), 500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.pokemon.getTag(BattlerTagType.RECHARGING || BattlerTagType.INTERRUPTED)) {
|
||||
if (this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.INTERRUPTED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -8,26 +8,26 @@ import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
|
||||
export class ObtainStatusEffectPhase extends PokemonPhase {
|
||||
private statusEffect?: StatusEffect | undefined;
|
||||
private cureTurn?: integer | null;
|
||||
private statusEffect?: StatusEffect;
|
||||
private turnsRemaining?: number;
|
||||
private sourceText?: string | null;
|
||||
private sourcePokemon?: Pokemon | null;
|
||||
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string | null, sourcePokemon?: Pokemon | null) {
|
||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, turnsRemaining?: number, sourceText?: string | null, sourcePokemon?: Pokemon | null) {
|
||||
super(scene, battlerIndex);
|
||||
|
||||
this.statusEffect = statusEffect;
|
||||
this.cureTurn = cureTurn;
|
||||
this.turnsRemaining = turnsRemaining;
|
||||
this.sourceText = sourceText;
|
||||
this.sourcePokemon = sourcePokemon; // For tracking which Pokemon caused the status effect
|
||||
this.sourcePokemon = sourcePokemon;
|
||||
}
|
||||
|
||||
start() {
|
||||
const pokemon = this.getPokemon();
|
||||
if (pokemon && !pokemon.status) {
|
||||
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
|
||||
if (this.cureTurn) {
|
||||
pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct?
|
||||
if (this.turnsRemaining) {
|
||||
pokemon.status!.sleepTurnsRemaining = this.turnsRemaining;
|
||||
}
|
||||
pokemon.updateInfo(true);
|
||||
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, false, () => {
|
||||
|
@ -18,7 +18,7 @@ export class PostSummonPhase extends PokemonPhase {
|
||||
const pokemon = this.getPokemon();
|
||||
|
||||
if (pokemon.status?.effect === StatusEffect.TOXIC) {
|
||||
pokemon.status.turnCount = 0;
|
||||
pokemon.status.toxicTurnCount = 0;
|
||||
}
|
||||
this.scene.arena.applyTags(ArenaTrapTag, false, pokemon);
|
||||
|
||||
|
@ -30,7 +30,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
|
||||
damage.value = Math.max(pokemon.getMaxHp() >> 3, 1);
|
||||
break;
|
||||
case StatusEffect.TOXIC:
|
||||
damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.turnCount), 1);
|
||||
damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.toxicTurnCount), 1);
|
||||
break;
|
||||
case StatusEffect.BURN:
|
||||
damage.value = Math.max(pokemon.getMaxHp() >> 4, 1);
|
||||
|
@ -65,8 +65,9 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
|
||||
const pokemon = this.getPokemon();
|
||||
|
||||
if (this.switchType === SwitchType.SWITCH) {
|
||||
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
|
||||
|
||||
if (this.switchType === SwitchType.SWITCH) {
|
||||
const substitute = pokemon.getTag(SubstituteTag);
|
||||
if (substitute) {
|
||||
this.scene.tweens.add({
|
||||
|
@ -137,7 +137,7 @@ export default class PokemonData {
|
||||
this.moveset = (source.moveset || [ new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL) ]).filter(m => m).map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp));
|
||||
if (!forHistory) {
|
||||
this.status = source.status
|
||||
? new Status(source.status.effect, source.status.turnCount, source.status.cureTurn)
|
||||
? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
|
||||
: null;
|
||||
}
|
||||
|
||||
|
93
src/test/abilities/early_bird.test.ts
Normal file
93
src/test/abilities/early_bird.test.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Status } from "#app/data/status-effect";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Early Bird", () => {
|
||||
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
|
||||
.moveset([ Moves.REST, Moves.BELLY_DRUM, Moves.SPLASH ])
|
||||
.ability(Abilities.EARLY_BIRD)
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("reduces Rest's sleep time to 1 turn", async () => {
|
||||
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.BELLY_DRUM);
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.REST);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status?.effect).toBeUndefined();
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("reduces 3-turn sleep to 1 turn", async () => {
|
||||
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
player.status = new Status(StatusEffect.SLEEP, 0, 4);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status?.effect).toBeUndefined();
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("reduces 1-turn sleep to 0 turns", async () => {
|
||||
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
player.status = new Status(StatusEffect.SLEEP, 0, 2);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status?.effect).toBeUndefined();
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
});
|
@ -36,9 +36,7 @@ describe("Abilities - Imposter", () => {
|
||||
});
|
||||
|
||||
it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
|
||||
await game.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
await game.classicMode.startBattle([ Species.DITTO ]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
@ -78,9 +76,7 @@ describe("Abilities - Imposter", () => {
|
||||
it("should copy in-battle overridden stats", async () => {
|
||||
game.override.enemyMoveset([ Moves.POWER_SPLIT ]);
|
||||
|
||||
await game.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
await game.classicMode.startBattle([ Species.DITTO ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
@ -97,4 +93,18 @@ describe("Abilities - Imposter", () => {
|
||||
expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
|
||||
expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
|
||||
});
|
||||
|
||||
it("should set each move's pp to a maximum of 5", async () => {
|
||||
game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]);
|
||||
|
||||
await game.classicMode.startBattle([ Species.DITTO ]);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.TACKLE);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
player.getMoveset().forEach(move => {
|
||||
expect(move!.getMovePp()).toBeLessThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -150,7 +150,7 @@ describe("Abilities - Magic Guard", () => {
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
const toxicStartCounter = enemyPokemon.status!.turnCount;
|
||||
const toxicStartCounter = enemyPokemon.status!.toxicTurnCount;
|
||||
//should be 0
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
@ -162,7 +162,7 @@ describe("Abilities - Magic Guard", () => {
|
||||
* - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5
|
||||
*/
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(enemyPokemon.status!.turnCount).toBeGreaterThan(toxicStartCounter);
|
||||
expect(enemyPokemon.status!.toxicTurnCount).toBeGreaterThan(toxicStartCounter);
|
||||
expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5);
|
||||
}
|
||||
);
|
||||
|
@ -52,6 +52,7 @@ describe("Abilities - Volt Absorb", () => {
|
||||
expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined();
|
||||
expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase");
|
||||
});
|
||||
|
||||
it("should activate regardless of accuracy checks", async () => {
|
||||
game.override.moveset(Moves.THUNDERBOLT);
|
||||
game.override.enemyMoveset(Moves.SPLASH);
|
||||
@ -71,6 +72,7 @@ describe("Abilities - Volt Absorb", () => {
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
});
|
||||
|
||||
it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => {
|
||||
game.override.moveset(Moves.THUNDERBOLT);
|
||||
game.override.enemyMoveset(Moves.DIVE);
|
||||
@ -84,9 +86,7 @@ describe("Abilities - Volt Absorb", () => {
|
||||
game.move.select(Moves.THUNDERBOLT);
|
||||
enemyPokemon.hp = enemyPokemon.hp - 1;
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
await game.move.forceMiss();
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { ArenaTagType } from "#app/enums/arena-tag-type";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
@ -31,7 +31,8 @@ describe("Arena - Gravity", () => {
|
||||
.ability(Abilities.UNNERVE)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemySpecies(Species.SHUCKLE)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyLevel(5);
|
||||
});
|
||||
|
||||
// Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move)
|
||||
@ -42,102 +43,121 @@ describe("Arena - Gravity", () => {
|
||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
||||
|
||||
// Setup Gravity on first turn
|
||||
await game.startBattle([ Species.PIKACHU ]);
|
||||
await game.classicMode.startBattle([ Species.PIKACHU ]);
|
||||
game.move.select(Moves.GRAVITY);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use non-OHKO move on second turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.TACKLE);
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100 * 1.67);
|
||||
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(100 * 1.67);
|
||||
});
|
||||
|
||||
it("OHKO move accuracy is not affected", async () => {
|
||||
game.override.startingLevel(5);
|
||||
game.override.enemyLevel(5);
|
||||
|
||||
/** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */
|
||||
const moveToCheck = allMoves[Moves.FISSURE];
|
||||
|
||||
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
|
||||
|
||||
// Setup Gravity on first turn
|
||||
await game.startBattle([ Species.PIKACHU ]);
|
||||
await game.classicMode.startBattle([ Species.PIKACHU ]);
|
||||
game.move.select(Moves.GRAVITY);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use OHKO move on second turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.FISSURE);
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(30);
|
||||
expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30);
|
||||
});
|
||||
|
||||
describe("Against flying types", () => {
|
||||
it("can be hit by ground-type moves now", async () => {
|
||||
game.override
|
||||
.startingLevel(5)
|
||||
.enemyLevel(5)
|
||||
.enemySpecies(Species.PIDGEOT)
|
||||
.moveset([ Moves.GRAVITY, Moves.EARTHQUAKE ]);
|
||||
|
||||
await game.startBattle([ Species.PIKACHU ]);
|
||||
await game.classicMode.startBattle([ Species.PIKACHU ]);
|
||||
|
||||
const pidgeot = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
||||
|
||||
// Try earthquake on 1st turn (fails!);
|
||||
game.move.select(Moves.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(0);
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(0);
|
||||
|
||||
// Setup Gravity on 2nd turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.GRAVITY);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use ground move on 3rd turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.EARTHQUAKE);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(1);
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(1);
|
||||
});
|
||||
|
||||
it("keeps super-effective moves super-effective after using gravity", async () => {
|
||||
game.override
|
||||
.startingLevel(5)
|
||||
.enemyLevel(5)
|
||||
.enemySpecies(Species.PIDGEOT)
|
||||
.moveset([ Moves.GRAVITY, Moves.THUNDERBOLT ]);
|
||||
|
||||
await game.startBattle([ Species.PIKACHU ]);
|
||||
await game.classicMode.startBattle([ Species.PIKACHU ]);
|
||||
|
||||
const pidgeot = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(pidgeot, "getAttackTypeEffectiveness");
|
||||
|
||||
// Setup Gravity on 1st turn
|
||||
game.move.select(Moves.GRAVITY);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined();
|
||||
|
||||
// Use electric move on 2nd turn
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.THUNDERBOLT);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(2);
|
||||
expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels Fly if its user is semi-invulnerable", async () => {
|
||||
game.override
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyMoveset(Moves.FLY)
|
||||
.moveset([ Moves.GRAVITY, Moves.SPLASH ]);
|
||||
|
||||
await game.classicMode.startBattle([ Species.CHARIZARD ]);
|
||||
|
||||
const charizard = game.scene.getPlayerPokemon()!;
|
||||
const snorlax = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.toNextTurn();
|
||||
expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.GRAVITY);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined();
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(charizard.hp).toBe(charizard.getMaxHp());
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
Status,
|
||||
StatusEffect,
|
||||
getStatusEffectActivationText,
|
||||
getStatusEffectDescriptor,
|
||||
@ -6,14 +7,19 @@ import {
|
||||
getStatusEffectObtainText,
|
||||
getStatusEffectOverlapText,
|
||||
} from "#app/data/status-effect";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { mockI18next } from "#test/utils/testUtils";
|
||||
import i18next from "i18next";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const pokemonName = "PKM";
|
||||
const sourceText = "SOURCE";
|
||||
|
||||
describe("status-effect", () => {
|
||||
describe("Status Effect Messages", () => {
|
||||
beforeAll(() => {
|
||||
i18next.init();
|
||||
});
|
||||
@ -299,3 +305,99 @@ describe("status-effect", () => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Status Effects", () => {
|
||||
describe("Paralysis", () => {
|
||||
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
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([ Moves.QUICK_ATTACK ])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.statusEffect(StatusEffect.PARALYSIS);
|
||||
});
|
||||
|
||||
it("causes the pokemon's move to fail when activated", async () => {
|
||||
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||
|
||||
game.move.select(Moves.QUICK_ATTACK);
|
||||
await game.move.forceStatusActivation(true);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(game.scene.getEnemyPokemon()!.isFullHp()).toBe(true);
|
||||
expect(game.scene.getPlayerPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sleep", () => {
|
||||
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
|
||||
.moveset([ Moves.SPLASH ])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("should last the appropriate number of turns", async () => {
|
||||
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
player.status = new Status(StatusEffect.SLEEP, 0, 4);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status.effect).toBe(StatusEffect.SLEEP);
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status?.effect).toBeUndefined();
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -7,8 +7,6 @@ import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const TIMEOUT = 20 * 1000;
|
||||
|
||||
describe("Items - Toxic orb", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
@ -27,10 +25,10 @@ describe("Items - Toxic orb", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleType("single")
|
||||
.enemySpecies(Species.RATTATA)
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([ Moves.SPLASH ])
|
||||
.moveset(Moves.SPLASH)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.startingHeldItems([{
|
||||
name: "TOXIC_ORB",
|
||||
@ -39,22 +37,19 @@ describe("Items - Toxic orb", () => {
|
||||
vi.spyOn(i18next, "t");
|
||||
});
|
||||
|
||||
it("badly poisons the holder", async () => {
|
||||
await game.classicMode.startBattle([ Species.MIGHTYENA ]);
|
||||
it("should badly poison the holder", async () => {
|
||||
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||
|
||||
const player = game.scene.getPlayerField()[0];
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
expect(player.getHeldItems()[0].type.id).toBe("TOXIC_ORB");
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
// Toxic orb should trigger here
|
||||
await game.phaseInterceptor.run("MessagePhase");
|
||||
await game.phaseInterceptor.to("MessagePhase");
|
||||
expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything());
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.status?.effect).toBe(StatusEffect.TOXIC);
|
||||
// Damage should not have ticked yet.
|
||||
expect(player.status?.turnCount).toBe(0);
|
||||
}, TIMEOUT);
|
||||
expect(player.status?.toxicTurnCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
@ -106,4 +106,28 @@ describe("Moves - Baton Pass", () => {
|
||||
|
||||
expect(player2.findTag((t) => t.tagType === BattlerTagType.SALT_CURED)).toBeUndefined();
|
||||
}, 20000);
|
||||
|
||||
it("doesn't allow binding effects from the user to persist", async () => {
|
||||
game.override.moveset([ Moves.FIRE_SPIN, Moves.BATON_PASS ]);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.FIRE_SPIN);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.move.forceHit();
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined();
|
||||
|
||||
game.move.select(Moves.BATON_PASS);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
114
src/test/moves/dig.test.ts
Normal file
114
src/test/moves/dig.test.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
|
||||
describe("Moves - Dig", () => {
|
||||
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
|
||||
.moveset(Moves.DIG)
|
||||
.battleType("single")
|
||||
.startingLevel(100)
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.TACKLE);
|
||||
});
|
||||
|
||||
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIG);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeDefined();
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIG);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
|
||||
const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG);
|
||||
expect(playerDig?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
|
||||
game.override.enemyAbility(Abilities.NO_GUARD);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIG);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should not expend PP when the attack phase is cancelled", async () => {
|
||||
game.override
|
||||
.enemyAbility(Abilities.NO_GUARD)
|
||||
.enemyMoveset(Moves.SPORE);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIG);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined();
|
||||
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG);
|
||||
expect(playerDig?.ppUsed).toBe(0);
|
||||
});
|
||||
|
||||
it("should cause the user to take double damage from Earthquake", async () => {
|
||||
await game.classicMode.startBattle([ Species.DONDOZO ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
const preDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
|
||||
|
||||
game.move.select(Moves.DIG);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
const postDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage;
|
||||
// these hopefully get avoid rounding errors :shrug:
|
||||
expect(postDigEarthquakeDmg).toBeGreaterThanOrEqual(2 * preDigEarthquakeDmg);
|
||||
expect(postDigEarthquakeDmg).toBeLessThan(2 * (preDigEarthquakeDmg + 1));
|
||||
});
|
||||
});
|
137
src/test/moves/dive.test.ts
Normal file
137
src/test/moves/dive.test.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
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, it, expect } from "vitest";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
|
||||
describe("Moves - Dive", () => {
|
||||
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
|
||||
.moveset(Moves.DIVE)
|
||||
.battleType("single")
|
||||
.startingLevel(100)
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.TACKLE);
|
||||
});
|
||||
|
||||
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIVE);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeDefined();
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIVE);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
|
||||
const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE);
|
||||
expect(playerDive?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
|
||||
game.override.enemyAbility(Abilities.NO_GUARD);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIVE);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should not expend PP when the attack phase is cancelled", async () => {
|
||||
game.override
|
||||
.enemyAbility(Abilities.NO_GUARD)
|
||||
.enemyMoveset(Moves.SPORE);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIVE);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
|
||||
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE);
|
||||
expect(playerDive?.ppUsed).toBe(0);
|
||||
});
|
||||
|
||||
it("should trigger on-contact post-defend ability effects", async () => {
|
||||
game.override
|
||||
.enemyAbility(Abilities.ROUGH_SKIN)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIVE);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.battleData.abilitiesApplied[0]).toBe(Abilities.ROUGH_SKIN);
|
||||
});
|
||||
|
||||
it("should cancel attack after Harsh Sunlight is set", async () => {
|
||||
game.override.enemyMoveset(Moves.SPLASH);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIVE);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.phaseInterceptor.to("TurnStartPhase", false);
|
||||
game.scene.arena.trySetWeather(WeatherType.HARSH_SUN, false);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined();
|
||||
|
||||
const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE);
|
||||
expect(playerDive?.ppUsed).toBe(1);
|
||||
});
|
||||
});
|
104
src/test/moves/electro_shot.test.ts
Normal file
104
src/test/moves/electro_shot.test.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
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, it, expect } from "vitest";
|
||||
|
||||
describe("Moves - Electro Shot", () => {
|
||||
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
|
||||
.moveset(Moves.ELECTRO_SHOT)
|
||||
.battleType("single")
|
||||
.startingLevel(100)
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("should increase the user's Sp. Atk on the first turn, then attack on the second turn", async () => {
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.ELECTRO_SHOT);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined();
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER);
|
||||
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT);
|
||||
expect(playerElectroShot?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ weatherType: WeatherType.RAIN, name: "Rain" },
|
||||
{ weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" }
|
||||
])("should fully resolve in one turn if $name is active", async ({ weatherType }) => {
|
||||
game.override.weather(weatherType);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.ELECTRO_SHOT);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT);
|
||||
expect(playerElectroShot?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it("should only increase Sp. Atk once with Multi-Lens", async () => {
|
||||
game.override
|
||||
.weather(WeatherType.RAIN)
|
||||
.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.ELECTRO_SHOT);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(playerPokemon.turnData.hitCount).toBe(2);
|
||||
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
|
||||
});
|
||||
});
|
122
src/test/moves/fly.test.ts
Normal file
122
src/test/moves/fly.test.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
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, it, expect, vi } from "vitest";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves } from "#app/data/move";
|
||||
|
||||
describe("Moves - Fly", () => {
|
||||
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
|
||||
.moveset(Moves.FLY)
|
||||
.battleType("single")
|
||||
.startingLevel(100)
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.TACKLE);
|
||||
|
||||
vi.spyOn(allMoves[Moves.FLY], "accuracy", "get").mockReturnValue(100);
|
||||
});
|
||||
|
||||
it("should make the user semi-invulnerable, then attack over 2 turns", async () => {
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.FLY);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeDefined();
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.FLY);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
|
||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY);
|
||||
expect(playerFly?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it("should not allow the user to evade attacks from Pokemon with No Guard", async () => {
|
||||
game.override.enemyAbility(Abilities.NO_GUARD);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.FLY);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp());
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
});
|
||||
|
||||
it("should not expend PP when the attack phase is cancelled", async () => {
|
||||
game.override
|
||||
.enemyAbility(Abilities.NO_GUARD)
|
||||
.enemyMoveset(Moves.SPORE);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.FLY);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined();
|
||||
expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP);
|
||||
|
||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY);
|
||||
expect(playerFly?.ppUsed).toBe(0);
|
||||
});
|
||||
|
||||
it("should be cancelled when another Pokemon uses Gravity", async () => {
|
||||
game.override.enemyMoveset([ Moves.SPLASH, Moves.GRAVITY ]);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.FLY);
|
||||
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
|
||||
await game.toNextTurn();
|
||||
await game.forceEnemyMove(Moves.GRAVITY);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
|
||||
const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY);
|
||||
expect(playerFly?.ppUsed).toBe(0);
|
||||
});
|
||||
});
|
78
src/test/moves/geomancy.test.ts
Normal file
78
src/test/moves/geomancy.test.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { EffectiveStat, Stat } from "#enums/stat";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
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, it, expect } from "vitest";
|
||||
|
||||
describe("Moves - Geomancy", () => {
|
||||
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
|
||||
.moveset(Moves.GEOMANCY)
|
||||
.battleType("single")
|
||||
.startingLevel(100)
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("should boost the user's stats on the second turn of use", async () => {
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ];
|
||||
|
||||
game.move.select(Moves.GEOMANCY);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(0));
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2));
|
||||
expect(player.getMoveHistory()).toHaveLength(2);
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY);
|
||||
expect(playerGeomancy?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it("should execute over 2 turns between waves", async () => {
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ];
|
||||
|
||||
game.move.select(Moves.GEOMANCY);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
await game.doKillOpponents();
|
||||
|
||||
await game.toNextWave();
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2));
|
||||
expect(player.getMoveHistory()).toHaveLength(2);
|
||||
expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY);
|
||||
expect(playerGeomancy?.ppUsed).toBe(1);
|
||||
});
|
||||
});
|
@ -1,12 +1,10 @@
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||
import { StatusEffect } from "#app/data/status-effect";
|
||||
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";
|
||||
import { StatusEffect } from "#app/data/status-effect";
|
||||
|
||||
describe("Moves - Nightmare", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -39,16 +37,16 @@ describe("Moves - Nightmare", () => {
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const enemyMaxHP = enemyPokemon.hp;
|
||||
|
||||
game.move.select(Moves.NIGHTMARE);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4));
|
||||
|
||||
// take a second turn to make sure damage occurs again
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
expect(enemyPokemon.hp).toBe(enemyMaxHP - Math.floor(enemyMaxHP / 4) - Math.floor(enemyMaxHP / 4));
|
||||
});
|
||||
});
|
||||
|
102
src/test/moves/solar_beam.test.ts
Normal file
102
src/test/moves/solar_beam.test.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
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, it, expect, vi } from "vitest";
|
||||
|
||||
describe("Moves - Solar Beam", () => {
|
||||
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
|
||||
.moveset(Moves.SOLAR_BEAM)
|
||||
.battleType("single")
|
||||
.startingLevel(100)
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyLevel(100)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("should deal damage in two turns if no weather is active", async () => {
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.SOLAR_BEAM);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined();
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM);
|
||||
expect(playerSolarBeam?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ weatherType: WeatherType.SUNNY, name: "Sun" },
|
||||
{ weatherType: WeatherType.HARSH_SUN, name: "Harsh Sun" }
|
||||
])("should deal damage in one turn if $name is active", async ({ weatherType }) => {
|
||||
game.override.weather(weatherType);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.SOLAR_BEAM);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM);
|
||||
expect(playerSolarBeam?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ weatherType: WeatherType.RAIN, name: "Rain" },
|
||||
{ weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" }
|
||||
])("should have its power halved in $name", async ({ weatherType }) => {
|
||||
game.override.weather(weatherType);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const solarBeam = allMoves[Moves.SOLAR_BEAM];
|
||||
|
||||
vi.spyOn(solarBeam, "calculateBattlePower");
|
||||
|
||||
game.move.select(Moves.SOLAR_BEAM);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(solarBeam.calculateBattlePower).toHaveLastReturnedWith(60);
|
||||
});
|
||||
});
|
@ -36,9 +36,7 @@ describe("Moves - Transform", () => {
|
||||
});
|
||||
|
||||
it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => {
|
||||
await game.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
await game.classicMode.startBattle([ Species.DITTO ]);
|
||||
|
||||
game.move.select(Moves.TRANSFORM);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
@ -78,9 +76,7 @@ describe("Moves - Transform", () => {
|
||||
it("should copy in-battle overridden stats", async () => {
|
||||
game.override.enemyMoveset([ Moves.POWER_SPLIT ]);
|
||||
|
||||
await game.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
await game.classicMode.startBattle([ Species.DITTO ]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
@ -97,4 +93,18 @@ describe("Moves - Transform", () => {
|
||||
expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
|
||||
expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk);
|
||||
});
|
||||
|
||||
it ("should set each move's pp to a maximum of 5", async () => {
|
||||
game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]);
|
||||
|
||||
await game.classicMode.startBattle([ Species.DITTO ]);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.TRANSFORM);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
player.getMoveset().forEach(move => {
|
||||
expect(move!.getMovePp()).toBeLessThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -41,7 +41,8 @@ describe("Moves - Whirlwind", () => {
|
||||
const staraptor = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(move);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
|
||||
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||
|
53
src/test/moves/will_o_wisp.test.ts
Normal file
53
src/test/moves/will_o_wisp.test.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Will-O-Wisp", () => {
|
||||
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
|
||||
.moveset([ Moves.WILL_O_WISP, Moves.SPLASH ])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("should burn the opponent", async () => {
|
||||
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.WILL_O_WISP);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.move.forceHit();
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
|
||||
});
|
||||
});
|
@ -1,12 +1,13 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import Overrides from "#app/overrides";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { Command } from "#app/ui/command-ui-handler";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#test/utils/gameManagerUtils";
|
||||
import { GameManagerHelper } from "#test/utils/helpers/gameManagerHelper";
|
||||
import { vi } from "vitest";
|
||||
import { getMovePosition } from "../gameManagerUtils";
|
||||
import { GameManagerHelper } from "./gameManagerHelper";
|
||||
|
||||
/**
|
||||
* Helper to handle a Pokemon's move
|
||||
@ -17,7 +18,7 @@ export class MoveHelper extends GameManagerHelper {
|
||||
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`.
|
||||
* Used to force a move to hit.
|
||||
*/
|
||||
async forceHit(): Promise<void> {
|
||||
public async forceHit(): Promise<void> {
|
||||
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
||||
}
|
||||
@ -26,9 +27,9 @@ export class MoveHelper extends GameManagerHelper {
|
||||
* Intercepts {@linkcode MoveEffectPhase} and mocks the
|
||||
* {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`.
|
||||
* Used to force a move to miss.
|
||||
* @param firstTargetOnly Whether the move should force miss on the first target only, in the case of multi-target moves.
|
||||
* @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves.
|
||||
*/
|
||||
async forceMiss(firstTargetOnly: boolean = false): Promise<void> {
|
||||
public async forceMiss(firstTargetOnly: boolean = false): Promise<void> {
|
||||
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
const hitCheck = vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck");
|
||||
|
||||
@ -41,11 +42,11 @@ export class MoveHelper extends GameManagerHelper {
|
||||
|
||||
/**
|
||||
* Select the move to be used by the given Pokemon(-index). Triggers during the next {@linkcode CommandPhase}
|
||||
* @param move the move to use
|
||||
* @param pkmIndex the pokemon index. Relevant for double-battles only (defaults to 0)
|
||||
* @param targetIndex The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required
|
||||
* @param move - the move to use
|
||||
* @param pkmIndex - the pokemon index. Relevant for double-battles only (defaults to 0)
|
||||
* @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required
|
||||
*/
|
||||
select(move: Moves, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) {
|
||||
public select(move: Moves, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) {
|
||||
const movePosition = getMovePosition(this.game.scene, pkmIndex, move);
|
||||
|
||||
this.game.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
|
||||
@ -59,4 +60,15 @@ export class MoveHelper extends GameManagerHelper {
|
||||
this.game.selectTarget(movePosition, targetIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the Paralysis or Freeze status to activate on the next move by temporarily mocking {@linkcode Overrides.STATUS_ACTIVATION_OVERRIDE},
|
||||
* advancing to the next `MovePhase`, and then resetting the override to `null`
|
||||
* @param activated - `true` to force the status to activate, `false` to force the status to not activate (will cause Freeze to heal)
|
||||
*/
|
||||
public async forceStatusActivation(activated: boolean): Promise<void> {
|
||||
vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(activated);
|
||||
await this.game.phaseInterceptor.to("MovePhase");
|
||||
vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(null);
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line
|
||||
* @param biome the biome to set
|
||||
*/
|
||||
startingBiome(biome: Biome): this {
|
||||
public startingBiome(biome: Biome): this {
|
||||
this.game.scene.newArena(biome);
|
||||
this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`);
|
||||
return this;
|
||||
@ -38,9 +38,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the starting wave (index)
|
||||
* @param wave the wave (index) to set. Classic: `1`-`200`
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
startingWave(wave: number): this {
|
||||
public startingWave(wave: number): this {
|
||||
vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(wave);
|
||||
this.log(`Starting wave set to ${wave}!`);
|
||||
return this;
|
||||
@ -49,9 +49,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the player (pokemon) starting level
|
||||
* @param level the (pokemon) level to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
startingLevel(level: Species | number): this {
|
||||
public startingLevel(level: Species | number): this {
|
||||
vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(level);
|
||||
this.log(`Player Pokemon starting level set to ${level}!`);
|
||||
return this;
|
||||
@ -62,7 +62,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @param value the XP multiplier to set
|
||||
* @returns `this`
|
||||
*/
|
||||
xpMultiplier(value: number): this {
|
||||
public xpMultiplier(value: number): this {
|
||||
vi.spyOn(Overrides, "XP_MULTIPLIER_OVERRIDE", "get").mockReturnValue(value);
|
||||
this.log(`XP Multiplier set to ${value}!`);
|
||||
return this;
|
||||
@ -71,9 +71,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the player (pokemon) starting held items
|
||||
* @param items the items to hold
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
startingHeldItems(items: ModifierOverride[]) {
|
||||
public startingHeldItems(items: ModifierOverride[]): this {
|
||||
vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
|
||||
this.log("Player Pokemon starting held items set to:", items);
|
||||
return this;
|
||||
@ -82,9 +82,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the player (pokemon) {@linkcode Species | species}
|
||||
* @param species the (pokemon) {@linkcode Species | species} to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
starterSpecies(species: Species | number): this {
|
||||
public starterSpecies(species: Species | number): this {
|
||||
vi.spyOn(Overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||
this.log(`Player Pokemon species set to ${Species[species]} (=${species})!`);
|
||||
return this;
|
||||
@ -92,9 +92,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
|
||||
/**
|
||||
* Override the player (pokemon) to be a random fusion
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
enableStarterFusion(): this {
|
||||
public enableStarterFusion(): this {
|
||||
vi.spyOn(Overrides, "STARTER_FUSION_OVERRIDE", "get").mockReturnValue(true);
|
||||
this.log("Player Pokemon is a random fusion!");
|
||||
return this;
|
||||
@ -103,9 +103,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the player (pokemon) fusion species
|
||||
* @param species the fusion species to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
starterFusionSpecies(species: Species | number): this {
|
||||
public starterFusionSpecies(species: Species | number): this {
|
||||
vi.spyOn(Overrides, "STARTER_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||
this.log(`Player Pokemon fusion species set to ${Species[species]} (=${species})!`);
|
||||
return this;
|
||||
@ -114,9 +114,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the player (pokemons) forms
|
||||
* @param forms the (pokemon) forms to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
starterForms(forms: Partial<Record<Species, number>>): this {
|
||||
public starterForms(forms: Partial<Record<Species, number>>): this {
|
||||
vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue(forms);
|
||||
const formsStr = Object.entries(forms)
|
||||
.map(([ speciesId, formIndex ]) => `${Species[speciesId]}=${formIndex}`)
|
||||
@ -128,9 +128,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the player's starting modifiers
|
||||
* @param modifiers the modifiers to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
startingModifier(modifiers: ModifierOverride[]): this {
|
||||
public startingModifier(modifiers: ModifierOverride[]): this {
|
||||
vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers);
|
||||
this.log(`Player starting modifiers set to: ${modifiers}`);
|
||||
return this;
|
||||
@ -139,9 +139,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the player (pokemon) {@linkcode Abilities | ability}
|
||||
* @param ability the (pokemon) {@linkcode Abilities | ability} to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
ability(ability: Abilities): this {
|
||||
public ability(ability: Abilities): this {
|
||||
vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(ability);
|
||||
this.log(`Player Pokemon ability set to ${Abilities[ability]} (=${ability})!`);
|
||||
return this;
|
||||
@ -150,9 +150,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the player (pokemon) **passive** {@linkcode Abilities | ability}
|
||||
* @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
passiveAbility(passiveAbility: Abilities): this {
|
||||
public passiveAbility(passiveAbility: Abilities): this {
|
||||
vi.spyOn(Overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
|
||||
this.log(`Player Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`);
|
||||
return this;
|
||||
@ -161,9 +161,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the player (pokemon) {@linkcode Moves | moves}set
|
||||
* @param moveset the {@linkcode Moves | moves}set to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
moveset(moveset: Moves | Moves[]): this {
|
||||
public moveset(moveset: Moves | Moves[]): this {
|
||||
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
|
||||
if (!Array.isArray(moveset)) {
|
||||
moveset = [ moveset ];
|
||||
@ -178,7 +178,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @param statusEffect the {@linkcode StatusEffect | status-effect} to set
|
||||
* @returns
|
||||
*/
|
||||
statusEffect(statusEffect: StatusEffect): this {
|
||||
public statusEffect(statusEffect: StatusEffect): this {
|
||||
vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
|
||||
this.log(`Player Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
|
||||
return this;
|
||||
@ -186,9 +186,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
|
||||
/**
|
||||
* Override each wave to not have standard trainer battles
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
disableTrainerWaves(): this {
|
||||
public disableTrainerWaves(): this {
|
||||
const realFn = getGameMode;
|
||||
vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => {
|
||||
const mode = realFn(gameMode);
|
||||
@ -201,9 +201,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
|
||||
/**
|
||||
* Override each wave to not have critical hits
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
disableCrits() {
|
||||
public disableCrits(): this {
|
||||
vi.spyOn(Overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true);
|
||||
this.log("Critical hits are disabled!");
|
||||
return this;
|
||||
@ -212,9 +212,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the {@linkcode WeatherType | weather (type)}
|
||||
* @param type {@linkcode WeatherType | weather type} to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
weather(type: WeatherType): this {
|
||||
public weather(type: WeatherType): this {
|
||||
vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type);
|
||||
this.log(`Weather set to ${Weather[type]} (=${type})!`);
|
||||
return this;
|
||||
@ -223,9 +223,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the seed
|
||||
* @param seed the seed to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
seed(seed: string): this {
|
||||
public seed(seed: string): this {
|
||||
vi.spyOn(this.game.scene, "resetSeed").mockImplementation(() => {
|
||||
this.game.scene.waveSeed = seed;
|
||||
Phaser.Math.RND.sow([ seed ]);
|
||||
@ -239,9 +239,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the battle type (single or double)
|
||||
* @param battleType battle type to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
battleType(battleType: "single" | "double" | null): this {
|
||||
public battleType(battleType: "single" | "double" | null): this {
|
||||
vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue(battleType);
|
||||
this.log(`Battle type set to ${battleType} only!`);
|
||||
return this;
|
||||
@ -250,9 +250,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the enemy (pokemon) {@linkcode Species | species}
|
||||
* @param species the (pokemon) {@linkcode Species | species} to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
enemySpecies(species: Species | number): this {
|
||||
public enemySpecies(species: Species | number): this {
|
||||
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||
this.log(`Enemy Pokemon species set to ${Species[species]} (=${species})!`);
|
||||
return this;
|
||||
@ -260,9 +260,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
|
||||
/**
|
||||
* Override the enemy (pokemon) to be a random fusion
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
enableEnemyFusion(): this {
|
||||
public enableEnemyFusion(): this {
|
||||
vi.spyOn(Overrides, "OPP_FUSION_OVERRIDE", "get").mockReturnValue(true);
|
||||
this.log("Enemy Pokemon is a random fusion!");
|
||||
return this;
|
||||
@ -271,9 +271,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the enemy (pokemon) fusion species
|
||||
* @param species the fusion species to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
enemyFusionSpecies(species: Species | number): this {
|
||||
public enemyFusionSpecies(species: Species | number): this {
|
||||
vi.spyOn(Overrides, "OPP_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||
this.log(`Enemy Pokemon fusion species set to ${Species[species]} (=${species})!`);
|
||||
return this;
|
||||
@ -282,9 +282,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the enemy (pokemon) {@linkcode Abilities | ability}
|
||||
* @param ability the (pokemon) {@linkcode Abilities | ability} to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
enemyAbility(ability: Abilities): this {
|
||||
public enemyAbility(ability: Abilities): this {
|
||||
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(ability);
|
||||
this.log(`Enemy Pokemon ability set to ${Abilities[ability]} (=${ability})!`);
|
||||
return this;
|
||||
@ -293,9 +293,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the enemy (pokemon) **passive** {@linkcode Abilities | ability}
|
||||
* @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
enemyPassiveAbility(passiveAbility: Abilities): this {
|
||||
public enemyPassiveAbility(passiveAbility: Abilities): this {
|
||||
vi.spyOn(Overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
|
||||
this.log(`Enemy Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`);
|
||||
return this;
|
||||
@ -304,9 +304,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the enemy (pokemon) {@linkcode Moves | moves}set
|
||||
* @param moveset the {@linkcode Moves | moves}set to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
enemyMoveset(moveset: Moves | Moves[]): this {
|
||||
public enemyMoveset(moveset: Moves | Moves[]): this {
|
||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
|
||||
if (!Array.isArray(moveset)) {
|
||||
moveset = [ moveset ];
|
||||
@ -319,9 +319,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the enemy (pokemon) level
|
||||
* @param level the level to set
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
enemyLevel(level: number): this {
|
||||
public enemyLevel(level: number): this {
|
||||
vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(level);
|
||||
this.log(`Enemy Pokemon level set to ${level}!`);
|
||||
return this;
|
||||
@ -332,7 +332,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @param statusEffect the {@linkcode StatusEffect | status-effect} to set
|
||||
* @returns
|
||||
*/
|
||||
enemyStatusEffect(statusEffect: StatusEffect): this {
|
||||
public enemyStatusEffect(statusEffect: StatusEffect): this {
|
||||
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
|
||||
this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
|
||||
return this;
|
||||
@ -341,9 +341,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the enemy (pokemon) held items
|
||||
* @param items the items to hold
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
enemyHeldItems(items: ModifierOverride[]) {
|
||||
public enemyHeldItems(items: ModifierOverride[]): this {
|
||||
vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
|
||||
this.log("Enemy Pokemon held items set to:", items);
|
||||
return this;
|
||||
@ -354,7 +354,7 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* @param unlockable The Unlockable(s) to enable.
|
||||
* @returns `this`
|
||||
*/
|
||||
enableUnlockable(unlockable: Unlockables[]) {
|
||||
public enableUnlockable(unlockable: Unlockables[]): this {
|
||||
vi.spyOn(Overrides, "ITEM_UNLOCK_OVERRIDE", "get").mockReturnValue(unlockable);
|
||||
this.log("Temporarily unlocked the following content: ", unlockable);
|
||||
return this;
|
||||
@ -363,9 +363,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the items rolled at the end of a battle
|
||||
* @param items the items to be rolled
|
||||
* @returns this
|
||||
* @returns `this`
|
||||
*/
|
||||
itemRewards(items: ModifierOverride[]) {
|
||||
public itemRewards(items: ModifierOverride[]): this {
|
||||
vi.spyOn(Overrides, "ITEM_REWARD_OVERRIDE", "get").mockReturnValue(items);
|
||||
this.log("Item rewards set to:", items);
|
||||
return this;
|
||||
@ -375,8 +375,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
* Override player shininess
|
||||
* @param shininess - `true` or `false` to force the player's pokemon to be shiny or not shiny,
|
||||
* `null` to disable the override and re-enable RNG shinies.
|
||||
* @returns `this`
|
||||
*/
|
||||
shiny(shininess: boolean | null): this {
|
||||
public shiny(shininess: boolean | null): this {
|
||||
vi.spyOn(Overrides, "SHINY_OVERRIDE", "get").mockReturnValue(shininess);
|
||||
if (shininess === null) {
|
||||
this.log("Disabled player Pokemon shiny override!");
|
||||
@ -389,8 +390,9 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override player shiny variant
|
||||
* @param variant - The player's shiny variant.
|
||||
* @returns `this`
|
||||
*/
|
||||
shinyVariant(variant: Variant): this {
|
||||
public shinyVariant(variant: Variant): this {
|
||||
vi.spyOn(Overrides, "VARIANT_OVERRIDE", "get").mockReturnValue(variant);
|
||||
this.log(`Set player Pokemon's shiny variant to ${variant}!`);
|
||||
return this;
|
||||
@ -420,23 +422,38 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
/**
|
||||
* Override the enemy (Pokemon) to have the given amount of health segments
|
||||
* @param healthSegments the number of segments to give
|
||||
* default: 0, the health segments will be handled like in the game based on wave, level and species
|
||||
* 1: the Pokemon will not be a boss
|
||||
* 2+: the Pokemon will be a boss with the given number of health segments
|
||||
* @returns this
|
||||
* - `0` (default): the health segments will be handled like in the game based on wave, level and species
|
||||
* - `1`: the Pokemon will not be a boss
|
||||
* - `2`+: the Pokemon will be a boss with the given number of health segments
|
||||
* @returns `this`
|
||||
*/
|
||||
enemyHealthSegments(healthSegments: number) {
|
||||
public enemyHealthSegments(healthSegments: number): this {
|
||||
vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
|
||||
this.log("Enemy Pokemon health segments set to:", healthSegments);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override statuses (Paralysis and Freeze) to always or never activate
|
||||
* @param activate - `true` to force activation, `false` to force no activation, `null` to disable the override
|
||||
* @returns `this`
|
||||
*/
|
||||
public statusActivation(activate: boolean | null): this {
|
||||
vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(activate);
|
||||
if (activate !== null) {
|
||||
this.log(`Paralysis and Freeze forced to ${activate ? "always" : "never"} activate!`);
|
||||
} else {
|
||||
this.log("Status activation override disabled!");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the encounter chance for a mystery encounter.
|
||||
* @param percentage the encounter chance in %
|
||||
* @returns spy instance
|
||||
* @returns `this`
|
||||
*/
|
||||
mysteryEncounterChance(percentage: number) {
|
||||
public mysteryEncounterChance(percentage: number): this {
|
||||
const maxRate: number = 256; // 100%
|
||||
const rate = maxRate * (percentage / 100);
|
||||
vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate);
|
||||
@ -446,10 +463,10 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
|
||||
/**
|
||||
* Override the encounter chance for a mystery encounter.
|
||||
* @returns spy instance
|
||||
* @param tier
|
||||
* @param tier - The {@linkcode MysteryEncounterTier} to encounter
|
||||
* @returns `this`
|
||||
*/
|
||||
mysteryEncounterTier(tier: MysteryEncounterTier) {
|
||||
public mysteryEncounterTier(tier: MysteryEncounterTier): this {
|
||||
vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier);
|
||||
this.log(`Mystery encounter tier set to ${tier}!`);
|
||||
return this;
|
||||
@ -457,10 +474,10 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
|
||||
/**
|
||||
* Override the encounter that spawns for the scene
|
||||
* @param encounterType
|
||||
* @returns spy instance
|
||||
* @param encounterType - The {@linkcode MysteryEncounterType} of the encounter
|
||||
* @returns `this`
|
||||
*/
|
||||
mysteryEncounter(encounterType: MysteryEncounterType) {
|
||||
public mysteryEncounter(encounterType: MysteryEncounterType): this {
|
||||
vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType);
|
||||
this.log(`Mystery encounter override set to ${encounterType}!`);
|
||||
return this;
|
||||
|
@ -279,11 +279,8 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container {
|
||||
this.pokemonAbilityText.setColor(getTextColor(abilityTextStyle, false, this.scene.uiTheme));
|
||||
this.pokemonAbilityText.setShadowColor(getTextColor(abilityTextStyle, true, this.scene.uiTheme));
|
||||
|
||||
|
||||
const ownedAbilityAttrs = pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId()].abilityAttr;
|
||||
|
||||
// Check if the player owns ability for the root form
|
||||
const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs);
|
||||
const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(starterEntry.abilityAttr);
|
||||
|
||||
if (!playerOwnsThisAbility) {
|
||||
this.pokemonAbilityLabelText.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme));
|
||||
|
Loading…
Reference in New Issue
Block a user