Compare commits
47 Commits
b28c28ac01
...
5f9f96cce5
Author | SHA1 | Date |
---|---|---|
Alex Van Liew | 5f9f96cce5 | |
flx-sta | 6030b780f2 | |
Jannik Tappert | e386504977 | |
podar | 106ed6b27b | |
flx-sta | 4389bff5d0 | |
Madmadness65 | 00ba2eebc8 | |
Leo Kim | fe69bd2b55 | |
Dakurei | 1fb5389765 | |
NightKev | 559097d309 | |
NightKev | b400cbf0c1 | |
NightKev | d1f0bf8c2f | |
Alex Van Liew | 5a582db148 | |
Alex Van Liew | c47c82e634 | |
Alex Van Liew | e80f158691 | |
Alex Van Liew | 6faaa9edaa | |
Alex Van Liew | 772a6fc079 | |
Alex Van Liew | f0eac00179 | |
Alex Van Liew | b057c144ac | |
snoozbuster | 113e8ab3bd | |
snoozbuster | f9ea476ef3 | |
snoozbuster | 88dcaa3d20 | |
snoozbuster | f5fabbb159 | |
snoozbuster | 74cbdbb837 | |
snoozbuster | daebb3a91c | |
snoozbuster | 1b7ccb1912 | |
snoozbuster | d45ed3d9d3 | |
snoozbuster | eb11b951e6 | |
snoozbuster | b681bc386a | |
snoozbuster | c31809ed35 | |
snoozbuster | 1bb79b87f6 | |
snoozbuster | c628759756 | |
snoozbuster | 8a4a297cd6 | |
snoozbuster | 9d721f6610 | |
snoozbuster | c080ba0b46 | |
snoozbuster | eb1d195866 | |
snoozbuster | c6e93985ca | |
snoozbuster | d6ec174715 | |
snoozbuster | e325af1f0b | |
Alex Van Liew | c1019bac39 | |
Alex Van Liew | a086b3a0ad | |
Alex Van Liew | 5e21005fe9 | |
snoozbuster | a8b4d6a9de | |
snoozbuster | 7517e16c84 | |
snoozbuster | a3a42931ba | |
snoozbuster | a0f8a4df5b | |
snoozbuster | 939a3f32a8 | |
snoozbuster | f428fd114c |
|
@ -55,7 +55,7 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to
|
||||||
- Pokémon Sword/Shield
|
- Pokémon Sword/Shield
|
||||||
- Pokémon Legends: Arceus
|
- Pokémon Legends: Arceus
|
||||||
- Pokémon Scarlet/Violet
|
- Pokémon Scarlet/Violet
|
||||||
- Firel (Custom Laboratory, Metropolis, Seabed, and Space biome music)
|
- Firel (Custom Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music)
|
||||||
- Lmz (Custom Jungle biome music)
|
- Lmz (Custom Jungle biome music)
|
||||||
- Andr06 (Custom Slum and Sea biome music)
|
- Andr06 (Custom Slum and Sea biome music)
|
||||||
|
|
||||||
|
|
26
index.css
26
index.css
|
@ -26,10 +26,36 @@ body {
|
||||||
#app {
|
#app {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app > div:first-child {
|
#app > div:first-child {
|
||||||
|
transform-origin: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Supports automatic vertical centering as suggested in PR#1114, but only via CSS
|
||||||
|
|
||||||
|
Condition factorized to deduce CSS rules:
|
||||||
|
true if (isLandscape && !isMobile() && !hasTouchscreen() || (hasTouchscreen() && !isTouchControlsEnabled))
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* isLandscape && !isMobile() && !hasTouchscreen() */
|
||||||
|
@media (orientation: landscape) and (pointer: fine) {
|
||||||
|
#app {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
/* hasTouchscreen() && !isTouchControlsEnabled */
|
||||||
|
body:has(> #touchControls[class=visible]) #app {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:has(> #touchControls[class=visible]) #app > div:first-child {
|
||||||
transform-origin: top !important;
|
transform-origin: top !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#layout:fullscreen #dpad, #layout:fullscreen {
|
#layout:fullscreen #dpad, #layout:fullscreen {
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -2625,7 +2625,11 @@ export class PreStatStageChangeAbAttr extends AbAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities
|
||||||
|
*/
|
||||||
export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
||||||
|
/** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */
|
||||||
private protectedStat?: BattleStat;
|
private protectedStat?: BattleStat;
|
||||||
|
|
||||||
constructor(protectedStat?: BattleStat) {
|
constructor(protectedStat?: BattleStat) {
|
||||||
|
@ -2634,7 +2638,17 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
||||||
this.protectedStat = protectedStat;
|
this.protectedStat = protectedStat;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean {
|
/**
|
||||||
|
* Apply the {@linkcode ProtectedStatAbAttr} to an interaction
|
||||||
|
* @param _pokemon
|
||||||
|
* @param _passive
|
||||||
|
* @param simulated
|
||||||
|
* @param stat the {@linkcode BattleStat} being affected
|
||||||
|
* @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true if the stat is protected
|
||||||
|
* @param _args
|
||||||
|
* @returns true if the stat is protected, false otherwise
|
||||||
|
*/
|
||||||
|
applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean {
|
||||||
if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) {
|
if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) {
|
||||||
cancelled.value = true;
|
cancelled.value = true;
|
||||||
return true;
|
return true;
|
||||||
|
@ -3757,7 +3771,7 @@ export class StatStageChangeMultiplierAbAttr extends AbAttr {
|
||||||
this.multiplier = multiplier;
|
this.multiplier = multiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||||
(args[0] as Utils.IntegerHolder).value *= this.multiplier;
|
(args[0] as Utils.IntegerHolder).value *= this.multiplier;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -488,14 +488,14 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
|
||||||
} else {
|
} else {
|
||||||
moveAnims.set(move, null);
|
moveAnims.set(move, null);
|
||||||
const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP;
|
const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP;
|
||||||
const moveName = Moves[move].toLowerCase().replace(/\_/g, "-");
|
|
||||||
const fetchAnimAndResolve = (move: Moves) => {
|
const fetchAnimAndResolve = (move: Moves) => {
|
||||||
scene.cachedFetch(`./battle-anims/${moveName}.json`)
|
scene.cachedFetch(`./battle-anims/${Utils.animationFileName(move)}.json`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
if (!response.ok || contentType?.indexOf("application/json") === -1) {
|
if (!response.ok || contentType?.indexOf("application/json") === -1) {
|
||||||
console.error(`Could not load animation file for move '${moveName}'`, response.status, response.statusText);
|
useDefaultAnim(move, defaultMoveAnim);
|
||||||
populateMoveAnim(move, moveAnims.get(defaultMoveAnim));
|
logMissingMoveAnim(move, response.status, response.statusText);
|
||||||
return resolve();
|
return resolve();
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
|
@ -515,6 +515,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
useDefaultAnim(move, defaultMoveAnim);
|
||||||
|
logMissingMoveAnim(move, error);
|
||||||
|
return resolve();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
fetchAnimAndResolve(move);
|
fetchAnimAndResolve(move);
|
||||||
|
@ -522,6 +527,29 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the default animation for the given move.
|
||||||
|
*
|
||||||
|
* @param move the move to populate an animation for
|
||||||
|
* @param defaultMoveAnim the move to use as the default animation
|
||||||
|
*/
|
||||||
|
function useDefaultAnim(move: Moves, defaultMoveAnim: Moves) {
|
||||||
|
populateMoveAnim(move, moveAnims.get(defaultMoveAnim));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method for printing a warning to the console when a move animation is missing.
|
||||||
|
*
|
||||||
|
* @param move the move to populate an animation for
|
||||||
|
* @param optionalParams parameters to add to the error logging
|
||||||
|
*
|
||||||
|
* @remarks use {@linkcode useDefaultAnim} to use a default animation
|
||||||
|
*/
|
||||||
|
function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) {
|
||||||
|
const moveName = Utils.animationFileName(move);
|
||||||
|
console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches animation configs to be used in a Mystery Encounter
|
* Fetches animation configs to be used in a Mystery Encounter
|
||||||
* @param scene
|
* @param scene
|
||||||
|
|
|
@ -2490,6 +2490,10 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||||
return new SubstituteTag(sourceMove, sourceId);
|
return new SubstituteTag(sourceMove, sourceId);
|
||||||
case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON:
|
case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON:
|
||||||
return new MysteryEncounterPostSummonTag();
|
return new MysteryEncounterPostSummonTag();
|
||||||
|
case BattlerTagType.ANTICIPATING_ACTION:
|
||||||
|
return new BattlerTag(BattlerTagType.ANTICIPATING_ACTION, BattlerTagLapseType.TURN_END, 1, sourceMove);
|
||||||
|
case BattlerTagType.ESCAPING:
|
||||||
|
return new BattlerTag(BattlerTagType.ESCAPING, BattlerTagLapseType.TURN_END, 1, sourceMove);
|
||||||
case BattlerTagType.NONE:
|
case BattlerTagType.NONE:
|
||||||
default:
|
default:
|
||||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||||
|
|
167
src/data/move.ts
167
src/data/move.ts
|
@ -1086,8 +1086,7 @@ export class AddBattlerTagHeaderAttr extends MoveHeaderAttr {
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
user.addTag(this.tagType);
|
return user.addTag(this.tagType, 0, move.id);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3839,7 +3838,7 @@ export class StormAccuracyAttr extends VariableAccuracyAttr {
|
||||||
* @extends VariableAccuracyAttr
|
* @extends VariableAccuracyAttr
|
||||||
* @see {@linkcode apply}
|
* @see {@linkcode apply}
|
||||||
*/
|
*/
|
||||||
export class MinimizeAccuracyAttr extends VariableAccuracyAttr {
|
export class AlwaysHitMinimizeAttr extends VariableAccuracyAttr {
|
||||||
/**
|
/**
|
||||||
* @see {@linkcode apply}
|
* @see {@linkcode apply}
|
||||||
* @param user N/A
|
* @param user N/A
|
||||||
|
@ -3887,6 +3886,21 @@ export class BlizzardAccuracyAttr extends VariableAccuracyAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPursuingFunc = (user: Pokemon, target: Pokemon) =>
|
||||||
|
user.getTag(BattlerTagType.ANTICIPATING_ACTION) && target.getTag(BattlerTagType.ESCAPING);
|
||||||
|
|
||||||
|
export class PursuitAccuracyAttr extends VariableAccuracyAttr {
|
||||||
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
if (isPursuingFunc(user, target)) {
|
||||||
|
const accuracy = args[0] as Utils.NumberHolder;
|
||||||
|
accuracy.value = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class VariableMoveCategoryAttr extends MoveAttr {
|
export class VariableMoveCategoryAttr extends MoveAttr {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
return false;
|
return false;
|
||||||
|
@ -4438,8 +4452,21 @@ export class TypelessAttr extends MoveAttr { }
|
||||||
/**
|
/**
|
||||||
* Attribute used for moves which ignore redirection effects, and always target their original target, i.e. Snipe Shot
|
* Attribute used for moves which ignore redirection effects, and always target their original target, i.e. Snipe Shot
|
||||||
* Bypasses Storm Drain, Follow Me, Ally Switch, and the like.
|
* Bypasses Storm Drain, Follow Me, Ally Switch, and the like.
|
||||||
|
*
|
||||||
|
* Optionally accepts a function to run which can be used to conditionally bypass redirection effects.
|
||||||
*/
|
*/
|
||||||
export class BypassRedirectAttr extends MoveAttr { }
|
export class BypassRedirectAttr extends MoveAttr {
|
||||||
|
private bypassConditionFn?: (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean;
|
||||||
|
|
||||||
|
constructor(bypassConditionFn?: (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean) {
|
||||||
|
super();
|
||||||
|
this.bypassConditionFn = bypassConditionFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(user: Pokemon | null, target: Pokemon | null, move: Move) {
|
||||||
|
return this.bypassConditionFn?.(user, target, move) ?? true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FrenzyAttr extends MoveEffectAttr {
|
export class FrenzyAttr extends MoveEffectAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -4699,8 +4726,8 @@ export class LapseBattlerTagAttr extends MoveEffectAttr {
|
||||||
export class RemoveBattlerTagAttr extends MoveEffectAttr {
|
export class RemoveBattlerTagAttr extends MoveEffectAttr {
|
||||||
public tagTypes: BattlerTagType[];
|
public tagTypes: BattlerTagType[];
|
||||||
|
|
||||||
constructor(tagTypes: BattlerTagType[], selfTarget: boolean = false) {
|
constructor(tagTypes: BattlerTagType[], selfTarget: boolean = false, trigger?: MoveEffectTrigger) {
|
||||||
super(selfTarget);
|
super(selfTarget, trigger);
|
||||||
|
|
||||||
this.tagTypes = tagTypes;
|
this.tagTypes = tagTypes;
|
||||||
}
|
}
|
||||||
|
@ -4855,10 +4882,10 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr {
|
||||||
* Attribute used when a move hits a {@linkcode BattlerTagType} for double damage
|
* Attribute used when a move hits a {@linkcode BattlerTagType} for double damage
|
||||||
* @extends MoveAttr
|
* @extends MoveAttr
|
||||||
*/
|
*/
|
||||||
export class HitsTagAttr extends MoveAttr {
|
export class DealsDoubleDamageToTagAttr extends MoveAttr {
|
||||||
/** The {@linkcode BattlerTagType} this move hits */
|
/** The {@linkcode BattlerTagType} this move hits */
|
||||||
public tagType: BattlerTagType;
|
public tagType: BattlerTagType;
|
||||||
/** Should this move deal double damage against {@linkcode HitsTagAttr.tagType}? */
|
/** Should this move deal double damage against {@linkcode DealsDoubleDamageToTagAttr.tagType}? */
|
||||||
public doubleDamage: boolean;
|
public doubleDamage: boolean;
|
||||||
|
|
||||||
constructor(tagType: BattlerTagType, doubleDamage?: boolean) {
|
constructor(tagType: BattlerTagType, doubleDamage?: boolean) {
|
||||||
|
@ -5157,6 +5184,11 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||||
private user: boolean;
|
private user: boolean;
|
||||||
private batonPass: boolean;
|
private batonPass: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param user Indicates if this move effect will switch out the user of the
|
||||||
|
* move (true) or the target of the move (false).
|
||||||
|
* @param batonPass Indicates if this move is a usage of Baton Pass.
|
||||||
|
*/
|
||||||
constructor(user?: boolean, batonPass?: boolean) {
|
constructor(user?: boolean, batonPass?: boolean) {
|
||||||
super(false, MoveEffectTrigger.POST_APPLY, false, true);
|
super(false, MoveEffectTrigger.POST_APPLY, false, true);
|
||||||
this.user = !!user;
|
this.user = !!user;
|
||||||
|
@ -5175,26 +5207,76 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||||
return resolve(false);
|
return resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the switch out logic inside the conditional block
|
|
||||||
// This ensures that the switch out only happens when the conditions are met
|
|
||||||
const switchOutTarget = this.user ? user : target;
|
const switchOutTarget = this.user ? user : target;
|
||||||
|
|
||||||
|
let willBePursued = false;
|
||||||
|
if (switchOutTarget.hp > 0 && !this.batonPass && this.user && move.id !== Moves.TELEPORT) {
|
||||||
|
switchOutTarget.addTag(BattlerTagType.ESCAPING);
|
||||||
|
|
||||||
|
const opposingField = user.isPlayer() ? user.scene.getEnemyField() : user.scene.getPlayerField();
|
||||||
|
const opposingPursuitUsers = opposingField
|
||||||
|
.filter((op: Pokemon) => op.getTag(BattlerTagType.ANTICIPATING_ACTION)?.sourceMove === Moves.PURSUIT)
|
||||||
|
.sort((a, b) => b.turnData.order - a.turnData.order);
|
||||||
|
if (opposingPursuitUsers.length) {
|
||||||
|
willBePursued = true;
|
||||||
|
opposingPursuitUsers.forEach(pursuiter => {
|
||||||
|
if (user.scene.tryRemovePhase(p => p instanceof MovePhase && p.pokemon.id === pursuiter.id)) {
|
||||||
|
user.scene.prependToPhase(
|
||||||
|
new MovePhase(
|
||||||
|
user.scene,
|
||||||
|
pursuiter,
|
||||||
|
[switchOutTarget.getBattlerIndex()],
|
||||||
|
pursuiter.getMoveset().find(m =>
|
||||||
|
m?.moveId === Moves.PURSUIT) || new PokemonMove(Moves.PURSUIT),
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
MoveEndPhase
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (switchOutTarget instanceof PlayerPokemon) {
|
if (switchOutTarget instanceof PlayerPokemon) {
|
||||||
|
if (!willBePursued) {
|
||||||
switchOutTarget.leaveField(!this.batonPass);
|
switchOutTarget.leaveField(!this.batonPass);
|
||||||
|
}
|
||||||
|
|
||||||
if (switchOutTarget.hp > 0) {
|
if (switchOutTarget.hp > 0) {
|
||||||
user.scene.prependToPhase(new SwitchPhase(user.scene, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
|
user.scene.prependToPhase(
|
||||||
|
new SwitchPhase(user.scene, switchOutTarget.getFieldIndex(), "moveEffect", willBePursued),
|
||||||
|
MoveEndPhase
|
||||||
|
);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
} else {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
|
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
|
||||||
|
if (!user.scene.currentBattle.trainer) {
|
||||||
|
return resolve(false); // what are we even doing here
|
||||||
|
}
|
||||||
|
|
||||||
// Switch out logic for trainer battles
|
// Switch out logic for trainer battles
|
||||||
|
if (!willBePursued) {
|
||||||
switchOutTarget.leaveField(!this.batonPass);
|
switchOutTarget.leaveField(!this.batonPass);
|
||||||
|
}
|
||||||
|
|
||||||
if (switchOutTarget.hp > 0) {
|
if (switchOutTarget.hp > 0) {
|
||||||
// for opponent switching out
|
// for opponent switching out
|
||||||
user.scene.prependToPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(), (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), false, this.batonPass, false), MoveEndPhase);
|
user.scene.prependToPhase(
|
||||||
|
new SwitchSummonPhase(
|
||||||
|
user.scene,
|
||||||
|
switchOutTarget.getFieldIndex(),
|
||||||
|
user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot),
|
||||||
|
willBePursued,
|
||||||
|
this.batonPass,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
MoveEndPhase
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Switch out logic for everything else (eg: WILD battles)
|
// Switch out logic for everything else (eg: WILD battles)
|
||||||
|
@ -6752,12 +6834,11 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1)
|
new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1)
|
||||||
.slicingMove(),
|
.slicingMove(),
|
||||||
new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1)
|
new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING, true)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true)
|
||||||
.windMove(),
|
.windMove(),
|
||||||
new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1),
|
new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1),
|
||||||
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
|
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
|
||||||
.attr(ForceSwitchOutAttr)
|
.attr(ForceSwitchOutAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
|
||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
.hidesTarget()
|
.hidesTarget()
|
||||||
.windMove(),
|
.windMove(),
|
||||||
|
@ -6770,8 +6851,8 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1),
|
new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1),
|
||||||
new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1),
|
new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1),
|
||||||
new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1)
|
new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1)
|
||||||
.attr(MinimizeAccuracyAttr)
|
.attr(AlwaysHitMinimizeAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
|
||||||
.attr(FlinchAttr),
|
.attr(FlinchAttr),
|
||||||
new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1)
|
new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1)
|
||||||
.attr(MultiHitAttr, MultiHitType._2),
|
.attr(MultiHitAttr, MultiHitType._2),
|
||||||
|
@ -6795,8 +6876,8 @@ export function initMoves() {
|
||||||
.attr(OneHitKOAccuracyAttr),
|
.attr(OneHitKOAccuracyAttr),
|
||||||
new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1),
|
new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1),
|
||||||
new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1)
|
new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1)
|
||||||
.attr(MinimizeAccuracyAttr)
|
.attr(AlwaysHitMinimizeAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
|
||||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
|
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
|
||||||
new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1)
|
new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1)
|
||||||
.attr(TrapAttr, BattlerTagType.WRAP),
|
.attr(TrapAttr, BattlerTagType.WRAP),
|
||||||
|
@ -6864,7 +6945,7 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1),
|
new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1),
|
||||||
new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1)
|
new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1)
|
||||||
.target(MoveTarget.ALL_NEAR_OTHERS)
|
.target(MoveTarget.ALL_NEAR_OTHERS)
|
||||||
.attr(HitsTagAttr, BattlerTagType.UNDERWATER, true)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true)
|
||||||
.attr(GulpMissileTagAttr),
|
.attr(GulpMissileTagAttr),
|
||||||
new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
|
new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
|
||||||
.attr(StatusEffectAttr, StatusEffect.FREEZE),
|
.attr(StatusEffectAttr, StatusEffect.FREEZE),
|
||||||
|
@ -6947,18 +7028,18 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1)
|
new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1)
|
||||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||||
.attr(ThunderAccuracyAttr)
|
.attr(ThunderAccuracyAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false),
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false),
|
||||||
new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1)
|
new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1)
|
new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1)
|
||||||
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true)
|
||||||
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
|
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
.target(MoveTarget.ALL_NEAR_OTHERS),
|
.target(MoveTarget.ALL_NEAR_OTHERS),
|
||||||
new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
|
new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
|
||||||
.attr(OneHitKOAttr)
|
.attr(OneHitKOAttr)
|
||||||
.attr(OneHitKOAccuracyAttr)
|
.attr(OneHitKOAccuracyAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, false)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, false)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
|
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)
|
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND)
|
||||||
|
@ -7347,7 +7428,7 @@ export function initMoves() {
|
||||||
.attr(PreMoveMessageAttr, magnitudeMessageFunc)
|
.attr(PreMoveMessageAttr, magnitudeMessageFunc)
|
||||||
.attr(MagnitudePowerAttr)
|
.attr(MagnitudePowerAttr)
|
||||||
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
|
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
|
||||||
.attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true)
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
.target(MoveTarget.ALL_NEAR_OTHERS),
|
.target(MoveTarget.ALL_NEAR_OTHERS),
|
||||||
new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2)
|
new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2)
|
||||||
|
@ -7364,7 +7445,11 @@ export function initMoves() {
|
||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target)),
|
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target)),
|
||||||
new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2)
|
new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2)
|
||||||
.partial(),
|
.attr(PursuitAccuracyAttr)
|
||||||
|
.attr(BypassRedirectAttr, (user, target) => Boolean(user && target && isPursuingFunc(user, target)))
|
||||||
|
.attr(AddBattlerTagHeaderAttr, BattlerTagType.ANTICIPATING_ACTION)
|
||||||
|
.attr(RemoveBattlerTagAttr, [BattlerTagType.ANTICIPATING_ACTION], true, MoveEffectTrigger.POST_APPLY)
|
||||||
|
.attr(MovePowerMultiplierAttr, (user, target) => isPursuingFunc(user, target) ? 2 : 1),
|
||||||
new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2)
|
new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true)
|
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true)
|
||||||
.attr(RemoveBattlerTagAttr, [
|
.attr(RemoveBattlerTagAttr, [
|
||||||
|
@ -7403,7 +7488,7 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2)
|
new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2)
|
||||||
.attr(HighCritAttr),
|
.attr(HighCritAttr),
|
||||||
new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2)
|
new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING, true)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true)
|
||||||
.attr(FlinchAttr)
|
.attr(FlinchAttr)
|
||||||
.windMove()
|
.windMove()
|
||||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||||
|
@ -7435,7 +7520,7 @@ export function initMoves() {
|
||||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1),
|
.attr(StatStageChangeAttr, [ Stat.DEF ], -1),
|
||||||
new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
|
new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
|
||||||
.attr(TrapAttr, BattlerTagType.WHIRLPOOL)
|
.attr(TrapAttr, BattlerTagType.WHIRLPOOL)
|
||||||
.attr(HitsTagAttr, BattlerTagType.UNDERWATER, true),
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true),
|
||||||
new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2)
|
new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2)
|
||||||
.attr(MultiHitAttr, MultiHitType.BEAT_UP)
|
.attr(MultiHitAttr, MultiHitType.BEAT_UP)
|
||||||
.attr(BeatUpAttr)
|
.attr(BeatUpAttr)
|
||||||
|
@ -7658,7 +7743,7 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3)
|
new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3)
|
||||||
.attr(FlinchAttr),
|
.attr(FlinchAttr),
|
||||||
new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3)
|
new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING)
|
||||||
.punchingMove(),
|
.punchingMove(),
|
||||||
new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
|
new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
|
||||||
.attr(TrapAttr, BattlerTagType.SAND_TOMB)
|
.attr(TrapAttr, BattlerTagType.SAND_TOMB)
|
||||||
|
@ -7889,8 +7974,8 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4)
|
new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4)
|
||||||
.pulseMove(),
|
.pulseMove(),
|
||||||
new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4)
|
new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4)
|
||||||
.attr(MinimizeAccuracyAttr)
|
.attr(AlwaysHitMinimizeAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
|
||||||
.attr(FlinchAttr),
|
.attr(FlinchAttr),
|
||||||
new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4),
|
new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4),
|
||||||
new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4)
|
new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4)
|
||||||
|
@ -8087,7 +8172,7 @@ export function initMoves() {
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
|
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||||
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
|
||||||
.attr(CritOnlyAttr),
|
.attr(CritOnlyAttr),
|
||||||
|
@ -8100,9 +8185,9 @@ export function initMoves() {
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
||||||
.danceMove(),
|
.danceMove(),
|
||||||
new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
|
new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
|
||||||
.attr(MinimizeAccuracyAttr)
|
.attr(AlwaysHitMinimizeAttr)
|
||||||
.attr(CompareWeightPowerAttr)
|
.attr(CompareWeightPowerAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true),
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true),
|
||||||
new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5)
|
new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5)
|
||||||
.target(MoveTarget.ALL_NEAR_OTHERS)
|
.target(MoveTarget.ALL_NEAR_OTHERS)
|
||||||
.condition(unknownTypeCondition)
|
.condition(unknownTypeCondition)
|
||||||
|
@ -8253,12 +8338,14 @@ export function initMoves() {
|
||||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1)
|
.attr(StatStageChangeAttr, [ Stat.DEF ], -1)
|
||||||
.slicingMove(),
|
.slicingMove(),
|
||||||
new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
|
new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
|
||||||
.attr(MinimizeAccuracyAttr)
|
.attr(AlwaysHitMinimizeAttr)
|
||||||
.attr(CompareWeightPowerAttr)
|
.attr(CompareWeightPowerAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true),
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true),
|
||||||
new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5)
|
new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5)
|
||||||
.attr(StatStageChangeAttr, [ Stat.ACC ], -1),
|
.attr(StatStageChangeAttr, [ Stat.ACC ], -1),
|
||||||
new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5)
|
new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5)
|
||||||
|
.attr(AlwaysHitMinimizeAttr)
|
||||||
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
|
||||||
.attr(FlinchAttr),
|
.attr(FlinchAttr),
|
||||||
new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5)
|
new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5)
|
||||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 3, true),
|
.attr(StatStageChangeAttr, [ Stat.DEF ], 3, true),
|
||||||
|
@ -8271,7 +8358,7 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5)
|
new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5)
|
||||||
.attr(ThunderAccuracyAttr)
|
.attr(ThunderAccuracyAttr)
|
||||||
.attr(ConfuseAttr)
|
.attr(ConfuseAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false)
|
||||||
.windMove(),
|
.windMove(),
|
||||||
new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5)
|
new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5)
|
||||||
.attr(RecoilAttr)
|
.attr(RecoilAttr)
|
||||||
|
@ -8325,9 +8412,9 @@ export function initMoves() {
|
||||||
.attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE)
|
.attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6)
|
new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6)
|
||||||
.attr(MinimizeAccuracyAttr)
|
.attr(AlwaysHitMinimizeAttr)
|
||||||
.attr(FlyingTypeMultiplierAttr)
|
.attr(FlyingTypeMultiplierAttr)
|
||||||
.attr(HitsTagAttr, BattlerTagType.MINIMIZED, true)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
|
||||||
.condition(failOnGravityCondition),
|
.condition(failOnGravityCondition),
|
||||||
new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6)
|
new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6)
|
||||||
.target(MoveTarget.USER_SIDE)
|
.target(MoveTarget.USER_SIDE)
|
||||||
|
@ -8498,8 +8585,8 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||||
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
|
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
|
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
|
||||||
.attr(HitsTagAttr, BattlerTagType.FLYING, false)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false)
|
||||||
.attr(HitsTagAttr, BattlerTagType.MAGNET_RISEN, false)
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MAGNET_RISEN, false)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
|
||||||
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
|
@ -8756,6 +8843,8 @@ export function initMoves() {
|
||||||
.partial()
|
.partial()
|
||||||
.ignoresVirtual(),
|
.ignoresVirtual(),
|
||||||
new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7)
|
new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7)
|
||||||
|
.attr(AlwaysHitMinimizeAttr)
|
||||||
|
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true)
|
||||||
.partial()
|
.partial()
|
||||||
.ignoresVirtual(),
|
.ignoresVirtual(),
|
||||||
new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7)
|
new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7)
|
||||||
|
|
|
@ -80,4 +80,14 @@ export enum BattlerTagType {
|
||||||
BURNED_UP = "BURNED_UP",
|
BURNED_UP = "BURNED_UP",
|
||||||
DOUBLE_SHOCKED = "DOUBLE_SHOCKED",
|
DOUBLE_SHOCKED = "DOUBLE_SHOCKED",
|
||||||
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON",
|
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag which indicates the battler is waiting for their opponent to make some
|
||||||
|
* sort of action (switch out, use a type of move, make contact, etc)
|
||||||
|
*/
|
||||||
|
ANTICIPATING_ACTION = "ANTICIPATING_ACTION",
|
||||||
|
/**
|
||||||
|
* Tag which indicates the battler is about to switch out.
|
||||||
|
*/
|
||||||
|
ESCAPING = "ESCAPING",
|
||||||
}
|
}
|
||||||
|
|
|
@ -746,7 +746,7 @@ export class Arena {
|
||||||
case Biome.TOWN:
|
case Biome.TOWN:
|
||||||
return 7.288;
|
return 7.288;
|
||||||
case Biome.PLAINS:
|
case Biome.PLAINS:
|
||||||
return 7.693;
|
return 17.485;
|
||||||
case Biome.GRASS:
|
case Biome.GRASS:
|
||||||
return 1.995;
|
return 1.995;
|
||||||
case Biome.TALL_GRASS:
|
case Biome.TALL_GRASS:
|
||||||
|
@ -774,13 +774,13 @@ export class Arena {
|
||||||
case Biome.DESERT:
|
case Biome.DESERT:
|
||||||
return 1.143;
|
return 1.143;
|
||||||
case Biome.ICE_CAVE:
|
case Biome.ICE_CAVE:
|
||||||
return 15.010;
|
return 0.000;
|
||||||
case Biome.MEADOW:
|
case Biome.MEADOW:
|
||||||
return 3.891;
|
return 3.891;
|
||||||
case Biome.POWER_PLANT:
|
case Biome.POWER_PLANT:
|
||||||
return 2.810;
|
return 9.447;
|
||||||
case Biome.VOLCANO:
|
case Biome.VOLCANO:
|
||||||
return 5.116;
|
return 17.637;
|
||||||
case Biome.GRAVEYARD:
|
case Biome.GRAVEYARD:
|
||||||
return 3.232;
|
return 3.232;
|
||||||
case Biome.DOJO:
|
case Biome.DOJO:
|
||||||
|
|
|
@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene";
|
||||||
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
||||||
import { variantData } from "#app/data/variant";
|
import { variantData } from "#app/data/variant";
|
||||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
|
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../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 } from "../data/move";
|
import Move, { HighCritAttr, DealsDoubleDamageToTagAttr, 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 } from "../data/move";
|
||||||
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
|
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
|
||||||
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
|
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
|
||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
|
@ -1273,13 +1273,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
* @param attrType {@linkcode AbAttr} The ability attribute to check for.
|
* @param attrType {@linkcode AbAttr} The ability attribute to check for.
|
||||||
* @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active
|
* @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active
|
||||||
* @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects
|
* @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects
|
||||||
* @returns {AbAttr[]} A list of all the ability attributes on this ability.
|
* @returns A list of all the ability attributes on this ability.
|
||||||
*/
|
*/
|
||||||
getAbilityAttrs(attrType: { new(...args: any[]): AbAttr }, canApply: boolean = true, ignoreOverride?: boolean): AbAttr[] {
|
getAbilityAttrs<T extends AbAttr = AbAttr>(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride?: boolean): T[] {
|
||||||
const abilityAttrs: AbAttr[] = [];
|
const abilityAttrs: T[] = [];
|
||||||
|
|
||||||
if (!canApply || this.canApplyAbility()) {
|
if (!canApply || this.canApplyAbility()) {
|
||||||
abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType));
|
abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs<T>(attrType));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canApply || this.canApplyAbility(true)) {
|
if (!canApply || this.canApplyAbility(true)) {
|
||||||
|
@ -1513,7 +1513,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
|
|
||||||
const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType);
|
const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType);
|
||||||
for (const tag of immuneTags) {
|
for (const tag of immuneTags) {
|
||||||
if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) {
|
if (move && !move.getAttrs(DealsDoubleDamageToTagAttr).some(attr => attr.tagType === tag.tagType)) {
|
||||||
typeMultiplier.value = 0;
|
typeMultiplier.value = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -2489,13 +2489,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier);
|
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if:
|
* For each {@linkcode DealsDoubleDamageToTagAttr} the move has, doubles the damage of the move if:
|
||||||
* The target has a {@linkcode BattlerTagType} that this move interacts with
|
* The target has a {@linkcode BattlerTagType} that this move interacts with
|
||||||
* AND
|
* AND
|
||||||
* The move doubles damage when used against that tag
|
* The move doubles damage when used against that tag
|
||||||
*/
|
*/
|
||||||
const hitsTagMultiplier = new Utils.NumberHolder(1);
|
const hitsTagMultiplier = new Utils.NumberHolder(1);
|
||||||
move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => {
|
move.getAttrs(DealsDoubleDamageToTagAttr).filter(hta => hta.doubleDamage).forEach(hta => {
|
||||||
if (this.getTag(hta.tagType)) {
|
if (this.getTag(hta.tagType)) {
|
||||||
hitsTagMultiplier.value *= 2;
|
hitsTagMultiplier.value *= 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import i18next from "i18next";
|
||||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||||
import { Species } from "#enums/species";
|
import { Species } from "#enums/species";
|
||||||
import { TrainerType } from "#enums/trainer-type";
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
|
import Overrides from "#app/overrides";
|
||||||
|
|
||||||
export enum TrainerVariant {
|
export enum TrainerVariant {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
|
@ -267,6 +268,17 @@ export default class Trainer extends Phaser.GameObjects.Container {
|
||||||
let ret: EnemyPokemon;
|
let ret: EnemyPokemon;
|
||||||
|
|
||||||
this.scene.executeWithSeedOffset(() => {
|
this.scene.executeWithSeedOffset(() => {
|
||||||
|
if (Overrides.TRAINER_PARTY_OVERRIDE?.length) {
|
||||||
|
ret = this.scene.addEnemyPokemon(
|
||||||
|
getPokemonSpecies(Overrides.TRAINER_PARTY_OVERRIDE[index % Overrides.TRAINER_PARTY_OVERRIDE.length]),
|
||||||
|
level,
|
||||||
|
!this.isDouble() || !(index % 2)
|
||||||
|
? TrainerSlot.TRAINER
|
||||||
|
: TrainerSlot.TRAINER_PARTNER
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const template = this.getPartyTemplate();
|
const template = this.getPartyTemplate();
|
||||||
const strength: PartyMemberStrength = template.getStrength(index);
|
const strength: PartyMemberStrength = template.getStrength(index);
|
||||||
|
|
||||||
|
@ -440,6 +452,11 @@ export default class Trainer extends Phaser.GameObjects.Container {
|
||||||
|
|
||||||
const party = this.scene.getEnemyParty();
|
const party = this.scene.getEnemyParty();
|
||||||
const nonFaintedLegalPartyMembers = party.slice(this.scene.currentBattle.getBattlerCount()).filter(p => p.isAllowedInBattle()).filter(p => !trainerSlot || p.trainerSlot === trainerSlot);
|
const nonFaintedLegalPartyMembers = party.slice(this.scene.currentBattle.getBattlerCount()).filter(p => p.isAllowedInBattle()).filter(p => !trainerSlot || p.trainerSlot === trainerSlot);
|
||||||
|
|
||||||
|
if (Overrides.TRAINER_ALWAYS_SWITCHES_OVERRIDE) {
|
||||||
|
return nonFaintedLegalPartyMembers.map(p => [party.indexOf(p), 100]);
|
||||||
|
}
|
||||||
|
|
||||||
const partyMemberScores = nonFaintedLegalPartyMembers.map(p => {
|
const partyMemberScores = nonFaintedLegalPartyMembers.map(p => {
|
||||||
const playerField = this.scene.getPlayerField().filter(p => p.isAllowedInBattle());
|
const playerField = this.scene.getPlayerField().filter(p => p.isAllowedInBattle());
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
"forest": "PMD EoS Dusk Forest",
|
"forest": "PMD EoS Dusk Forest",
|
||||||
"grass": "PMD EoS Apple Woods",
|
"grass": "PMD EoS Apple Woods",
|
||||||
"graveyard": "PMD EoS Mystifying Forest",
|
"graveyard": "PMD EoS Mystifying Forest",
|
||||||
"ice_cave": "PMD EoS Vast Ice Mountain",
|
"ice_cave": "Firel - -60F",
|
||||||
"island": "PMD EoS Craggy Coast",
|
"island": "PMD EoS Craggy Coast",
|
||||||
"jungle": "Lmz - Jungle",
|
"jungle": "Lmz - Jungle",
|
||||||
"laboratory": "Firel - Laboratory",
|
"laboratory": "Firel - Laboratory",
|
||||||
|
@ -116,8 +116,8 @@
|
||||||
"meadow": "PMD EoS Sky Peak Forest",
|
"meadow": "PMD EoS Sky Peak Forest",
|
||||||
"metropolis": "Firel - Metropolis",
|
"metropolis": "Firel - Metropolis",
|
||||||
"mountain": "PMD EoS Mt. Horn",
|
"mountain": "PMD EoS Mt. Horn",
|
||||||
"plains": "PMD EoS Sky Peak Prairie",
|
"plains": "Firel - Route 888",
|
||||||
"power_plant": "PMD EoS Far Amp Plains",
|
"power_plant": "Firel - The Klink",
|
||||||
"ruins": "PMD EoS Deep Sealed Ruin",
|
"ruins": "PMD EoS Deep Sealed Ruin",
|
||||||
"sea": "Andr06 - Marine Mystique",
|
"sea": "Andr06 - Marine Mystique",
|
||||||
"seabed": "Firel - Seabed",
|
"seabed": "Firel - Seabed",
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
"tall_grass": "PMD EoS Foggy Forest",
|
"tall_grass": "PMD EoS Foggy Forest",
|
||||||
"temple": "PMD EoS Aegis Cave",
|
"temple": "PMD EoS Aegis Cave",
|
||||||
"town": "PMD EoS Random Dungeon Theme 3",
|
"town": "PMD EoS Random Dungeon Theme 3",
|
||||||
"volcano": "PMD EoS Steam Cave",
|
"volcano": "Firel - Twisturn Volcano",
|
||||||
"wasteland": "PMD EoS Hidden Highland",
|
"wasteland": "PMD EoS Hidden Highland",
|
||||||
"encounter_ace_trainer": "BW Trainers' Eyes Meet (Ace Trainer)",
|
"encounter_ace_trainer": "BW Trainers' Eyes Meet (Ace Trainer)",
|
||||||
"encounter_backpacker": "BW Trainers' Eyes Meet (Backpacker)",
|
"encounter_backpacker": "BW Trainers' Eyes Meet (Backpacker)",
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
"ALL": "전부",
|
"ALL": "전부",
|
||||||
"PASS_BATON": "배턴터치한다",
|
"PASS_BATON": "배턴터치한다",
|
||||||
"UNPAUSE_EVOLUTION": "진화 재개",
|
"UNPAUSE_EVOLUTION": "진화 재개",
|
||||||
|
"PAUSE_EVOLUTION": "진화 중지",
|
||||||
"REVIVE": "되살린다",
|
"REVIVE": "되살린다",
|
||||||
"RENAME": "닉네임 바꾸기",
|
"RENAME": "닉네임 바꾸기",
|
||||||
|
"SELECT": "선택한다",
|
||||||
"choosePokemon": "포켓몬을 선택하세요.",
|
"choosePokemon": "포켓몬을 선택하세요.",
|
||||||
"doWhatWithThisPokemon": "포켓몬을 어떻게 하겠습니까?",
|
"doWhatWithThisPokemon": "포켓몬을 어떻게 하겠습니까?",
|
||||||
"noEnergy": "{{pokemonName}}[[는]] 싸울 수 있는\n기력이 남아 있지 않습니다!",
|
"noEnergy": "{{pokemonName}}[[는]] 싸울 수 있는\n기력이 남아 있지 않습니다!",
|
||||||
|
@ -23,6 +25,7 @@
|
||||||
"tooManyItems": "{{pokemonName}}[[는]] 지닌 도구의 수가\n너무 많습니다",
|
"tooManyItems": "{{pokemonName}}[[는]] 지닌 도구의 수가\n너무 많습니다",
|
||||||
"anyEffect": "써도 효과가 없다.",
|
"anyEffect": "써도 효과가 없다.",
|
||||||
"unpausedEvolutions": "{{pokemonName}}의 진화가 재개되었다.",
|
"unpausedEvolutions": "{{pokemonName}}의 진화가 재개되었다.",
|
||||||
|
"pausedEvolutions": "{{pokemonName}}[[가]] 진화하지 않도록 했다.",
|
||||||
"unspliceConfirmation": "{{pokemonName}}로부터 {{fusionName}}의 융합을 해제하시겠습니까?\n{{fusionName}}는 사라지게 됩니다.",
|
"unspliceConfirmation": "{{pokemonName}}로부터 {{fusionName}}의 융합을 해제하시겠습니까?\n{{fusionName}}는 사라지게 됩니다.",
|
||||||
"wasReverted": "{{fusionName}}은 {{pokemonName}}의 모습으로 돌아갔습니다!",
|
"wasReverted": "{{fusionName}}은 {{pokemonName}}의 모습으로 돌아갔습니다!",
|
||||||
"releaseConfirmation": "{{pokemonName}}[[를]]\n정말 놓아주겠습니까?",
|
"releaseConfirmation": "{{pokemonName}}[[를]]\n정말 놓아주겠습니까?",
|
||||||
|
|
|
@ -11,6 +11,10 @@
|
||||||
"expGainsSpeed": "경험치 획득 속도",
|
"expGainsSpeed": "경험치 획득 속도",
|
||||||
"expPartyDisplay": "파티 경험치 표시",
|
"expPartyDisplay": "파티 경험치 표시",
|
||||||
"skipSeenDialogues": "본 대화 생략",
|
"skipSeenDialogues": "본 대화 생략",
|
||||||
|
"eggSkip": "알 스킵",
|
||||||
|
"never": "안 함",
|
||||||
|
"always": "항상",
|
||||||
|
"ask": "확인하기",
|
||||||
"battleStyle": "시합 룰",
|
"battleStyle": "시합 룰",
|
||||||
"enableRetries": "재도전 허용",
|
"enableRetries": "재도전 허용",
|
||||||
"hideIvs": "개체값탐지기 효과 끄기",
|
"hideIvs": "개체값탐지기 효과 끄기",
|
||||||
|
|
|
@ -128,6 +128,25 @@ class DefaultOverrides {
|
||||||
*/
|
*/
|
||||||
readonly OPP_HEALTH_SEGMENTS_OVERRIDE: number = 0;
|
readonly OPP_HEALTH_SEGMENTS_OVERRIDE: number = 0;
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// TRAINER/AI OVERRIDES
|
||||||
|
// --------------------------
|
||||||
|
/**
|
||||||
|
* Force enemy AI to always switch pkmn
|
||||||
|
*/
|
||||||
|
readonly TRAINER_ALWAYS_SWITCHES_OVERRIDE: boolean = false;
|
||||||
|
/**
|
||||||
|
* Force enemy trainer battles to always pick pkmn in this order. If the
|
||||||
|
* trainer would have more Pokemon than in the array, it will wrap around to
|
||||||
|
* the beginning. If the trainer would have less Pokemon than in the array,
|
||||||
|
* it will ignore the extras.
|
||||||
|
*
|
||||||
|
* Has no effect on wild battles. Only affects newly generated trainers (eg,
|
||||||
|
* won't work on a saved trainer wave). OPP_SPECIES_OVERRIDE and other OPP_
|
||||||
|
* overrides will supercede this value.
|
||||||
|
*/
|
||||||
|
readonly TRAINER_PARTY_OVERRIDE: Species[] = [];
|
||||||
|
|
||||||
// -------------
|
// -------------
|
||||||
// EGG OVERRIDES
|
// EGG OVERRIDES
|
||||||
// -------------
|
// -------------
|
||||||
|
|
|
@ -50,7 +50,7 @@ export class CheckSwitchPhase extends BattlePhase {
|
||||||
this.scene.ui.setMode(Mode.CONFIRM, () => {
|
this.scene.ui.setMode(Mode.CONFIRM, () => {
|
||||||
this.scene.ui.setMode(Mode.MESSAGE);
|
this.scene.ui.setMode(Mode.MESSAGE);
|
||||||
this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex);
|
this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex);
|
||||||
this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, false, true));
|
this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, "switchMode", true));
|
||||||
this.end();
|
this.end();
|
||||||
}, () => {
|
}, () => {
|
||||||
this.scene.ui.setMode(Mode.MESSAGE);
|
this.scene.ui.setMode(Mode.MESSAGE);
|
||||||
|
|
|
@ -106,7 +106,7 @@ export class FaintPhase extends PokemonPhase {
|
||||||
* If previous conditions weren't met, and the player has at least 1 legal Pokemon off the field,
|
* If previous conditions weren't met, and the player has at least 1 legal Pokemon off the field,
|
||||||
* push a phase that prompts the player to summon a Pokemon from their party.
|
* push a phase that prompts the player to summon a Pokemon from their party.
|
||||||
*/
|
*/
|
||||||
this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false));
|
this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, "faint", false));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex));
|
this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex));
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
|
||||||
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
||||||
import { MoveAnim } from "#app/data/battle-anims";
|
import { MoveAnim } from "#app/data/battle-anims";
|
||||||
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
|
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
|
||||||
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move";
|
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, DealsDoubleDamageToTagAttr } from "#app/data/move";
|
||||||
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
||||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||||
import { Moves } from "#app/enums/moves";
|
import { Moves } from "#app/enums/moves";
|
||||||
|
@ -394,7 +394,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||||
}
|
}
|
||||||
|
|
||||||
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
|
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
|
||||||
if (semiInvulnerableTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) {
|
if (semiInvulnerableTag && !this.move.getMove().getAttrs(DealsDoubleDamageToTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,9 +95,11 @@ export class MovePhase extends BattlePhase {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//Check if this move is immune to being redirected, and restore its target to the intended target if it is.
|
//Check if this move is immune to being redirected, and restore its target to the intended target if it is.
|
||||||
if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().hasAttr(BypassRedirectAttr))) {
|
const abilityRedirectImmune = this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr);
|
||||||
|
const moveRedirectImmune = this.move.getMove().getAttrs(BypassRedirectAttr).some(attr => attr.apply(this.pokemon, this.scene.getField(false)[oldTarget], this.move.getMove()));
|
||||||
|
if (abilityRedirectImmune || moveRedirectImmune) {
|
||||||
//If an ability prevented this move from being redirected, display its ability pop up.
|
//If an ability prevented this move from being redirected, display its ability pop up.
|
||||||
if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().hasAttr(BypassRedirectAttr)) && oldTarget !== moveTarget.value) {
|
if (abilityRedirectImmune && !moveRedirectImmune && oldTarget !== moveTarget.value) {
|
||||||
this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr)));
|
this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr)));
|
||||||
}
|
}
|
||||||
moveTarget.value = oldTarget;
|
moveTarget.value = oldTarget;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Pokemon from "#app/field/pokemon";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { ResetNegativeStatStageModifier } from "#app/modifier/modifier";
|
import { ResetNegativeStatStageModifier } from "#app/modifier/modifier";
|
||||||
import { handleTutorial, Tutorial } from "#app/tutorial";
|
import { handleTutorial, Tutorial } from "#app/tutorial";
|
||||||
import * as Utils from "#app/utils";
|
import { NumberHolder, BooleanHolder } from "#app/utils";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { PokemonPhase } from "./pokemon-phase";
|
import { PokemonPhase } from "./pokemon-phase";
|
||||||
import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
|
import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
|
||||||
|
@ -42,17 +42,23 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||||
return this.end();
|
return this.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stages = new NumberHolder(this.stages);
|
||||||
|
|
||||||
|
if (!this.ignoreAbilities) {
|
||||||
|
applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages);
|
||||||
|
}
|
||||||
|
|
||||||
let simulate = false;
|
let simulate = false;
|
||||||
|
|
||||||
const filteredStats = this.stats.filter(stat => {
|
const filteredStats = this.stats.filter(stat => {
|
||||||
const cancelled = new Utils.BooleanHolder(false);
|
const cancelled = new BooleanHolder(false);
|
||||||
|
|
||||||
if (!this.selfTarget && this.stages < 0) {
|
if (!this.selfTarget && stages.value < 0) {
|
||||||
// TODO: Include simulate boolean when tag applications can be simulated
|
// TODO: Include simulate boolean when tag applications can be simulated
|
||||||
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
|
this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cancelled.value && !this.selfTarget && this.stages < 0) {
|
if (!cancelled.value && !this.selfTarget && stages.value < 0) {
|
||||||
applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate);
|
applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,12 +70,6 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||||
return !cancelled.value;
|
return !cancelled.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const stages = new Utils.IntegerHolder(this.stages);
|
|
||||||
|
|
||||||
if (!this.ignoreAbilities) {
|
|
||||||
applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s));
|
const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s));
|
||||||
|
|
||||||
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
|
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
|
||||||
|
|
|
@ -10,52 +10,56 @@ import { SwitchSummonPhase } from "./switch-summon-phase";
|
||||||
*/
|
*/
|
||||||
export class SwitchPhase extends BattlePhase {
|
export class SwitchPhase extends BattlePhase {
|
||||||
protected fieldIndex: integer;
|
protected fieldIndex: integer;
|
||||||
private isModal: boolean;
|
private switchReason: "faint" | "moveEffect" | "switchMode";
|
||||||
private doReturn: boolean;
|
private doReturn: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new SwitchPhase
|
* Creates a new SwitchPhase
|
||||||
* @param scene {@linkcode BattleScene} Current battle scene
|
* @param scene {@linkcode BattleScene} Current battle scene
|
||||||
* @param fieldIndex Field index to switch out
|
* @param fieldIndex Field index to switch out
|
||||||
* @param isModal Indicates if the switch should be forced (true) or is
|
* @param switchReason Indicates why this switch is occurring. The valid options are
|
||||||
* optional (false).
|
* `'faint'` (party member fainted), `'moveEffect'` (uturn, baton pass, dragon
|
||||||
* @param doReturn Indicates if the party member on the field should be
|
* tail, etc), and `'switchMode'` (start-of-battle optional switch). This
|
||||||
* recalled to ball or has already left the field. Passed to {@linkcode SwitchSummonPhase}.
|
* helps the phase determine both if the switch should be cancellable by the
|
||||||
|
* user, as well as determine if the party UI should be shown at all.
|
||||||
|
* @param doReturn Indicates if this switch should call back the pokemon at
|
||||||
|
* the {@linkcode fieldIndex} (true), or if the mon has already been recalled
|
||||||
|
* (false).
|
||||||
*/
|
*/
|
||||||
constructor(scene: BattleScene, fieldIndex: integer, isModal: boolean, doReturn: boolean) {
|
constructor(scene: BattleScene, fieldIndex: integer, switchReason: "faint" | "moveEffect" | "switchMode", doReturn: boolean) {
|
||||||
super(scene);
|
super(scene);
|
||||||
|
|
||||||
this.fieldIndex = fieldIndex;
|
this.fieldIndex = fieldIndex;
|
||||||
this.isModal = isModal;
|
this.switchReason = switchReason;
|
||||||
this.doReturn = doReturn;
|
this.doReturn = doReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
// Skip modal switch if impossible (no remaining party members that aren't in battle)
|
const isForcedSwitch = this.switchReason !== "switchMode";
|
||||||
if (this.isModal && !this.scene.getParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) {
|
|
||||||
|
// Skip forced switch if impossible (no remaining party members that aren't in battle)
|
||||||
|
if (isForcedSwitch && !this.scene.getParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) {
|
||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if the fainted party member has been revived already. doReturn is
|
// Skip if the fainted party member has been revived already. see also; battle.test.ts
|
||||||
// only passed as `false` from FaintPhase (as opposed to other usages such
|
if (this.switchReason === "faint" && !this.scene.getParty()[this.fieldIndex].isFainted()) {
|
||||||
// as ForceSwitchOutAttr or CheckSwitchPhase), so we only want to check this
|
|
||||||
// if the mon should have already been returned but is still alive and well
|
|
||||||
// on the field. see also; battle.test.ts
|
|
||||||
if (this.isModal && !this.doReturn && !this.scene.getParty()[this.fieldIndex].isFainted()) {
|
|
||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is any space still in field
|
// Check if there is any space still in field
|
||||||
if (this.isModal && this.scene.getPlayerField().filter(p => p.isAllowedInBattle() && p.isActive(true)).length >= this.scene.currentBattle.getBattlerCount()) {
|
const numActiveBattlers = this.scene.getPlayerField().filter(p => p.isAllowedInBattle() && p.isActive(true)).length;
|
||||||
|
const willReturnModifer = (this.doReturn ? 1 : 0); // need to subtract this if doReturn is true, because the pokemon in the given index hasn't left the field yet. (used for volt switch + pursuit, etc)
|
||||||
|
if (isForcedSwitch && numActiveBattlers - willReturnModifer >= this.scene.currentBattle.getBattlerCount()) {
|
||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override field index to 0 in case of double battle where 2/3 remaining legal party members fainted at once
|
// Override field index to 0 in case of double battle where 2/3 remaining legal party members fainted at once
|
||||||
const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? this.fieldIndex : 0;
|
const fieldIndex = this.scene.currentBattle.getBattlerCount() === 1 || this.scene.getParty().filter(p => p.isAllowedInBattle()).length > 1 ? this.fieldIndex : 0;
|
||||||
|
|
||||||
this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => {
|
this.scene.ui.setMode(Mode.PARTY, isForcedSwitch ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => {
|
||||||
if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) {
|
if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) {
|
||||||
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, fieldIndex, slotIndex, this.doReturn, option === PartyOption.PASS_BATON));
|
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, fieldIndex, slotIndex, this.doReturn, option === PartyOption.PASS_BATON));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
import BattleScene from "#app/battle-scene";
|
import BattleScene from "#app/battle-scene";
|
||||||
import { applyAbAttrs, BypassSpeedChanceAbAttr, PreventBypassSpeedChanceAbAttr, ChangeMovePriorityAbAttr } from "#app/data/ability";
|
import { applyAbAttrs, BypassSpeedChanceAbAttr, ChangeMovePriorityAbAttr, PreventBypassSpeedChanceAbAttr } from "#app/data/ability";
|
||||||
|
import { TrickRoomTag } from "#app/data/arena-tag";
|
||||||
import { allMoves, applyMoveAttrs, IncrementMovePriorityAttr, MoveHeaderAttr } from "#app/data/move";
|
import { allMoves, applyMoveAttrs, IncrementMovePriorityAttr, MoveHeaderAttr } from "#app/data/move";
|
||||||
import { Abilities } from "#app/enums/abilities";
|
import { Abilities } from "#app/enums/abilities";
|
||||||
|
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||||
|
import { Moves } from "#app/enums/moves";
|
||||||
import { Stat } from "#app/enums/stat";
|
import { Stat } from "#app/enums/stat";
|
||||||
import Pokemon, { PokemonMove } from "#app/field/pokemon";
|
import Pokemon, { PokemonMove } from "#app/field/pokemon";
|
||||||
import { BypassSpeedChanceModifier } from "#app/modifier/modifier";
|
import { BypassSpeedChanceModifier } from "#app/modifier/modifier";
|
||||||
|
@ -17,8 +21,6 @@ import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase";
|
||||||
import { SwitchSummonPhase } from "./switch-summon-phase";
|
import { SwitchSummonPhase } from "./switch-summon-phase";
|
||||||
import { TurnEndPhase } from "./turn-end-phase";
|
import { TurnEndPhase } from "./turn-end-phase";
|
||||||
import { WeatherEffectPhase } from "./weather-effect-phase";
|
import { WeatherEffectPhase } from "./weather-effect-phase";
|
||||||
import { BattlerIndex } from "#app/battle";
|
|
||||||
import { TrickRoomTag } from "#app/data/arena-tag";
|
|
||||||
|
|
||||||
export class TurnStartPhase extends FieldPhase {
|
export class TurnStartPhase extends FieldPhase {
|
||||||
constructor(scene: BattleScene) {
|
constructor(scene: BattleScene) {
|
||||||
|
@ -87,14 +89,22 @@ export class TurnStartPhase extends FieldPhase {
|
||||||
// Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands.
|
// Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands.
|
||||||
moveOrder = moveOrder.slice(0);
|
moveOrder = moveOrder.slice(0);
|
||||||
moveOrder.sort((a, b) => {
|
moveOrder.sort((a, b) => {
|
||||||
const aCommand = this.scene.currentBattle.turnCommands[a];
|
const aCommand = this.scene.currentBattle.turnCommands[a]!;
|
||||||
const bCommand = this.scene.currentBattle.turnCommands[b];
|
const bCommand = this.scene.currentBattle.turnCommands[b]!;
|
||||||
|
|
||||||
if (aCommand?.command !== bCommand?.command) {
|
if (aCommand.command !== bCommand.command) {
|
||||||
if (aCommand?.command === Command.FIGHT) {
|
if (aCommand.command === Command.FIGHT) {
|
||||||
return 1;
|
if (aCommand.move?.move === Moves.PURSUIT && bCommand.command === Command.POKEMON) {
|
||||||
} else if (bCommand?.command === Command.FIGHT) {
|
|
||||||
return -1;
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else if (bCommand.command === Command.FIGHT) {
|
||||||
|
if (bCommand.move?.move === Moves.PURSUIT && aCommand.command === Command.POKEMON) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (aCommand?.command === Command.FIGHT) {
|
} else if (aCommand?.command === Command.FIGHT) {
|
||||||
const aMove = allMoves[aCommand.move!.move];
|
const aMove = allMoves[aCommand.move!.move];
|
||||||
|
@ -160,26 +170,54 @@ export class TurnStartPhase extends FieldPhase {
|
||||||
if (!queuedMove) {
|
if (!queuedMove) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const move = pokemon.getMoveset().find(m => m?.moveId === queuedMove.move) || new PokemonMove(queuedMove.move);
|
const move = pokemon.getMoveset().find(m => m?.moveId === queuedMove.move) ?? new PokemonMove(queuedMove.move);
|
||||||
if (move.getMove().hasAttr(MoveHeaderAttr)) {
|
if (move.getMove().hasAttr(MoveHeaderAttr)) {
|
||||||
this.scene.unshiftPhase(new MoveHeaderPhase(this.scene, pokemon, move));
|
this.scene.unshiftPhase(new MoveHeaderPhase(this.scene, pokemon, move));
|
||||||
}
|
}
|
||||||
|
// even though pursuit is ordered before Pokemon commands in the move
|
||||||
|
// order, the SwitchSummonPhase is unshifted onto the phase list, which
|
||||||
|
// would cause it to run before pursuit if pursuit was pushed normally.
|
||||||
|
// the SwitchSummonPhase can't be changed to a push either, because then
|
||||||
|
// the MoveHeaderPhase for all moves would run prior to the switch-out,
|
||||||
|
// which is not correct (eg, when focus punching a switching opponent,
|
||||||
|
// the correct order is switch -> tightening focus message -> attack
|
||||||
|
// fires, not focus -> switch -> attack). so, we have to specifically
|
||||||
|
// unshift pursuit when there are other pokemon commands after it, as
|
||||||
|
// well as order it before any Pokemon commands, otherwise it won't go first.
|
||||||
|
const remainingMoves = moveOrder.slice(moveOrder.findIndex(mo => mo === o) + 1);
|
||||||
|
const pendingOpposingPokemonCommands = remainingMoves.filter(o =>
|
||||||
|
this.scene.currentBattle.turnCommands[o]!.command === Command.POKEMON
|
||||||
|
&& (pokemon.isPlayer() ? o >= BattlerIndex.ENEMY : o < BattlerIndex.ENEMY)
|
||||||
|
);
|
||||||
|
const arePokemonCommandsLeftInQueue = Boolean(pendingOpposingPokemonCommands.length);
|
||||||
|
const addPhase = (
|
||||||
|
queuedMove.move === Moves.PURSUIT && arePokemonCommandsLeftInQueue
|
||||||
|
? this.scene.unshiftPhase
|
||||||
|
: this.scene.pushPhase
|
||||||
|
).bind(this.scene);
|
||||||
|
|
||||||
|
// pursuit also hits the first pokemon to switch out in doubles,
|
||||||
|
// regardless of original target
|
||||||
|
const targets = queuedMove.move === Moves.PURSUIT && arePokemonCommandsLeftInQueue
|
||||||
|
? [pendingOpposingPokemonCommands[0]]
|
||||||
|
: turnCommand.targets || turnCommand.move!.targets;
|
||||||
if (pokemon.isPlayer()) {
|
if (pokemon.isPlayer()) {
|
||||||
if (turnCommand.cursor === -1) {
|
if (turnCommand.cursor === -1) {
|
||||||
this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move));//TODO: is the bang correct here?
|
addPhase(new MovePhase(this.scene, pokemon, targets, move));
|
||||||
} else {
|
} else {
|
||||||
const playerPhase = new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move, false, queuedMove.ignorePP);//TODO: is the bang correct here?
|
const playerPhase = new MovePhase(this.scene, pokemon, targets, move, false, queuedMove.ignorePP);
|
||||||
this.scene.pushPhase(playerPhase);
|
addPhase(playerPhase);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move!.targets, move, false, queuedMove.ignorePP));//TODO: is the bang correct here?
|
addPhase(new MovePhase(this.scene, pokemon, targets, move, false, queuedMove.ignorePP));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Command.BALL:
|
case Command.BALL:
|
||||||
this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets![0] % 2, turnCommand.cursor!));//TODO: is the bang correct here?
|
this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets![0] % 2, turnCommand.cursor!));//TODO: is the bang correct here?
|
||||||
break;
|
break;
|
||||||
case Command.POKEMON:
|
case Command.POKEMON:
|
||||||
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor!, true, turnCommand.args![0] as boolean, pokemon.isPlayer()));//TODO: is the bang correct here?
|
pokemon.addTag(BattlerTagType.ESCAPING);
|
||||||
|
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor!, true, turnCommand.args![0] as boolean, pokemon.isPlayer()));
|
||||||
break;
|
break;
|
||||||
case Command.RUN:
|
case Command.RUN:
|
||||||
let runningPokemon = pokemon;
|
let runningPokemon = pokemon;
|
||||||
|
|
|
@ -31,7 +31,7 @@ describe("Abilities - Contrary", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should invert stat changes when applied", async() => {
|
it("should invert stat changes when applied", async() => {
|
||||||
await game.startBattle([
|
await game.classicMode.startBattle([
|
||||||
Species.SLOWBRO
|
Species.SLOWBRO
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -39,4 +39,39 @@ describe("Abilities - Contrary", () => {
|
||||||
|
|
||||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
|
describe("With Clear Body", () => {
|
||||||
|
it("should apply positive effects", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyPassiveAbility(Abilities.CLEAR_BODY)
|
||||||
|
.moveset([Moves.TAIL_WHIP]);
|
||||||
|
await game.classicMode.startBattle([Species.SLOWBRO]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||||
|
|
||||||
|
game.move.select(Moves.TAIL_WHIP);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block negative effects", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyPassiveAbility(Abilities.CLEAR_BODY)
|
||||||
|
.enemyMoveset([Moves.HOWL, Moves.HOWL, Moves.HOWL, Moves.HOWL])
|
||||||
|
.moveset([Moves.SPLASH]);
|
||||||
|
await game.classicMode.startBattle([Species.SLOWBRO]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,834 @@
|
||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { allMoves } from "#app/data/move";
|
||||||
|
import { TerrainType } from "#app/data/terrain";
|
||||||
|
import { Abilities } from "#app/enums/abilities";
|
||||||
|
import { Moves } from "#app/enums/moves";
|
||||||
|
import { Species } from "#app/enums/species";
|
||||||
|
import { Stat } from "#app/enums/stat";
|
||||||
|
import Pokemon, { MoveResult } from "#app/field/pokemon";
|
||||||
|
import { EncounterPhase } from "#app/phases/encounter-phase";
|
||||||
|
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
|
||||||
|
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||||
|
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
interface PokemonAssertionChainer {
|
||||||
|
and(expectation: (p?: Pokemon) => PokemonAssertionChainer): PokemonAssertionChainer;
|
||||||
|
and(expectation: (p?: Pokemon) => void): void
|
||||||
|
}
|
||||||
|
|
||||||
|
function chain(pokemon?: Pokemon): PokemonAssertionChainer {
|
||||||
|
return {
|
||||||
|
and: (expectation) => {
|
||||||
|
return expectation(pokemon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArray<T>(a?: T | T[]) {
|
||||||
|
return Array.isArray(a) ? a : [a!];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Moves - Pursuit", { timeout: 10000 }, () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
const pursuitMoveDef = allMoves[Moves.PURSUIT];
|
||||||
|
|
||||||
|
const playerLead = Species.MUDSDALE;
|
||||||
|
const enemyLead = Species.KANGASKHAN;
|
||||||
|
|
||||||
|
function startBattle() {
|
||||||
|
return game.startBattle([playerLead, Species.RAICHU, Species.ABSOL]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Many of these tests can't run only to {@linkcode BerryPhase} because of interactions with fainting
|
||||||
|
* during switches. We need to run tests all the way through to the start of the next turn
|
||||||
|
* to ensure that, for example, the game doesn't attempt to switch two pokemon into the same
|
||||||
|
* slot (once in the originally queued summon phase, and the next as a result of {@linkcode FaintPhase})
|
||||||
|
* @param to Phase to run to (default {@linkcode TurnInitPhase})
|
||||||
|
*/
|
||||||
|
async function runCombatTurn(to: string = "TurnInitPhase"): Promise<void> {
|
||||||
|
return await game.phaseInterceptor.to(to, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterFirstSwitch(action: () => void) {
|
||||||
|
game.phaseInterceptor.onNextPhase(EnemyCommandPhase, () =>
|
||||||
|
game.phaseInterceptor.advance().then(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
function playerDoesNothing() {
|
||||||
|
game.override.moveset(Moves.SPLASH);
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
if (game.scene.currentBattle.double) {
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playerUsesPursuit(pokemonIndex: 0 | 1 = 0, targetIndex: BattlerIndex = BattlerIndex.ENEMY) {
|
||||||
|
game.move.select(Moves.PURSUIT, pokemonIndex, targetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playerUsesSwitchMove(pokemonIndex: 0 | 1 = 0) {
|
||||||
|
game.move.select(Moves.U_TURN, pokemonIndex);
|
||||||
|
game.doSelectPartyPokemon(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playerSwitches(pokemonIndex: number = 1) {
|
||||||
|
game.doSwitchPokemon(pokemonIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enemyUses(move: Moves) {
|
||||||
|
game.override.enemyMoveset(move);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enemySwitches(once?: boolean) {
|
||||||
|
game.override.forceTrainerSwitches();
|
||||||
|
if (once) {
|
||||||
|
afterFirstSwitch(() => game.override.forceTrainerSwitches(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceMovesLast(pokemon?: Pokemon | Pokemon[]) {
|
||||||
|
pokemon = toArray(pokemon);
|
||||||
|
const otherPkmn = game.scene.getField().filter(p => p && !pokemon.find(p1 => p1 === p));
|
||||||
|
const moveOrder = ([...otherPkmn, ...pokemon].map(pkmn => pkmn.getBattlerIndex()));
|
||||||
|
|
||||||
|
game.phaseInterceptor.onNextPhase(TurnStartPhase, p => {
|
||||||
|
vi.spyOn(p, "getCommandOrder").mockReturnValue(
|
||||||
|
// TurnStartPhase crashes if a BI returned by getOrder() is fainted.
|
||||||
|
// not an issue normally but some of the test setups can cause this
|
||||||
|
moveOrder!.filter(i => game.scene.getField(false)[i]?.isActive(true))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceMovesFirst(pokemon?: Pokemon | Pokemon[]) {
|
||||||
|
pokemon = toArray(pokemon);
|
||||||
|
const otherPkmn = game.scene.getField().filter(p => p && !pokemon.find(p1 => p1 === p));
|
||||||
|
const moveOrder = ([...pokemon, ...otherPkmn].map(pkmn => pkmn.getBattlerIndex()));
|
||||||
|
|
||||||
|
game.phaseInterceptor.onNextPhase(TurnStartPhase, p => {
|
||||||
|
vi.spyOn(p, "getCommandOrder").mockReturnValue(
|
||||||
|
// TurnStartPhase crashes if a BI returned by getOrder() is fainted.
|
||||||
|
// not an issue normally but some of the test setups can cause this
|
||||||
|
moveOrder!.filter(i => game.scene.getField(false)[i]?.isActive(true))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceLowestPriorityBracket() {
|
||||||
|
vi.spyOn(pursuitMoveDef, "priority", "get").mockReturnValue(-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectPursuitPowerDoubled() {
|
||||||
|
expect(pursuitMoveDef.calculateBattlePower).toHaveReturnedWith(80);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectPursuitPowerUnchanged() {
|
||||||
|
expect(pursuitMoveDef.calculateBattlePower).toHaveReturnedWith(40);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectPursuitSucceeded(pokemon?: Pokemon) {
|
||||||
|
const lastMove = pokemon!.getLastXMoves(0)[0];
|
||||||
|
expect(lastMove.move).toBe(Moves.PURSUIT);
|
||||||
|
expect(lastMove.result).toBe(MoveResult.SUCCESS);
|
||||||
|
return chain(pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectPursuitFailed(pokemon?: Pokemon) {
|
||||||
|
const lastMove = pokemon!.getLastXMoves(0)[0];
|
||||||
|
expect(lastMove.result).toBe(MoveResult.FAIL);
|
||||||
|
return chain(pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectWasHit(pokemon?: Pokemon) {
|
||||||
|
expect(pokemon!.hp).toBeLessThan(pokemon!.getMaxHp());
|
||||||
|
return chain(pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectWasNotHit(pokemon?: Pokemon) {
|
||||||
|
expect(pokemon!.hp).toBe(pokemon!.getMaxHp());
|
||||||
|
return chain(pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectOnField(pokemon?: Pokemon) {
|
||||||
|
expect(pokemon!.isOnField()).toBe(true);
|
||||||
|
return chain(pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectNotOnField(pokemon?: Pokemon) {
|
||||||
|
expect(pokemon!.isOnField()).toBe(false);
|
||||||
|
return chain(pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectHasFled(pokemon?: Pokemon) {
|
||||||
|
expect(pokemon!.wildFlee).toBe(true);
|
||||||
|
return chain(pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectIsSpecies(species: Species) {
|
||||||
|
return (pokemon?: Pokemon) => {
|
||||||
|
expect(pokemon!.species.speciesId).toBe(species);
|
||||||
|
return chain(pokemon);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPartyMember(party: Pokemon[], species: Species) {
|
||||||
|
return party.find(pkmn => pkmn.species.speciesId === species);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.battleType("single")
|
||||||
|
.enemyParty([enemyLead, Species.SNORLAX, Species.BASCULIN, Species.ALCREMIE])
|
||||||
|
.startingLevel(20)
|
||||||
|
.startingWave(25)
|
||||||
|
.moveset([Moves.PURSUIT, Moves.U_TURN, Moves.DRAGON_TAIL, Moves.FOLLOW_ME])
|
||||||
|
.enemyMoveset(Moves.SPLASH)
|
||||||
|
.disableCrits();
|
||||||
|
|
||||||
|
vi.spyOn(pursuitMoveDef, "calculateBattlePower");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hit for normal power if the target is not switching", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasHit(game.scene.getEnemyPokemon());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit a uturning target for double power if the pursuiter moves before the uturner", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getEnemyPokemon());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.U_TURN);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasNotHit(game.scene.getEnemyPokemon());
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead))
|
||||||
|
.and(expectNotOnField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hit a hard-switching target for double power (player attacks, enemy switches)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemySwitches();
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasNotHit(game.scene.getEnemyPokemon());
|
||||||
|
expectNotOnField(findPartyMember(game.scene.getEnemyParty(), enemyLead))
|
||||||
|
.and(expectWasHit);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hit a hard-switching target for double power (player switches, enemy attacks)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerSwitches();
|
||||||
|
enemyUses(Moves.PURSUIT);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasNotHit(game.scene.getPlayerPokemon());
|
||||||
|
expectNotOnField(findPartyMember(game.scene.getParty(), playerLead))
|
||||||
|
.and(expectWasHit);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hit an outgoing uturning target if pursuiter has not moved yet (player attacks, enemy switches)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.U_TURN);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasNotHit(game.scene.getEnemyPokemon());
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead))
|
||||||
|
.and(expectNotOnField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hit an outgoing uturning target if pursuiter has not moved yet (player switches, enemy attacks)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getEnemyPokemon());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesSwitchMove();
|
||||||
|
enemyUses(Moves.PURSUIT);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasNotHit(game.scene.getPlayerPokemon());
|
||||||
|
expectWasHit(findPartyMember(game.scene.getParty(), playerLead))
|
||||||
|
.and(expectNotOnField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bypass accuracy checks when hitting a hard-switching target", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
game.scene.getPlayerPokemon()!.summonData.statStages[Stat.ACC] = -6;
|
||||||
|
game.scene.getEnemyPokemon()!.summonData.statStages[Stat.EVA] = 6;
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemySwitches();
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bypass accuracy checks when hitting a uturning target", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
game.scene.getEnemyPokemon()!.summonData.statStages[Stat.ACC] = -6;
|
||||||
|
game.scene.getPlayerPokemon()!.summonData.statStages[Stat.EVA] = 6;
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.U_TURN);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead));
|
||||||
|
});
|
||||||
|
|
||||||
|
it.todo("should bypass substitute when hitting an escaping target (hard switch)");
|
||||||
|
|
||||||
|
it.todo("should bypass substitute when hitting an escaping target (switch move)");
|
||||||
|
|
||||||
|
it.todo("should not bypass substitute when hitting a non-escaping target");
|
||||||
|
|
||||||
|
it("should hit a grounded, switching target under Psychic Terrain (switch move)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
game.scene.arena.trySetTerrain(TerrainType.PSYCHIC, false, true);
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.U_TURN);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hit a grounded, switching target under Psychic Terrain (hard-switch)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
game.scene.arena.trySetTerrain(TerrainType.PSYCHIC, false, true);
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemySwitches();
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit a baton pass user", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
game.scene.getEnemyPokemon()!.summonData.statStages[Stat.ACC] = -6;
|
||||||
|
game.scene.getPlayerPokemon()!.summonData.statStages[Stat.EVA] = 6;
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.BATON_PASS);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasHit(game.scene.getEnemyPokemon());
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getEnemyParty(), enemyLead))
|
||||||
|
.and(expectNotOnField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit a teleport user", () => async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
forceLowestPriorityBracket();
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.TELEPORT);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasHit(game.scene.getEnemyPokemon());
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getEnemyParty(), enemyLead))
|
||||||
|
.and(expectNotOnField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit a fleeing wild pokemon", async () => {
|
||||||
|
// arrange
|
||||||
|
game.override
|
||||||
|
.startingWave(24)
|
||||||
|
.disableTrainerWaves();
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.U_TURN);
|
||||||
|
await runCombatTurn("BerryPhase");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitFailed(game.scene.getPlayerPokemon());
|
||||||
|
expectWasNotHit(game.scene.getEnemyParty()[0])
|
||||||
|
.and(expectHasFled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit a switch move user for double damage if the switch move fails and does not switch out the user", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.VOLT_SWITCH);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead))
|
||||||
|
.and(expectOnField);
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getParty(), playerLead));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers contact abilities on the pokemon that is switching out (hard-switch)", async () => {
|
||||||
|
// arrange
|
||||||
|
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemySwitches();
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead));
|
||||||
|
expectWasHit(game.scene.getPlayerPokemon());
|
||||||
|
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers contact abilities on the pokemon that is switching out (switch move, player switching)", async () => {
|
||||||
|
// arrange
|
||||||
|
game.override.ability(Abilities.STAMINA);
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesSwitchMove();
|
||||||
|
enemyUses(Moves.PURSUIT);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers contact abilities on the pokemon that is switching out (switch move, enemy switching)", async () => {
|
||||||
|
// arrange
|
||||||
|
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getEnemyPokemon());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesSwitchMove();
|
||||||
|
enemySwitches();
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not have a pokemon fainted by a switch move pursue its killer", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
game.scene.getPlayerPokemon()!.hp = 1;
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.U_TURN);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(game.scene.getParty()[0]!.isFainted()).toBe(true);
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getEnemyParty(), enemyLead))
|
||||||
|
.and(expectNotOnField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not cause two pokemon to enter the field if a switching-out pokemon is fainted by pursuit (hard-switch, enemy faints)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
game.scene.getEnemyPokemon()!.hp = 1;
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemySwitches();
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(game.scene.getEnemyParty().filter(p => p.isOnField())).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not cause two pokemon to enter the field if a switching-out pokemon is fainted by pursuit (u-turn, enemy faints)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerPokemon());
|
||||||
|
game.scene.getEnemyPokemon()!.hp = 1;
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit();
|
||||||
|
enemyUses(Moves.U_TURN);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(game.scene.getEnemyParty().filter(p => p.isOnField())).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not cause two pokemon to enter the field or a premature switch if a switching-out pokemon is fainted by pursuit (hard-switch, player faints)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getEnemyPokemon());
|
||||||
|
game.scene.getPlayerPokemon()!.hp = 1;
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerSwitches();
|
||||||
|
enemyUses(Moves.PURSUIT);
|
||||||
|
await runCombatTurn("TurnEndPhase");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(game.scene.getParty().filter(p => p.isOnField())).toHaveLength(1);
|
||||||
|
// SwitchPhase for fainted pokemon happens after TurnEndPhase - if we
|
||||||
|
// skipped the uturn switch, we shouldn't have executed a switchout by this point
|
||||||
|
expect(game.scene.getPlayerField()[0]?.isFainted()).toBeTruthy();
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not cause two pokemon to enter the field or a premature switch if a switching-out pokemon is fainted by pursuit (u-turn, player faints)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getEnemyPokemon());
|
||||||
|
game.scene.getPlayerPokemon()!.hp = 1;
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesSwitchMove();
|
||||||
|
enemyUses(Moves.PURSUIT);
|
||||||
|
await runCombatTurn("TurnEndPhase");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(game.scene.getParty().filter(p => p.isOnField())).toHaveLength(1);
|
||||||
|
// SwitchPhase for fainted pokemon happens after TurnEndPhase - if we
|
||||||
|
// skipped the uturn switch, we shouldn't have executed a switchout by this point
|
||||||
|
expect(game.scene.getPlayerField()[0]?.isFainted()).toBeTruthy();
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("doubles interactions", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
game.override
|
||||||
|
.battleType("double")
|
||||||
|
.enemyMoveset([Moves.PURSUIT, Moves.U_TURN]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bypass follow me when hitting a switching target", async () => {
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getEnemyPokemon());
|
||||||
|
|
||||||
|
game.move.select(Moves.FOLLOW_ME);
|
||||||
|
playerUsesSwitchMove(1);
|
||||||
|
game.forceEnemyMove(Moves.PURSUIT, BattlerIndex.PLAYER_2);
|
||||||
|
await game.killPokemon(game.scene.getEnemyField()[1]);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasHit(findPartyMember(game.scene.getParty(), Species.RAICHU))
|
||||||
|
.and(expectNotOnField);
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getParty(), playerLead));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not bypass follow me when hitting a non-switching target", async () => {
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getEnemyPokemon());
|
||||||
|
|
||||||
|
game.override.moveset([Moves.FOLLOW_ME, Moves.SPLASH]);
|
||||||
|
game.move.select(Moves.FOLLOW_ME);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
game.forceEnemyMove(Moves.PURSUIT, BattlerIndex.PLAYER_2);
|
||||||
|
await game.killPokemon(game.scene.getEnemyField()[1]);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasHit(findPartyMember(game.scene.getParty(), playerLead));
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getParty(), Species.RAICHU));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not cause the enemy AI to send out a fainted pokemon if they command 2 switches and one of the outgoing pokemon faints to pursuit", async () => {
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerField());
|
||||||
|
game.scene.getEnemyField()[0]!.hp = 1;
|
||||||
|
|
||||||
|
playerUsesPursuit(0);
|
||||||
|
playerUsesPursuit(1);
|
||||||
|
enemySwitches();
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
expect(game.scene.getEnemyParty().filter(p => p.isOnField())).toHaveLength(2);
|
||||||
|
expect(game.scene.getEnemyParty().filter(p => p.isOnField() && p.isFainted())).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: is this correct behavior?
|
||||||
|
it("should fail if both pokemon use pursuit on a target that is switching out and it faints after the first one with no other targets on field", async () => {
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerField());
|
||||||
|
game.scene.getEnemyField()[0]!.hp = 1;
|
||||||
|
|
||||||
|
playerUsesPursuit(0);
|
||||||
|
playerUsesPursuit(1);
|
||||||
|
enemySwitches();
|
||||||
|
await game.killPokemon(game.scene.getEnemyField()[1]);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectPursuitSucceeded(game.scene.getPlayerField()[0]);
|
||||||
|
expectPursuitFailed(game.scene.getPlayerField()[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: is this correct behavior?
|
||||||
|
it("should attack the second switching pokemon if both pokemon switch and the first is KOd", async () => {
|
||||||
|
game.phaseInterceptor.onNextPhase(EncounterPhase, () => {
|
||||||
|
game.scene.currentBattle.enemyLevels = [...game.scene.currentBattle.enemyLevels!, game.scene.currentBattle.enemyLevels![0]];
|
||||||
|
});
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerField());
|
||||||
|
game.scene.getEnemyField()[0]!.hp = 1;
|
||||||
|
|
||||||
|
playerUsesPursuit(0);
|
||||||
|
playerUsesPursuit(1);
|
||||||
|
enemySwitches();
|
||||||
|
afterFirstSwitch(() => {
|
||||||
|
vi.spyOn(game.scene.currentBattle.trainer!, "getPartyMemberMatchupScores").mockReturnValue([[3, 100]]);
|
||||||
|
vi.spyOn(game.scene.getPlayerPokemon()!, "getMatchupScore").mockReturnValue(0);
|
||||||
|
});
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectPursuitSucceeded(game.scene.getPlayerField()[0]);
|
||||||
|
expectPursuitSucceeded(game.scene.getPlayerField()[1]);
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), Species.SNORLAX));
|
||||||
|
expectNotOnField(findPartyMember(game.scene.getEnemyParty(), enemyLead))
|
||||||
|
.and(p => expect(p?.isFainted()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: confirm correct behavior and add tests for other pursuit/switch combos in doubles
|
||||||
|
|
||||||
|
it("should not hit a pokemon being forced out with dragon tail", async () => {
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerField());
|
||||||
|
|
||||||
|
game.move.select(Moves.DRAGON_TAIL);
|
||||||
|
playerUsesPursuit(1);
|
||||||
|
enemyUses(Moves.SPLASH);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasHit(game.scene.getEnemyPokemon()).and(pkmn => {
|
||||||
|
expect(pkmn?.turnData.attacksReceived[0]).toEqual(expect.objectContaining({
|
||||||
|
move: Moves.PURSUIT,
|
||||||
|
result: MoveResult.SUCCESS,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// fails: command re-ordering does not work due to particulars of sort/move ordering;
|
||||||
|
// pursuit moves after switch
|
||||||
|
it("should hit the first pokemon to switch out in a double battle regardless of who was targeted", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerField());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit(0, BattlerIndex.ENEMY_2);
|
||||||
|
playerUsesPursuit(1, BattlerIndex.ENEMY_2);
|
||||||
|
enemySwitches(true);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead)).and(expectNotOnField);
|
||||||
|
expectWasNotHit(game.scene.getEnemyField()[0]);
|
||||||
|
expectWasNotHit(game.scene.getEnemyField()[1]);
|
||||||
|
expectPursuitSucceeded(game.scene.getPlayerField()[0]);
|
||||||
|
expectPursuitSucceeded(game.scene.getPlayerField()[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit both pokemon in a double battle if both switch out", async () => {
|
||||||
|
// arrange
|
||||||
|
game.phaseInterceptor.onNextPhase(EncounterPhase, () => {
|
||||||
|
game.scene.currentBattle.enemyLevels = [...game.scene.currentBattle.enemyLevels!, game.scene.currentBattle.enemyLevels![0]];
|
||||||
|
});
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerField());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit(0, BattlerIndex.ENEMY);
|
||||||
|
playerUsesPursuit(1, BattlerIndex.ENEMY);
|
||||||
|
enemySwitches();
|
||||||
|
afterFirstSwitch(() => {
|
||||||
|
vi.spyOn(game.scene.currentBattle.trainer!, "getPartyMemberMatchupScores").mockReturnValue([[3, 100]]);
|
||||||
|
vi.spyOn(game.scene.getPlayerPokemon()!, "getMatchupScore").mockReturnValue(0);
|
||||||
|
});
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerDoubled();
|
||||||
|
expectWasHit(findPartyMember(game.scene.getEnemyParty(), enemyLead)).and(expectNotOnField);
|
||||||
|
expectWasNotHit(game.scene.getEnemyField()[0]).and(expectIsSpecies(Species.BASCULIN));
|
||||||
|
expectWasNotHit(game.scene.getEnemyField()[1]).and(expectIsSpecies(Species.ALCREMIE));
|
||||||
|
expectPursuitSucceeded(game.scene.getPlayerField()[0]);
|
||||||
|
expectPursuitSucceeded(game.scene.getPlayerField()[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// This test is hard to verify, because it's hard to observe independently -
|
||||||
|
// but depending on exactly how the command ordering is done, it is possible
|
||||||
|
// for the command order to put one ally's Pursuit move before the other
|
||||||
|
// ally's Pokemon command, even if the pursuit move does not target a
|
||||||
|
// pursuer. At this time, this "appears" to work correctly, because of
|
||||||
|
// nuances in when phases are pushed vs. shifted; but the pursuit
|
||||||
|
// MoveHeaderPhase is actually run before the switch, which is the only way
|
||||||
|
// to find the issue at present. Asserting the direct output of the command order
|
||||||
|
// is probably a better solution.
|
||||||
|
it("should not move or apply tags before switch when ally switches and not pursuing an enemy", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesFirst(game.scene.getPlayerField().reverse());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerSwitches(2);
|
||||||
|
playerUsesPursuit(0);
|
||||||
|
enemyUses(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to(SwitchSummonPhase);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("MovePhase");
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("MoveHeaderPhase");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit a switching ally for double damage (hard-switch, player field)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getPlayerField());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerSwitches(2);
|
||||||
|
playerUsesPursuit(1, BattlerIndex.PLAYER);
|
||||||
|
enemyUses(Moves.SPLASH);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasHit(game.scene.getPlayerField()[0]);
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getParty(), playerLead)).and(expectNotOnField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit a switching ally for double damage (hard-switch, enemy field)", async () => {
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getEnemyField());
|
||||||
|
|
||||||
|
playerDoesNothing();
|
||||||
|
enemySwitches(true);
|
||||||
|
game.forceEnemyMove(Moves.PURSUIT, BattlerIndex.ENEMY);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getEnemyParty(), enemyLead)).and(expectNotOnField);
|
||||||
|
expectWasHit(game.scene.getEnemyField()[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit a switching ally for double damage (switch move, player field)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast([...game.scene.getPlayerField()].reverse());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerUsesPursuit(0, BattlerIndex.PLAYER_2);
|
||||||
|
playerUsesSwitchMove(1); // prompts need to be queued in the order of pursuit, then switch move or else they will softlock
|
||||||
|
enemyUses(Moves.SPLASH);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasHit(game.scene.getPlayerField()[1]);
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getParty(), Species.RAICHU)).and(expectNotOnField);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not hit a switching ally for double damage (switch move, enemy field)", async () => {
|
||||||
|
// arrange
|
||||||
|
await startBattle();
|
||||||
|
forceMovesLast(game.scene.getEnemyField());
|
||||||
|
|
||||||
|
// act
|
||||||
|
playerDoesNothing();
|
||||||
|
game.forceEnemyMove(Moves.U_TURN);
|
||||||
|
game.forceEnemyMove(Moves.PURSUIT, BattlerIndex.ENEMY);
|
||||||
|
await runCombatTurn();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expectPursuitPowerUnchanged();
|
||||||
|
expectWasNotHit(findPartyMember(game.scene.getEnemyParty(), enemyLead)).and(expectNotOnField);
|
||||||
|
expectWasHit(game.scene.getEnemyField()[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { allMoves } from "#app/data/move";
|
||||||
|
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||||
|
import { DamageCalculationResult } 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, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Steamroller", () => {
|
||||||
|
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.STEAMROLLER]).battleType("single").enemyAbility(Abilities.BALL_FETCH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should always hit a minimzed target with double damage", async () => {
|
||||||
|
game.override.enemySpecies(Species.DITTO).enemyMoveset(Moves.MINIMIZE);
|
||||||
|
await game.classicMode.startBattle([Species.IRON_BOULDER]);
|
||||||
|
|
||||||
|
const ditto = game.scene.getEnemyPokemon()!;
|
||||||
|
vi.spyOn(ditto, "getAttackDamage");
|
||||||
|
ditto.hp = 5000;
|
||||||
|
const steamroller = allMoves[Moves.STEAMROLLER];
|
||||||
|
vi.spyOn(steamroller, "calculateBattleAccuracy");
|
||||||
|
const ironBoulder = game.scene.getPlayerPokemon()!;
|
||||||
|
vi.spyOn(ironBoulder, "getAccuracyMultiplier");
|
||||||
|
// Turn 1
|
||||||
|
game.move.select(Moves.STEAMROLLER);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
// Turn 2
|
||||||
|
game.move.select(Moves.STEAMROLLER);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const [dmgCalcTurn1, dmgCalcTurn2]: DamageCalculationResult[] = vi
|
||||||
|
.mocked(ditto.getAttackDamage)
|
||||||
|
.mock.results.map((r) => r.value);
|
||||||
|
|
||||||
|
expect(dmgCalcTurn2.damage).toBeGreaterThanOrEqual(dmgCalcTurn1.damage * 2);
|
||||||
|
expect(ditto.getTag(BattlerTagType.MINIMIZED)).toBeDefined();
|
||||||
|
expect(steamroller.calculateBattleAccuracy).toHaveReturnedWith(-1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -35,64 +35,46 @@ describe("Moves - U-turn", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("triggers regenerator a single time when a regenerator user switches out with u-turn", async () => {
|
it("triggers regenerator a single time when a regenerator user switches out with u-turn", async () => {
|
||||||
// arrange
|
|
||||||
const playerHp = 1;
|
const playerHp = 1;
|
||||||
game.override.ability(Abilities.REGENERATOR);
|
game.override.ability(Abilities.REGENERATOR);
|
||||||
await game.startBattle([
|
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||||
Species.RAICHU,
|
|
||||||
Species.SHUCKLE
|
|
||||||
]);
|
|
||||||
game.scene.getPlayerPokemon()!.hp = playerHp;
|
game.scene.getPlayerPokemon()!.hp = playerHp;
|
||||||
|
|
||||||
// act
|
|
||||||
game.move.select(Moves.U_TURN);
|
game.move.select(Moves.U_TURN);
|
||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
await game.phaseInterceptor.to(TurnEndPhase);
|
||||||
|
|
||||||
// assert
|
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE);
|
||||||
expect(game.scene.getParty()[1].hp).toEqual(Math.floor(game.scene.getParty()[1].getMaxHp() * 0.33 + playerHp));
|
expect(game.scene.getParty()[1].hp).toEqual(Math.floor(game.scene.getParty()[1].getMaxHp() * 0.33 + playerHp));
|
||||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE);
|
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => {
|
it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => {
|
||||||
// arrange
|
|
||||||
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||||
await game.startBattle([
|
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||||
Species.RAICHU,
|
|
||||||
Species.SHUCKLE
|
|
||||||
]);
|
|
||||||
|
|
||||||
// act
|
|
||||||
game.move.select(Moves.U_TURN);
|
game.move.select(Moves.U_TURN);
|
||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
await game.phaseInterceptor.to(SwitchPhase, false);
|
await game.phaseInterceptor.to(SwitchPhase, false);
|
||||||
|
|
||||||
// assert
|
|
||||||
const playerPkm = game.scene.getPlayerPokemon()!;
|
const playerPkm = game.scene.getPlayerPokemon()!;
|
||||||
|
expect(playerPkm.species.speciesId).toEqual(Species.RAICHU);
|
||||||
expect(playerPkm.hp).not.toEqual(playerPkm.getMaxHp());
|
expect(playerPkm.hp).not.toEqual(playerPkm.getMaxHp());
|
||||||
expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated
|
expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated
|
||||||
expect(playerPkm.species.speciesId).toEqual(Species.RAICHU);
|
|
||||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
it("triggers contact abilities on the u-turn user (eg poison point) before a new pokemon is switched in", async () => {
|
it("triggers contact abilities on the u-turn user (eg poison point) before a new pokemon is switched in", async () => {
|
||||||
// arrange
|
|
||||||
game.override.enemyAbility(Abilities.POISON_POINT);
|
game.override.enemyAbility(Abilities.POISON_POINT);
|
||||||
await game.startBattle([
|
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||||
Species.RAICHU,
|
|
||||||
Species.SHUCKLE
|
|
||||||
]);
|
|
||||||
vi.spyOn(game.scene.getEnemyPokemon()!, "randSeedInt").mockReturnValue(0);
|
vi.spyOn(game.scene.getEnemyPokemon()!, "randSeedInt").mockReturnValue(0);
|
||||||
|
|
||||||
// act
|
|
||||||
game.move.select(Moves.U_TURN);
|
game.move.select(Moves.U_TURN);
|
||||||
await game.phaseInterceptor.to(SwitchPhase, false);
|
await game.phaseInterceptor.to(SwitchPhase, false);
|
||||||
|
|
||||||
// assert
|
|
||||||
const playerPkm = game.scene.getPlayerPokemon()!;
|
const playerPkm = game.scene.getPlayerPokemon()!;
|
||||||
expect(playerPkm.status?.effect).toEqual(StatusEffect.POISON);
|
|
||||||
expect(playerPkm.species.speciesId).toEqual(Species.RAICHU);
|
expect(playerPkm.species.speciesId).toEqual(Species.RAICHU);
|
||||||
|
expect(playerPkm.status?.effect).toEqual(StatusEffect.POISON);
|
||||||
expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated
|
expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); // proxy for asserting ability activated
|
||||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { BerryPhase } from "#app/phases/berry-phase";
|
||||||
|
import { Mode } from "#app/ui/ui";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Volt Switch", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
const TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.moveset([Moves.SPLASH])
|
||||||
|
.battleType("single")
|
||||||
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
|
.enemyMoveset(Moves.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not switch out the user if the move fails", async () => {
|
||||||
|
game.override
|
||||||
|
.enemySpecies(Species.DUGTRIO)
|
||||||
|
.moveset(Moves.VOLT_SWITCH);
|
||||||
|
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]);
|
||||||
|
|
||||||
|
game.move.select(Moves.VOLT_SWITCH);
|
||||||
|
game.onNextPrompt("SwitchPhase", Mode.PARTY, () => {
|
||||||
|
expect.fail("Switch was forced");
|
||||||
|
}, () => game.isCurrentPhase(BerryPhase));
|
||||||
|
await game.phaseInterceptor.to(BerryPhase, false);
|
||||||
|
|
||||||
|
const playerPkm = game.scene.getPlayerPokemon()!;
|
||||||
|
expect(playerPkm.species.speciesId).toEqual(Species.RAICHU);
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||||
|
}, TIMEOUT);
|
||||||
|
});
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { allMoves } from "#app/data/move";
|
||||||
|
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||||
|
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 - Whirlwind", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.battleType("single")
|
||||||
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
|
.enemyMoveset(Moves.WHIRLWIND)
|
||||||
|
.enemySpecies(Species.PIDGEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ move: Moves.FLY, name: "Fly" },
|
||||||
|
{ move: Moves.BOUNCE, name: "Bounce" },
|
||||||
|
{ move: Moves.SKY_DROP, name: "Sky Drop" },
|
||||||
|
])("should not hit a flying target: $name (=$move)", async ({ move }) => {
|
||||||
|
game.override.moveset([move]);
|
||||||
|
await game.classicMode.startBattle([Species.STARAPTOR]);
|
||||||
|
|
||||||
|
const staraptor = game.scene.getPlayerPokemon()!;
|
||||||
|
const whirlwind = allMoves[Moves.WHIRLWIND];
|
||||||
|
vi.spyOn(whirlwind, "getFailedText");
|
||||||
|
|
||||||
|
game.move.select(move);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
|
||||||
|
expect(whirlwind.getFailedText).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
|
@ -88,7 +88,7 @@ describe("UI - Starter select", () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
|
await game.phaseInterceptor.to(EncounterPhase, false);
|
||||||
|
|
||||||
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
||||||
expect(game.scene.getParty()[0].shiny).toBe(true);
|
expect(game.scene.getParty()[0].shiny).toBe(true);
|
||||||
|
@ -149,7 +149,7 @@ describe("UI - Starter select", () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
|
await game.phaseInterceptor.to(EncounterPhase, false);
|
||||||
|
|
||||||
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
||||||
expect(game.scene.getParty()[0].shiny).toBe(true);
|
expect(game.scene.getParty()[0].shiny).toBe(true);
|
||||||
|
@ -213,7 +213,7 @@ describe("UI - Starter select", () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
|
await game.phaseInterceptor.to(EncounterPhase, false);
|
||||||
|
|
||||||
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
||||||
expect(game.scene.getParty()[0].shiny).toBe(true);
|
expect(game.scene.getParty()[0].shiny).toBe(true);
|
||||||
|
@ -276,7 +276,7 @@ describe("UI - Starter select", () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
|
await game.phaseInterceptor.to(EncounterPhase, false);
|
||||||
|
|
||||||
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
||||||
expect(game.scene.getParty()[0].shiny).toBe(true);
|
expect(game.scene.getParty()[0].shiny).toBe(true);
|
||||||
|
@ -337,7 +337,7 @@ describe("UI - Starter select", () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
|
await game.phaseInterceptor.to(EncounterPhase, false);
|
||||||
|
|
||||||
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
||||||
expect(game.scene.getParty()[0].shiny).toBe(false);
|
expect(game.scene.getParty()[0].shiny).toBe(false);
|
||||||
|
@ -398,7 +398,7 @@ describe("UI - Starter select", () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
|
await game.phaseInterceptor.to(EncounterPhase, false);
|
||||||
|
|
||||||
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
||||||
expect(game.scene.getParty()[0].shiny).toBe(true);
|
expect(game.scene.getParty()[0].shiny).toBe(true);
|
||||||
|
@ -458,7 +458,7 @@ describe("UI - Starter select", () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
|
await game.phaseInterceptor.to(EncounterPhase, false);
|
||||||
|
|
||||||
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.BULBASAUR);
|
||||||
expect(game.scene.getParty()[0].shiny).toBe(true);
|
expect(game.scene.getParty()[0].shiny).toBe(true);
|
||||||
|
@ -525,7 +525,7 @@ describe("UI - Starter select", () => {
|
||||||
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
|
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
|
||||||
saveSlotSelectUiHandler.processInput(Button.ACTION);
|
saveSlotSelectUiHandler.processInput(Button.ACTION);
|
||||||
});
|
});
|
||||||
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
|
await game.phaseInterceptor.to(EncounterPhase, false);
|
||||||
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CATERPIE);
|
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.CATERPIE);
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
|
@ -591,7 +591,7 @@ describe("UI - Starter select", () => {
|
||||||
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
|
const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler;
|
||||||
saveSlotSelectUiHandler.processInput(Button.ACTION);
|
saveSlotSelectUiHandler.processInput(Button.ACTION);
|
||||||
});
|
});
|
||||||
await game.phaseInterceptor.whenAboutToRun(EncounterPhase);
|
await game.phaseInterceptor.to(EncounterPhase, false);
|
||||||
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.NIDORAN_M);
|
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.NIDORAN_M);
|
||||||
}, 20000);
|
}, 20000);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
import { updateUserInfo } from "#app/account";
|
import { updateUserInfo } from "#app/account";
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
import BattleScene from "#app/battle-scene";
|
import BattleScene from "#app/battle-scene";
|
||||||
import { BattleStyle } from "#app/enums/battle-style";
|
|
||||||
import { Moves } from "#app/enums/moves";
|
|
||||||
import { getMoveTargets } from "#app/data/move";
|
import { getMoveTargets } from "#app/data/move";
|
||||||
import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
|
import { BattleStyle } from "#app/enums/battle-style";
|
||||||
|
import { ExpGainsSpeed } from "#app/enums/exp-gains-speed";
|
||||||
|
import { Moves } from "#app/enums/moves";
|
||||||
|
import Pokemon from "#app/field/pokemon";
|
||||||
import Trainer from "#app/field/trainer";
|
import Trainer from "#app/field/trainer";
|
||||||
import { GameModes, getGameMode } from "#app/game-mode";
|
import { GameModes, getGameMode } from "#app/game-mode";
|
||||||
import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type";
|
import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type";
|
||||||
import overrides from "#app/overrides";
|
import overrides from "#app/overrides";
|
||||||
|
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
|
||||||
import { CommandPhase } from "#app/phases/command-phase";
|
import { CommandPhase } from "#app/phases/command-phase";
|
||||||
import { EncounterPhase } from "#app/phases/encounter-phase";
|
import { EncounterPhase } from "#app/phases/encounter-phase";
|
||||||
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
|
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
|
||||||
import { FaintPhase } from "#app/phases/faint-phase";
|
import { FaintPhase } from "#app/phases/faint-phase";
|
||||||
import { LoginPhase } from "#app/phases/login-phase";
|
import { LoginPhase } from "#app/phases/login-phase";
|
||||||
import { MovePhase } from "#app/phases/move-phase";
|
import { MovePhase } from "#app/phases/move-phase";
|
||||||
|
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
|
||||||
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
||||||
import { SelectStarterPhase } from "#app/phases/select-starter-phase";
|
import { SelectStarterPhase } from "#app/phases/select-starter-phase";
|
||||||
import { SelectTargetPhase } from "#app/phases/select-target-phase";
|
import { SelectTargetPhase } from "#app/phases/select-target-phase";
|
||||||
|
@ -24,13 +27,16 @@ import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||||
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
||||||
import ErrorInterceptor from "#app/test/utils/errorInterceptor";
|
import ErrorInterceptor from "#app/test/utils/errorInterceptor";
|
||||||
import InputsHandler from "#app/test/utils/inputsHandler";
|
import InputsHandler from "#app/test/utils/inputsHandler";
|
||||||
|
import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler";
|
||||||
import CommandUiHandler from "#app/ui/command-ui-handler";
|
import CommandUiHandler from "#app/ui/command-ui-handler";
|
||||||
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
|
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
|
||||||
import PartyUiHandler from "#app/ui/party-ui-handler";
|
import PartyUiHandler from "#app/ui/party-ui-handler";
|
||||||
import TargetSelectUiHandler from "#app/ui/target-select-ui-handler";
|
import TargetSelectUiHandler from "#app/ui/target-select-ui-handler";
|
||||||
import { Mode } from "#app/ui/ui";
|
import { Mode } from "#app/ui/ui";
|
||||||
|
import { isNullOrUndefined } from "#app/utils";
|
||||||
import { Button } from "#enums/buttons";
|
import { Button } from "#enums/buttons";
|
||||||
import { ExpNotification } from "#enums/exp-notification";
|
import { ExpNotification } from "#enums/exp-notification";
|
||||||
|
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||||
import { PlayerGender } from "#enums/player-gender";
|
import { PlayerGender } from "#enums/player-gender";
|
||||||
import { Species } from "#enums/species";
|
import { Species } from "#enums/species";
|
||||||
import { generateStarter, waitUntil } from "#test/utils/gameManagerUtils";
|
import { generateStarter, waitUntil } from "#test/utils/gameManagerUtils";
|
||||||
|
@ -39,21 +45,14 @@ import PhaseInterceptor from "#test/utils/phaseInterceptor";
|
||||||
import TextInterceptor from "#test/utils/TextInterceptor";
|
import TextInterceptor from "#test/utils/TextInterceptor";
|
||||||
import { AES, enc } from "crypto-js";
|
import { AES, enc } from "crypto-js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { vi } from "vitest";
|
import { expect, vi } from "vitest";
|
||||||
|
import { ChallengeModeHelper } from "./helpers/challengeModeHelper";
|
||||||
import { ClassicModeHelper } from "./helpers/classicModeHelper";
|
import { ClassicModeHelper } from "./helpers/classicModeHelper";
|
||||||
import { DailyModeHelper } from "./helpers/dailyModeHelper";
|
import { DailyModeHelper } from "./helpers/dailyModeHelper";
|
||||||
import { ChallengeModeHelper } from "./helpers/challengeModeHelper";
|
|
||||||
import { MoveHelper } from "./helpers/moveHelper";
|
import { MoveHelper } from "./helpers/moveHelper";
|
||||||
import { OverridesHelper } from "./helpers/overridesHelper";
|
import { OverridesHelper } from "./helpers/overridesHelper";
|
||||||
import { SettingsHelper } from "./helpers/settingsHelper";
|
|
||||||
import { ReloadHelper } from "./helpers/reloadHelper";
|
import { ReloadHelper } from "./helpers/reloadHelper";
|
||||||
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
|
import { SettingsHelper } from "./helpers/settingsHelper";
|
||||||
import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler";
|
|
||||||
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
|
|
||||||
import { expect } from "vitest";
|
|
||||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
|
||||||
import { isNullOrUndefined } from "#app/utils";
|
|
||||||
import { ExpGainsSpeed } from "#app/enums/exp-gains-speed";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to manage the game state and transitions between phases.
|
* Class to manage the game state and transitions between phases.
|
||||||
|
@ -141,7 +140,7 @@ export default class GameManager {
|
||||||
* @returns A promise that resolves when the title phase is reached.
|
* @returns A promise that resolves when the title phase is reached.
|
||||||
*/
|
*/
|
||||||
async runToTitle(): Promise<void> {
|
async runToTitle(): Promise<void> {
|
||||||
await this.phaseInterceptor.whenAboutToRun(LoginPhase);
|
await this.phaseInterceptor.to(LoginPhase, false);
|
||||||
this.phaseInterceptor.pop();
|
this.phaseInterceptor.pop();
|
||||||
await this.phaseInterceptor.run(TitlePhase);
|
await this.phaseInterceptor.run(TitlePhase);
|
||||||
|
|
||||||
|
@ -251,7 +250,7 @@ export default class GameManager {
|
||||||
* @param {BattlerIndex} targetIndex The index of the attack target, or `undefined` for multi-target attacks
|
* @param {BattlerIndex} targetIndex The index of the attack target, or `undefined` for multi-target attacks
|
||||||
* @param movePosition The index of the move in the pokemon's moveset array
|
* @param movePosition The index of the move in the pokemon's moveset array
|
||||||
*/
|
*/
|
||||||
selectTarget(movePosition: integer, targetIndex?: BattlerIndex) {
|
selectTarget(movePosition: number, targetIndex?: BattlerIndex) {
|
||||||
this.onNextPrompt("SelectTargetPhase", Mode.TARGET_SELECT, () => {
|
this.onNextPrompt("SelectTargetPhase", Mode.TARGET_SELECT, () => {
|
||||||
const handler = this.scene.ui.getHandler() as TargetSelectUiHandler;
|
const handler = this.scene.ui.getHandler() as TargetSelectUiHandler;
|
||||||
const move = (this.scene.getCurrentPhase() as SelectTargetPhase).getPokemon().getMoveset()[movePosition]!.getMove(); // TODO: is the bang correct?
|
const move = (this.scene.getCurrentPhase() as SelectTargetPhase).getPokemon().getMoveset()[movePosition]!.getMove(); // TODO: is the bang correct?
|
||||||
|
@ -397,7 +396,7 @@ export default class GameManager {
|
||||||
return updateUserInfo();
|
return updateUserInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) {
|
async killPokemon(pokemon: Pokemon) {
|
||||||
return new Promise<void>(async (resolve, reject) => {
|
return new Promise<void>(async (resolve, reject) => {
|
||||||
pokemon.hp = 0;
|
pokemon.hp = 0;
|
||||||
this.scene.pushPhase(new FaintPhase(this.scene, pokemon.getBattlerIndex(), true));
|
this.scene.pushPhase(new FaintPhase(this.scene, pokemon.getBattlerIndex(), true));
|
||||||
|
@ -441,6 +440,10 @@ export default class GameManager {
|
||||||
*/
|
*/
|
||||||
doSelectPartyPokemon(slot: number, inPhase = "SwitchPhase") {
|
doSelectPartyPokemon(slot: number, inPhase = "SwitchPhase") {
|
||||||
this.onNextPrompt(inPhase, Mode.PARTY, () => {
|
this.onNextPrompt(inPhase, Mode.PARTY, () => {
|
||||||
|
if (this.scene.getParty()[slot].isActive(true)) {
|
||||||
|
throw new Error("Attempting to switch in a party member that is already active");
|
||||||
|
}
|
||||||
|
|
||||||
const partyHandler = this.scene.ui.getHandler() as PartyUiHandler;
|
const partyHandler = this.scene.ui.getHandler() as PartyUiHandler;
|
||||||
|
|
||||||
partyHandler.setCursor(slot);
|
partyHandler.setCursor(slot);
|
||||||
|
@ -458,10 +461,11 @@ export default class GameManager {
|
||||||
* await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2]);
|
* await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2]);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
async setTurnOrder(order: BattlerIndex[]): Promise<void> {
|
async setTurnOrder(order: BattlerIndex[], modifyPriority: boolean = false): Promise<void> {
|
||||||
await this.phaseInterceptor.to(TurnStartPhase, false);
|
await this.phaseInterceptor.to(TurnStartPhase, false);
|
||||||
|
console.log(`${modifyPriority ? "Turn" : "Speed"} order modified to: `, order);
|
||||||
|
|
||||||
vi.spyOn(this.scene.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order);
|
vi.spyOn(this.scene.getCurrentPhase() as TurnStartPhase, (modifyPriority ? "getCommandOrder" : "getSpeedOrder")).mockReturnValue(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,10 +8,10 @@ import * as GameMode from "#app/game-mode";
|
||||||
import { GameModes, getGameMode } from "#app/game-mode";
|
import { GameModes, getGameMode } from "#app/game-mode";
|
||||||
import { ModifierOverride } from "#app/modifier/modifier-type";
|
import { ModifierOverride } from "#app/modifier/modifier-type";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
|
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||||
|
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import { GameManagerHelper } from "./gameManagerHelper";
|
import { GameManagerHelper } from "./gameManagerHelper";
|
||||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
|
||||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to handle overrides in tests
|
* Helper to handle overrides in tests
|
||||||
|
@ -325,6 +325,27 @@ export class OverridesHelper extends GameManagerHelper {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the trainer AI's party
|
||||||
|
* @param species List of pokemon to generate in the party
|
||||||
|
* @returns this
|
||||||
|
*/
|
||||||
|
enemyParty(species: Species[]) {
|
||||||
|
vi.spyOn(Overrides, "TRAINER_PARTY_OVERRIDE", "get").mockReturnValue(species);
|
||||||
|
this.log("Enemy trainer party set to:", species);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces the AI to always switch out, or reset to allow normal switching decisions
|
||||||
|
* @returns this
|
||||||
|
*/
|
||||||
|
forceTrainerSwitches(newValue: boolean = true) {
|
||||||
|
vi.spyOn(Overrides, "TRAINER_ALWAYS_SWITCHES_OVERRIDE", "get").mockReturnValue(newValue);
|
||||||
|
this.log("Trainers will always switch out set to:", newValue);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the encounter chance for a mystery encounter.
|
* Override the encounter chance for a mystery encounter.
|
||||||
* @param percentage the encounter chance in %
|
* @param percentage the encounter chance in %
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Phase } from "#app/phase";
|
import { Phase } from "#app/phase";
|
||||||
import ErrorInterceptor from "#app/test/utils/errorInterceptor";
|
|
||||||
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
|
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
|
||||||
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
||||||
import { BerryPhase } from "#app/phases/berry-phase";
|
import { BerryPhase } from "#app/phases/berry-phase";
|
||||||
|
@ -11,17 +10,28 @@ import { EncounterPhase } from "#app/phases/encounter-phase";
|
||||||
import { EndEvolutionPhase } from "#app/phases/end-evolution-phase";
|
import { EndEvolutionPhase } from "#app/phases/end-evolution-phase";
|
||||||
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
|
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
|
||||||
import { EvolutionPhase } from "#app/phases/evolution-phase";
|
import { EvolutionPhase } from "#app/phases/evolution-phase";
|
||||||
|
import { ExpPhase } from "#app/phases/exp-phase";
|
||||||
import { FaintPhase } from "#app/phases/faint-phase";
|
import { FaintPhase } from "#app/phases/faint-phase";
|
||||||
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
||||||
import { LevelCapPhase } from "#app/phases/level-cap-phase";
|
import { LevelCapPhase } from "#app/phases/level-cap-phase";
|
||||||
import { LoginPhase } from "#app/phases/login-phase";
|
import { LoginPhase } from "#app/phases/login-phase";
|
||||||
import { MessagePhase } from "#app/phases/message-phase";
|
import { MessagePhase } from "#app/phases/message-phase";
|
||||||
|
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
|
||||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||||
import { MovePhase } from "#app/phases/move-phase";
|
import { MovePhase } from "#app/phases/move-phase";
|
||||||
|
import {
|
||||||
|
MysteryEncounterBattlePhase,
|
||||||
|
MysteryEncounterOptionSelectedPhase,
|
||||||
|
MysteryEncounterPhase,
|
||||||
|
MysteryEncounterRewardsPhase,
|
||||||
|
PostMysteryEncounterPhase
|
||||||
|
} from "#app/phases/mystery-encounter-phases";
|
||||||
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
||||||
import { NewBiomeEncounterPhase } from "#app/phases/new-biome-encounter-phase";
|
import { NewBiomeEncounterPhase } from "#app/phases/new-biome-encounter-phase";
|
||||||
import { NextEncounterPhase } from "#app/phases/next-encounter-phase";
|
import { NextEncounterPhase } from "#app/phases/next-encounter-phase";
|
||||||
|
import { PartyExpPhase } from "#app/phases/party-exp-phase";
|
||||||
|
import { PartyHealPhase } from "#app/phases/party-heal-phase";
|
||||||
import { PostSummonPhase } from "#app/phases/post-summon-phase";
|
import { PostSummonPhase } from "#app/phases/post-summon-phase";
|
||||||
import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
|
import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
|
||||||
import { SelectGenderPhase } from "#app/phases/select-gender-phase";
|
import { SelectGenderPhase } from "#app/phases/select-gender-phase";
|
||||||
|
@ -41,17 +51,11 @@ import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||||
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
||||||
import { UnavailablePhase } from "#app/phases/unavailable-phase";
|
import { UnavailablePhase } from "#app/phases/unavailable-phase";
|
||||||
import { VictoryPhase } from "#app/phases/victory-phase";
|
import { VictoryPhase } from "#app/phases/victory-phase";
|
||||||
import { PartyHealPhase } from "#app/phases/party-heal-phase";
|
import ErrorInterceptor from "#app/test/utils/errorInterceptor";
|
||||||
import UI, { Mode } from "#app/ui/ui";
|
import UI, { Mode } from "#app/ui/ui";
|
||||||
import {
|
import { expect } from "vitest";
|
||||||
MysteryEncounterBattlePhase,
|
|
||||||
MysteryEncounterOptionSelectedPhase,
|
type PhaseClassType = (abstract new (...args: any) => Phase); // `typeof Phase` does not work here because of some issue with ctor signatures
|
||||||
MysteryEncounterPhase,
|
|
||||||
MysteryEncounterRewardsPhase,
|
|
||||||
PostMysteryEncounterPhase
|
|
||||||
} from "#app/phases/mystery-encounter-phases";
|
|
||||||
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
|
|
||||||
import { PartyExpPhase } from "#app/phases/party-exp-phase";
|
|
||||||
|
|
||||||
export interface PromptHandler {
|
export interface PromptHandler {
|
||||||
phaseTarget?: string;
|
phaseTarget?: string;
|
||||||
|
@ -60,7 +64,6 @@ export interface PromptHandler {
|
||||||
expireFn?: () => void;
|
expireFn?: () => void;
|
||||||
awaitingActionInput?: boolean;
|
awaitingActionInput?: boolean;
|
||||||
}
|
}
|
||||||
import { ExpPhase } from "#app/phases/exp-phase";
|
|
||||||
|
|
||||||
export default class PhaseInterceptor {
|
export default class PhaseInterceptor {
|
||||||
public scene;
|
public scene;
|
||||||
|
@ -211,13 +214,21 @@ export default class PhaseInterceptor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance a single phase
|
||||||
|
* @returns A promise that resolves when the next phase has started
|
||||||
|
*/
|
||||||
|
advance(): Promise<void> {
|
||||||
|
return this.run(this.onHold[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to run a phase with an optional skip function.
|
* Method to run a phase with an optional skip function.
|
||||||
* @param phaseTarget - The phase to run.
|
* @param phaseTarget - The phase to run.
|
||||||
* @param skipFn - Optional skip function.
|
* @param skipFn - Optional skip function.
|
||||||
* @returns A promise that resolves when the phase is run.
|
* @returns A promise that resolves when the phase is run.
|
||||||
*/
|
*/
|
||||||
run(phaseTarget, skipFn?): Promise<void> {
|
async run(phaseTarget, skipFn?): Promise<void> {
|
||||||
const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name;
|
const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name;
|
||||||
this.scene.moveAnimations = null; // Mandatory to avoid crash
|
this.scene.moveAnimations = null; // Mandatory to avoid crash
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
@ -251,19 +262,25 @@ export default class PhaseInterceptor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
whenAboutToRun(phaseTarget, skipFn?): Promise<void> {
|
/**
|
||||||
const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name;
|
* The next time a phase of the given type would be run, first run the provided callback.
|
||||||
|
* The phase instance is passed to the callback, for easier mocking.
|
||||||
|
*
|
||||||
|
* This function does not actually start running phases - for that, see {@linkcode to()}.
|
||||||
|
* @param phaseType Class type of the phase you want to tap
|
||||||
|
* @param cb callback to run when the phase next arrives
|
||||||
|
*/
|
||||||
|
onNextPhase<T extends PhaseClassType>(phaseType: T, cb: (phase: InstanceType<T>) => void) {
|
||||||
|
const targetName = phaseType.name;
|
||||||
this.scene.moveAnimations = null; // Mandatory to avoid crash
|
this.scene.moveAnimations = null; // Mandatory to avoid crash
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
ErrorInterceptor.getInstance().add(this);
|
ErrorInterceptor.getInstance().add(this);
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
const currentPhase = this.onHold[0];
|
const currentPhase = this.onHold[0];
|
||||||
if (currentPhase?.name === targetName) {
|
if (currentPhase?.name === targetName) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
resolve();
|
cb(this.scene.getCurrentPhase());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pop() {
|
pop() {
|
||||||
|
@ -359,6 +376,13 @@ export default class PhaseInterceptor {
|
||||||
* Method to start the prompt handler.
|
* Method to start the prompt handler.
|
||||||
*/
|
*/
|
||||||
startPromptHandler() {
|
startPromptHandler() {
|
||||||
|
const PROMPT_TIMEOUT = 2000;
|
||||||
|
|
||||||
|
let timeSpentInPrompt = 0;
|
||||||
|
let lastTime: number | undefined = undefined;
|
||||||
|
let lastPhase, lastPromptPhase, lastMode;
|
||||||
|
let warned = false;
|
||||||
|
|
||||||
this.promptInterval = setInterval(() => {
|
this.promptInterval = setInterval(() => {
|
||||||
if (this.prompts.length) {
|
if (this.prompts.length) {
|
||||||
const actionForNextPrompt = this.prompts[0];
|
const actionForNextPrompt = this.prompts[0];
|
||||||
|
@ -366,8 +390,31 @@ export default class PhaseInterceptor {
|
||||||
const currentMode = this.scene.ui.getMode();
|
const currentMode = this.scene.ui.getMode();
|
||||||
const currentPhase = this.scene.getCurrentPhase()?.constructor.name;
|
const currentPhase = this.scene.getCurrentPhase()?.constructor.name;
|
||||||
const currentHandler = this.scene.ui.getHandler();
|
const currentHandler = this.scene.ui.getHandler();
|
||||||
|
|
||||||
|
if (lastPhase === currentPhase && lastPromptPhase === actionForNextPrompt.phaseTarget && lastMode === currentMode && currentMode !== Mode.MESSAGE) {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
timeSpentInPrompt += lastTime === undefined ? 0 : currentTime - lastTime;
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
|
if (timeSpentInPrompt > PROMPT_TIMEOUT && !warned) {
|
||||||
|
warned = true;
|
||||||
|
console.error("Prompt handling stalled waiting for prompt:", actionForNextPrompt);
|
||||||
|
expect.fail("Prompt timeout");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warned = false;
|
||||||
|
lastMode = currentMode;
|
||||||
|
lastPhase = currentPhase;
|
||||||
|
lastPromptPhase = actionForNextPrompt.phaseTarget;
|
||||||
|
timeSpentInPrompt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (expireFn) {
|
if (expireFn) {
|
||||||
this.prompts.shift();
|
this.prompts.shift();
|
||||||
|
console.log(`Prompt for ${actionForNextPrompt.phaseTarget} (mode ${actionForNextPrompt.mode}) has expired`);
|
||||||
|
timeSpentInPrompt = 0;
|
||||||
|
lastTime = undefined;
|
||||||
} else if (currentMode === actionForNextPrompt.mode && currentPhase === actionForNextPrompt.phaseTarget && currentHandler.active && (!actionForNextPrompt.awaitingActionInput || (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))) {
|
} else if (currentMode === actionForNextPrompt.mode && currentPhase === actionForNextPrompt.phaseTarget && currentHandler.active && (!actionForNextPrompt.awaitingActionInput || (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))) {
|
||||||
const prompt = this.prompts.shift();
|
const prompt = this.prompts.shift();
|
||||||
if (prompt?.callback) {
|
if (prompt?.callback) {
|
||||||
|
@ -394,6 +441,7 @@ export default class PhaseInterceptor {
|
||||||
expireFn,
|
expireFn,
|
||||||
awaitingActionInput
|
awaitingActionInput
|
||||||
});
|
});
|
||||||
|
console.log(`Prompt added for ${phaseTarget} (mode ${mode})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -162,7 +162,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
||||||
this.splicedIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains);
|
this.splicedIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains);
|
||||||
this.add(this.splicedIcon);
|
this.add(this.splicedIcon);
|
||||||
|
|
||||||
this.statusIndicator = this.scene.add.sprite(0, 0, `statuses_${i18next.resolvedLanguage}`);
|
this.statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses"));
|
||||||
this.statusIndicator.setName("icon_status");
|
this.statusIndicator.setName("icon_status");
|
||||||
this.statusIndicator.setVisible(false);
|
this.statusIndicator.setVisible(false);
|
||||||
this.statusIndicator.setOrigin(0, 0);
|
this.statusIndicator.setOrigin(0, 0);
|
||||||
|
|
|
@ -91,7 +91,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem
|
||||||
valuesBg.setOrigin(0, 0);
|
valuesBg.setOrigin(0, 0);
|
||||||
this.val.add(valuesBg);
|
this.val.add(valuesBg);
|
||||||
|
|
||||||
this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, `types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, "unknown");
|
this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, Utils.getLocalizedSpriteKey("types"), "unknown");
|
||||||
this.typ.setScale(0.8);
|
this.typ.setScale(0.8);
|
||||||
this.val.add(this.typ);
|
this.val.add(this.typ);
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem
|
||||||
this.pow.setText(move.power >= 0 ? move.power.toString() : "---");
|
this.pow.setText(move.power >= 0 ? move.power.toString() : "---");
|
||||||
this.acc.setText(move.accuracy >= 0 ? move.accuracy.toString() : "---");
|
this.acc.setText(move.accuracy >= 0 ? move.accuracy.toString() : "---");
|
||||||
this.pp.setText(move.pp >= 0 ? move.pp.toString() : "---");
|
this.pp.setText(move.pp >= 0 ? move.pp.toString() : "---");
|
||||||
this.typ.setTexture(`types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, Type[move.type].toLowerCase());
|
this.typ.setTexture(Utils.getLocalizedSpriteKey("types"), Type[move.type].toLowerCase());
|
||||||
this.cat.setFrame(MoveCategory[move.category].toLowerCase());
|
this.cat.setFrame(MoveCategory[move.category].toLowerCase());
|
||||||
|
|
||||||
this.desc.setText(move?.effect || "");
|
this.desc.setText(move?.effect || "");
|
||||||
|
|
|
@ -1272,7 +1272,7 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pokemon.status) {
|
if (this.pokemon.status) {
|
||||||
const statusIndicator = this.scene.add.sprite(0, 0, `statuses_${i18next.resolvedLanguage}`);
|
const statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses"));
|
||||||
statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase());
|
statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase());
|
||||||
statusIndicator.setOrigin(0, 0);
|
statusIndicator.setOrigin(0, 0);
|
||||||
statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0);
|
statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0);
|
||||||
|
|
|
@ -214,7 +214,7 @@ export default class SummaryUiHandler extends UiHandler {
|
||||||
|
|
||||||
this.statusContainer.add(statusLabel);
|
this.statusContainer.add(statusLabel);
|
||||||
|
|
||||||
this.status = this.scene.add.sprite(91, 4, `statuses_${i18next.resolvedLanguage}`);
|
this.status = this.scene.add.sprite(91, 4, Utils.getLocalizedSpriteKey("statuses"));
|
||||||
this.status.setOrigin(0.5, 0);
|
this.status.setOrigin(0.5, 0);
|
||||||
|
|
||||||
this.statusContainer.add(this.status);
|
this.statusContainer.add(this.status);
|
||||||
|
|
10
src/utils.ts
10
src/utils.ts
|
@ -1,4 +1,5 @@
|
||||||
import { MoneyFormat } from "#enums/money-format";
|
import { MoneyFormat } from "#enums/money-format";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export const MissingTextureKey = "__MISSING";
|
export const MissingTextureKey = "__MISSING";
|
||||||
|
@ -628,3 +629,12 @@ export function getLocalizedSpriteKey(baseKey: string) {
|
||||||
export function isBetween(num: number, min: number, max: number): boolean {
|
export function isBetween(num: number, min: number, max: number): boolean {
|
||||||
return num >= min && num <= max;
|
return num >= min && num <= max;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to return the animation filename for a given move
|
||||||
|
*
|
||||||
|
* @param move the move for which the animation filename is needed
|
||||||
|
*/
|
||||||
|
export function animationFileName(move: Moves): string {
|
||||||
|
return Moves[move].toLowerCase().replace(/\_/g, "-");
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue