Merge branch 'beta' into tera-rework

This commit is contained in:
Xavion3 2025-02-11 23:16:05 +11:00
commit adcab4bc90
67 changed files with 4252 additions and 4418 deletions

View File

@ -5,6 +5,7 @@ import importX from 'eslint-plugin-import-x';
export default [ export default [
{ {
name: "eslint-config",
files: ["src/**/*.{ts,tsx,js,jsx}"], files: ["src/**/*.{ts,tsx,js,jsx}"],
ignores: ["dist/*", "build/*", "coverage/*", "public/*", ".github/*", "node_modules/*", ".vscode/*"], ignores: ["dist/*", "build/*", "coverage/*", "public/*", ".github/*", "node_modules/*", ".vscode/*"],
languageOptions: { languageOptions: {
@ -48,5 +49,22 @@ export default [
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], // Disallows multiple empty lines "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], // Disallows multiple empty lines
"@typescript-eslint/consistent-type-imports": "error", // Enforces type-only imports wherever possible "@typescript-eslint/consistent-type-imports": "error", // Enforces type-only imports wherever possible
} }
},
{
name: "eslint-tests",
files: ["src/test/**/**.test.ts"],
languageOptions: {
parser: parser,
parserOptions: {
"project": ["./tsconfig.json"]
}
},
plugins: {
"@typescript-eslint": tseslint
},
rules: {
"@typescript-eslint/no-floating-promises": "error", // Require Promise-like statements to be handled appropriately. - https://typescript-eslint.io/rules/no-floating-promises/
"@typescript-eslint/no-misused-promises": "error", // Disallow Promises in places not designed to handle them. - https://typescript-eslint.io/rules/no-misused-promises/
}
} }
] ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,19 +1,19 @@
{ "frames": [ { "frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 77, "h": 77 }, "frame": { "x": 0, "y": 0, "w": 77, "h": 65 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 }, "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 65 },
"sourceSize": { "w": 77, "h": 77 }, "sourceSize": { "w": 77, "h": 65 },
"duration": 100 "duration": 100
} }
], ],
"meta": { "meta": {
"app": "https://www.aseprite.org/", "app": "https://www.aseprite.org/",
"version": "1.3.7-x64", "version": "1.3.9.2-x64",
"format": "I8", "format": "I8",
"size": { "w": 77, "h": 77 }, "size": { "w": 77, "h": 65 },
"scale": "1" "scale": "1"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 890 B

View File

@ -1,11 +1,11 @@
{ "frames": [ { "frames": [
{ {
"filename": "0001.png", "filename": "0001.png",
"frame": { "x": 0, "y": 0, "w": 77, "h": 77 }, "frame": { "x": 0, "y": 0, "w": 77, "h": 65 },
"rotated": false, "rotated": false,
"trimmed": false, "trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 }, "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 65 },
"sourceSize": { "w": 77, "h": 77 }, "sourceSize": { "w": 77, "h": 65 },
"duration": 100 "duration": 100
} }
], ],
@ -13,7 +13,7 @@
"app": "https://www.aseprite.org/", "app": "https://www.aseprite.org/",
"version": "1.3.7-x64", "version": "1.3.7-x64",
"format": "I8", "format": "I8",
"size": { "w": 77, "h": 77 }, "size": { "w": 77, "h": 65 },
"scale": "1" "scale": "1"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 890 B

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@ -1 +1 @@
Subproject commit 5f6fa82c17d5981eaec15f105880ac2b4c99cc8d Subproject commit bfcd7f91c39630f155839872c8f66fd0a89e12ac

View File

@ -1405,8 +1405,8 @@ export default class BattleScene extends SceneBase {
return this.currentBattle; return this.currentBattle;
} }
newArena(biome: Biome): Arena { newArena(biome: Biome, playerFaints?: number): Arena {
this.arena = new Arena(biome, Biome[biome].toLowerCase()); this.arena = new Arena(biome, Biome[biome].toLowerCase(), playerFaints);
this.eventTarget.dispatchEvent(new NewArenaEvent()); this.eventTarget.dispatchEvent(new NewArenaEvent());
this.arenaBg.pipelineData = { terrainColorRatio: this.arena.getBgTerrainColorRatioForBiome() }; this.arenaBg.pipelineData = { terrainColorRatio: this.arena.getBgTerrainColorRatioForBiome() };
@ -2357,14 +2357,14 @@ export default class BattleScene extends SceneBase {
} }
/** /**
* Adds Phase to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex * Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
* @param phase {@linkcode Phase} the phase to add * @param phases {@linkcode Phase} the phase(s) to add
*/ */
unshiftPhase(phase: Phase): void { unshiftPhase(...phases: Phase[]): void {
if (this.phaseQueuePrependSpliceIndex === -1) { if (this.phaseQueuePrependSpliceIndex === -1) {
this.phaseQueuePrepend.push(phase); this.phaseQueuePrepend.push(...phases);
} else { } else {
this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, phase); this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases);
} }
} }
@ -2502,32 +2502,38 @@ export default class BattleScene extends SceneBase {
* @param targetPhase {@linkcode Phase} the type of phase to search for in phaseQueue * @param targetPhase {@linkcode Phase} the type of phase to search for in phaseQueue
* @returns boolean if a targetPhase was found and added * @returns boolean if a targetPhase was found and added
*/ */
prependToPhase(phase: Phase, targetPhase: Constructor<Phase>): boolean { prependToPhase(phase: Phase | Phase [], targetPhase: Constructor<Phase>): boolean {
if (!Array.isArray(phase)) {
phase = [ phase ];
}
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase); const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase);
if (targetIndex !== -1) { if (targetIndex !== -1) {
this.phaseQueue.splice(targetIndex, 0, phase); this.phaseQueue.splice(targetIndex, 0, ...phase);
return true; return true;
} else { } else {
this.unshiftPhase(phase); this.unshiftPhase(...phase);
return false; return false;
} }
} }
/** /**
* Tries to add the input phase to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} * Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()}
* @param phase {@linkcode Phase} the phase to be added * @param phase {@linkcode Phase} the phase(s) to be added
* @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue}
* @returns `true` if a `targetPhase` was found to append to * @returns `true` if a `targetPhase` was found to append to
*/ */
appendToPhase(phase: Phase, targetPhase: Constructor<Phase>): boolean { appendToPhase(phase: Phase | Phase[], targetPhase: Constructor<Phase>): boolean {
if (!Array.isArray(phase)) {
phase = [ phase ];
}
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase); const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase);
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
this.phaseQueue.splice(targetIndex + 1, 0, phase); this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
return true; return true;
} else { } else {
this.unshiftPhase(phase); this.unshiftPhase(...phase);
return false; return false;
} }
} }

View File

@ -102,10 +102,15 @@ export default class Battle {
public battleSeed: string = Utils.randomString(16, true); public battleSeed: string = Utils.randomString(16, true);
private battleSeedState: string | null = null; private battleSeedState: string | null = null;
public moneyScattered: number = 0; public moneyScattered: number = 0;
/** Primarily for double battles, keeps track of last enemy and player pokemon that triggered its ability or used a move */
public lastEnemyInvolved: number;
public lastPlayerInvolved: number;
public lastUsedPokeball: PokeballType | null = null; public lastUsedPokeball: PokeballType | null = null;
/** The number of times a Pokemon on the player's side has fainted this battle */ /**
public playerFaints: number = 0; * Saves the number of times a Pokemon on the enemy's side has fainted during this battle.
/** The number of times a Pokemon on the enemy's side has fainted this battle */ * This is saved here since we encounter a new enemy every wave.
* {@linkcode globalScene.arena.playerFaints} is the corresponding faint counter for the player and needs to be save across waves (reset every arena encounter).
*/
public enemyFaints: number = 0; public enemyFaints: number = 0;
public playerFaintsHistory: FaintLogEntry[] = []; public playerFaintsHistory: FaintLogEntry[] = [];
public enemyFaintsHistory: FaintLogEntry[] = []; public enemyFaintsHistory: FaintLogEntry[] = [];
@ -116,7 +121,7 @@ export default class Battle {
private rngCounter: number = 0; private rngCounter: number = 0;
constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double?: boolean) { constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double: boolean = false) {
this.gameMode = gameMode; this.gameMode = gameMode;
this.waveIndex = waveIndex; this.waveIndex = waveIndex;
this.battleType = battleType; this.battleType = battleType;
@ -125,7 +130,7 @@ export default class Battle {
this.enemyLevels = battleType !== BattleType.TRAINER this.enemyLevels = battleType !== BattleType.TRAINER
? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave()) ? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave())
: trainer?.getPartyLevels(this.waveIndex); : trainer?.getPartyLevels(this.waveIndex);
this.double = double ?? false; this.double = double;
} }
private initBattleSpec(): void { private initBattleSpec(): void {

View File

@ -2744,6 +2744,44 @@ export class PreStatStageChangeAbAttr extends AbAttr {
} }
} }
/**
* Reflect all {@linkcode BattleStat} reductions caused by other Pokémon's moves and Abilities.
* Currently only applies to Mirror Armor.
*/
export class ReflectStatStageChangeAbAttr extends PreStatStageChangeAbAttr {
/** {@linkcode BattleStat} to reflect */
private reflectedStat? : BattleStat;
/**
* Apply the {@linkcode ReflectStatStageChangeAbAttr} to an interaction
* @param _pokemon The user pokemon
* @param _passive N/A
* @param simulated `true` if the ability is being simulated by the AI
* @param stat the {@linkcode BattleStat} being affected
* @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true due to reflection
* @param args
* @returns true because it reflects any stat being lowered
*/
applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const attacker: Pokemon = args[0];
const stages = args[1];
this.reflectedStat = stat;
if (!simulated) {
globalScene.unshiftPhase(new StatStageChangePhase(attacker.getBattlerIndex(), false, [ stat ], stages, true, false, true, null, true));
}
cancelled.value = true;
return true;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
return i18next.t("abilityTriggers:protectStat", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
abilityName,
statName: this.reflectedStat ? i18next.t(getStatKey(this.reflectedStat)) : i18next.t("battle:stats")
});
}
}
/** /**
* Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities * Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities
*/ */
@ -4434,6 +4472,13 @@ export class InfiltratorAbAttr extends AbAttr {
} }
} }
/**
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Magic_Bounce_(ability) | Magic Bounce}.
* Allows the source to bounce back {@linkcode MoveFlags.REFLECTABLE | Reflectable}
* moves as if the user had used {@linkcode Moves.MAGIC_COAT | Magic Coat}.
*/
export class ReflectStatusMoveAbAttr extends AbAttr { }
export class UncopiableAbilityAbAttr extends AbAttr { export class UncopiableAbilityAbAttr extends AbAttr {
constructor() { constructor() {
super(false); super(false);
@ -5755,8 +5800,11 @@ export function initAbilities() {
}, Stat.SPD, 1) }, Stat.SPD, 1)
.attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1), .attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1),
new Ability(Abilities.MAGIC_BOUNCE, 5) new Ability(Abilities.MAGIC_BOUNCE, 5)
.attr(ReflectStatusMoveAbAttr)
.ignorable() .ignorable()
.unimplemented(), // Interactions with stomping tantrum, instruct, encore, and probably other moves that
// rely on move history
.edgeCase(),
new Ability(Abilities.SAP_SIPPER, 5) new Ability(Abilities.SAP_SIPPER, 5)
.attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1) .attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1)
.ignorable(), .ignorable(),
@ -6053,8 +6101,8 @@ export function initAbilities() {
new Ability(Abilities.PROPELLER_TAIL, 8) new Ability(Abilities.PROPELLER_TAIL, 8)
.attr(BlockRedirectAbAttr), .attr(BlockRedirectAbAttr),
new Ability(Abilities.MIRROR_ARMOR, 8) new Ability(Abilities.MIRROR_ARMOR, 8)
.ignorable() .attr(ReflectStatStageChangeAbAttr)
.unimplemented(), .ignorable(),
/** /**
* Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an * Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an
* ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case * ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case
@ -6263,8 +6311,8 @@ export function initAbilities() {
new Ability(Abilities.SHARPNESS, 9) new Ability(Abilities.SHARPNESS, 9)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5),
new Ability(Abilities.SUPREME_OVERLORD, 9) new Ability(Abilities.SUPREME_OVERLORD, 9)
.attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.currentBattle.playerFaints : globalScene.currentBattle.enemyFaints, 5)) .attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5))
.partial(), // Counter resets every wave instead of on arena reset .partial(), // Should only boost once, on summon
new Ability(Abilities.COSTAR, 9) new Ability(Abilities.COSTAR, 9)
.attr(PostSummonCopyAllyStatsAbAttr), .attr(PostSummonCopyAllyStatsAbAttr),
new Ability(Abilities.TOXIC_DEBRIS, 9) new Ability(Abilities.TOXIC_DEBRIS, 9)

View File

@ -910,7 +910,7 @@ class StickyWebTag extends ArenaTrapTag {
if (!cancelled.value) { if (!cancelled.value) {
globalScene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() })); globalScene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() }));
const stages = new NumberHolder(-1); const stages = new NumberHolder(-1);
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value)); globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value, true, false, true, null, false, true));
return true; return true;
} }
} }

View File

@ -1752,7 +1752,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
super.onAdd(pokemon); super.onAdd(pokemon);
let highestStat: EffectiveStat; let highestStat: EffectiveStat;
EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s)).reduce((highestValue: number, value: number, i: number) => { EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s, undefined, undefined, undefined, undefined, undefined, undefined, true)).reduce((highestValue: number, value: number, i: number) => {
if (value > highestValue) { if (value > highestValue) {
highestStat = EFFECTIVE_STATS[i]; highestStat = EFFECTIVE_STATS[i];
return value; return value;
@ -1763,15 +1763,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
highestStat = highestStat!; // tell TS compiler it's defined! highestStat = highestStat!; // tell TS compiler it's defined!
this.stat = highestStat; this.stat = highestStat;
switch (this.stat) { this.multiplier = this.stat === Stat.SPD ? 1.5 : 1.3;
case Stat.SPD:
this.multiplier = 1.5;
break;
default:
this.multiplier = 1.3;
break;
}
globalScene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: i18next.t(getStatKey(highestStat)) }), null, false, null, true); globalScene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: i18next.t(getStatKey(highestStat)) }), null, false, null, true);
} }
@ -2983,6 +2975,24 @@ export class PsychoShiftTag extends BattlerTag {
} }
} }
/**
* Tag associated with the move Magic Coat.
*/
export class MagicCoatTag extends BattlerTag {
constructor() {
super(BattlerTagType.MAGIC_COAT, BattlerTagLapseType.TURN_END, 1, Moves.MAGIC_COAT);
}
/**
* Queues the "[PokemonName] shrouded itself with Magic Coat" message when the tag is added.
* @param pokemon - The target {@linkcode Pokemon}
*/
override onAdd(pokemon: Pokemon) {
// "{pokemonNameWithAffix} shrouded itself with Magic Coat!"
globalScene.queueMessage(i18next.t("battlerTags:magicCoatOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
}
/** /**
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
* @param sourceId - The ID of the pokemon adding the tag * @param sourceId - The ID of the pokemon adding the tag
@ -3172,6 +3182,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new GrudgeTag(); return new GrudgeTag();
case BattlerTagType.PSYCHO_SHIFT: case BattlerTagType.PSYCHO_SHIFT:
return new PsychoShiftTag(); return new PsychoShiftTag();
case BattlerTagType.MAGIC_COAT:
return new MagicCoatTag();
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);

View File

@ -125,7 +125,9 @@ export enum MoveFlags {
/** Indicates a move is able to bypass its target's Substitute (if the target has one) */ /** Indicates a move is able to bypass its target's Substitute (if the target has one) */
IGNORE_SUBSTITUTE = 1 << 17, IGNORE_SUBSTITUTE = 1 << 17,
/** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */ /** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */
REDIRECT_COUNTER = 1 << 18, REDIRECT_COUNTER = 1 << 18,
/** Indicates a move is able to be reflected by {@linkcode Abilities.MAGIC_BOUNCE} and {@linkcode Moves.MAGIC_COAT} */
REFLECTABLE = 1 << 19,
} }
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
@ -610,6 +612,16 @@ export default class Move implements Localizable {
return this; return this;
} }
/**
* Sets the {@linkcode MoveFlags.REFLECTABLE} flag for the calling Move
* @see {@linkcode Moves.ATTRACT}
* @returns The {@linkcode Move} that called this function
*/
reflectable(): this {
this.setFlag(MoveFlags.REFLECTABLE, true);
return this;
}
/** /**
* Checks if the move flag applies to the pokemon(s) using/receiving the move * Checks if the move flag applies to the pokemon(s) using/receiving the move
* @param flag {@linkcode MoveFlags} MoveFlag to check on user and/or target * @param flag {@linkcode MoveFlags} MoveFlag to check on user and/or target
@ -4368,6 +4380,69 @@ export class CueNextRoundAttr extends MoveEffectAttr {
} }
} }
/**
* Attribute that changes stat stages before the damage is calculated
*/
export class StatChangeBeforeDmgCalcAttr extends MoveAttr {
/**
* Applies Stat Changes before damage is calculated
*
* @param user {@linkcode Pokemon} that called {@linkcode move}
* @param target {@linkcode Pokemon} that is the target of {@linkcode move}
* @param move {@linkcode Move} called by {@linkcode user}
* @param args N/A
*
* @returns true if stat stages where correctly applied
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return false;
}
}
/**
* Steals the postitive Stat stages of the target before damage calculation so stat changes
* apply to damage calculation (e.g. {@linkcode Moves.SPECTRAL_THIEF})
* {@link https://bulbapedia.bulbagarden.net/wiki/Spectral_Thief_(move) | Spectral Thief}
*/
export class SpectralThiefAttr extends StatChangeBeforeDmgCalcAttr {
/**
* steals max amount of positive stats of the target while not exceeding the limit of max 6 stat stages
*
* @param user {@linkcode Pokemon} that called {@linkcode move}
* @param target {@linkcode Pokemon} that is the target of {@linkcode move}
* @param move {@linkcode Move} called by {@linkcode user}
* @param args N/A
*
* @returns true if stat stages where correctly stolen
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
/**
* Copy all positive stat stages to user and reduce copied stat stages on target.
*/
for (const s of BATTLE_STATS) {
const statStageValueTarget = target.getStatStage(s);
const statStageValueUser = user.getStatStage(s);
if (statStageValueTarget > 0) {
/**
* Only value of up to 6 can be stolen (stat stages don't exceed 6)
*/
const availableToSteal = Math.min(statStageValueTarget, 6 - statStageValueUser);
globalScene.unshiftPhase(new StatStageChangePhase(user.getBattlerIndex(), this.selfTarget, [ s ], availableToSteal));
target.setStatStage(s, statStageValueTarget - availableToSteal);
}
}
target.updateInfo();
user.updateInfo();
globalScene.queueMessage(i18next.t("moveTriggers:stealPositiveStats", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
return true;
}
}
export class VariableAtkAttr extends MoveAttr { export class VariableAtkAttr extends MoveAttr {
constructor() { constructor() {
super(); super();
@ -4559,7 +4634,8 @@ export class TeraMoveCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.NumberHolder); const category = (args[0] as Utils.NumberHolder);
if (user.isTerastallized && user.getEffectiveStat(Stat.ATK, target, move) > user.getEffectiveStat(Stat.SPATK, target, move)) { if (user.isTerastallized && user.getEffectiveStat(Stat.ATK, target, move, true, true, false, false, true) >
user.getEffectiveStat(Stat.SPATK, target, move, true, true, false, false, true)) {
category.value = MoveCategory.PHYSICAL; category.value = MoveCategory.PHYSICAL;
return true; return true;
} }
@ -5331,6 +5407,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
case BattlerTagType.INGRAIN: case BattlerTagType.INGRAIN:
case BattlerTagType.IGNORE_ACCURACY: case BattlerTagType.IGNORE_ACCURACY:
case BattlerTagType.AQUA_RING: case BattlerTagType.AQUA_RING:
case BattlerTagType.MAGIC_COAT:
return 3; return 3;
case BattlerTagType.PROTECTED: case BattlerTagType.PROTECTED:
case BattlerTagType.FLYING: case BattlerTagType.FLYING:
@ -8333,7 +8410,8 @@ export function initMoves() {
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.ignoresSubstitute() .ignoresSubstitute()
.hidesTarget() .hidesTarget()
.windMove(), .windMove()
.reflectable(),
new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
.chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
@ -8357,7 +8435,8 @@ export function initMoves() {
new AttackMove(Moves.ROLLING_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1) new AttackMove(Moves.ROLLING_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1)
.attr(FlinchAttr), .attr(FlinchAttr),
new StatusMove(Moves.SAND_ATTACK, Type.GROUND, 100, 15, -1, 0, 1) new StatusMove(Moves.SAND_ATTACK, Type.GROUND, 100, 15, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.reflectable(),
new AttackMove(Moves.HEADBUTT, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 15, 30, 0, 1) new AttackMove(Moves.HEADBUTT, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 15, 30, 0, 1)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.HORN_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1), new AttackMove(Moves.HORN_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1),
@ -8386,7 +8465,8 @@ export function initMoves() {
.recklessMove(), .recklessMove(),
new StatusMove(Moves.TAIL_WHIP, Type.NORMAL, 100, 30, -1, 0, 1) new StatusMove(Moves.TAIL_WHIP, Type.NORMAL, 100, 30, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.POISON_STING, Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, 30, 0, 1) new AttackMove(Moves.POISON_STING, Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, 30, 0, 1)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.makesContact(false), .makesContact(false),
@ -8399,30 +8479,36 @@ export function initMoves() {
.makesContact(false), .makesContact(false),
new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 1) new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.BITE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, 30, 0, 1) new AttackMove(Moves.BITE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, 30, 0, 1)
.attr(FlinchAttr) .attr(FlinchAttr)
.bitingMove(), .bitingMove(),
new StatusMove(Moves.GROWL, Type.NORMAL, 100, 40, -1, 0, 1) new StatusMove(Moves.GROWL, Type.NORMAL, 100, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.soundBased() .soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1) new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.soundBased() .soundBased()
.hidesTarget(), .hidesTarget()
.reflectable(),
new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1) new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.SUPERSONIC, Type.NORMAL, 55, 20, -1, 0, 1) new StatusMove(Moves.SUPERSONIC, Type.NORMAL, 55, 20, -1, 0, 1)
.attr(ConfuseAttr) .attr(ConfuseAttr)
.soundBased(), .soundBased()
.reflectable(),
new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1) new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1)
.attr(FixedDamageAttr, 20), .attr(FixedDamageAttr, 20),
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1) new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1)
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
.condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined) .condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -8475,7 +8561,8 @@ export function initMoves() {
.triageMove(), .triageMove(),
new StatusMove(Moves.LEECH_SEED, Type.GRASS, 90, 10, -1, 0, 1) new StatusMove(Moves.LEECH_SEED, Type.GRASS, 90, 10, -1, 0, 1)
.attr(LeechSeedAttr) .attr(LeechSeedAttr)
.condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS)), .condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS))
.reflectable(),
new SelfStatusMove(Moves.GROWTH, Type.NORMAL, -1, 20, -1, 0, 1) new SelfStatusMove(Moves.GROWTH, Type.NORMAL, -1, 20, -1, 0, 1)
.attr(GrowthStatStageChangeAttr), .attr(GrowthStatStageChangeAttr),
new AttackMove(Moves.RAZOR_LEAF, Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, 0, 1) new AttackMove(Moves.RAZOR_LEAF, Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, 0, 1)
@ -8489,13 +8576,16 @@ export function initMoves() {
.attr(AntiSunlightPowerDecreaseAttr), .attr(AntiSunlightPowerDecreaseAttr),
new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1) new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.powderMove(), .powderMove()
.reflectable(),
new StatusMove(Moves.STUN_SPORE, Type.GRASS, 75, 30, -1, 0, 1) new StatusMove(Moves.STUN_SPORE, Type.GRASS, 75, 30, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.powderMove(), .powderMove()
.reflectable(),
new StatusMove(Moves.SLEEP_POWDER, Type.GRASS, 75, 15, -1, 0, 1) new StatusMove(Moves.SLEEP_POWDER, Type.GRASS, 75, 15, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.powderMove(), .powderMove()
.reflectable(),
new AttackMove(Moves.PETAL_DANCE, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) new AttackMove(Moves.PETAL_DANCE, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1)
.attr(FrenzyAttr) .attr(FrenzyAttr)
.attr(MissEffectAttr, frenzyMissFunc) .attr(MissEffectAttr, frenzyMissFunc)
@ -8505,7 +8595,8 @@ export function initMoves() {
.target(MoveTarget.RANDOM_NEAR_ENEMY), .target(MoveTarget.RANDOM_NEAR_ENEMY),
new StatusMove(Moves.STRING_SHOT, Type.BUG, 95, 40, -1, 0, 1) new StatusMove(Moves.STRING_SHOT, Type.BUG, 95, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPD ], -2) .attr(StatStageChangeAttr, [ Stat.SPD ], -2)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.DRAGON_RAGE, Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 1) new AttackMove(Moves.DRAGON_RAGE, Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 1)
.attr(FixedDamageAttr, 40), .attr(FixedDamageAttr, 40),
new AttackMove(Moves.FIRE_SPIN, Type.FIRE, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 1) new AttackMove(Moves.FIRE_SPIN, Type.FIRE, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 1)
@ -8516,7 +8607,8 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1) new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.attr(RespectAttackTypeImmunityAttr), .attr(RespectAttackTypeImmunityAttr)
.reflectable(),
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)
@ -8538,13 +8630,15 @@ export function initMoves() {
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND), .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND),
new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1) new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.TOXIC) .attr(StatusEffectAttr, StatusEffect.TOXIC)
.attr(ToxicAccuracyAttr), .attr(ToxicAccuracyAttr)
.reflectable(),
new AttackMove(Moves.CONFUSION, Type.PSYCHIC, MoveCategory.SPECIAL, 50, 100, 25, 10, 0, 1) new AttackMove(Moves.CONFUSION, Type.PSYCHIC, MoveCategory.SPECIAL, 50, 100, 25, 10, 0, 1)
.attr(ConfuseAttr), .attr(ConfuseAttr),
new AttackMove(Moves.PSYCHIC, Type.PSYCHIC, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) new AttackMove(Moves.PSYCHIC, Type.PSYCHIC, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1), .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1),
new StatusMove(Moves.HYPNOSIS, Type.PSYCHIC, 60, 20, -1, 0, 1) new StatusMove(Moves.HYPNOSIS, Type.PSYCHIC, 60, 20, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP), .attr(StatusEffectAttr, StatusEffect.SLEEP)
.reflectable(),
new SelfStatusMove(Moves.MEDITATE, Type.PSYCHIC, -1, 40, -1, 0, 1) new SelfStatusMove(Moves.MEDITATE, Type.PSYCHIC, -1, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true), .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true),
new SelfStatusMove(Moves.AGILITY, Type.PSYCHIC, -1, 30, -1, 0, 1) new SelfStatusMove(Moves.AGILITY, Type.PSYCHIC, -1, 30, -1, 0, 1)
@ -8562,7 +8656,8 @@ export function initMoves() {
.ignoresSubstitute(), .ignoresSubstitute(),
new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1) new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], -2) .attr(StatStageChangeAttr, [ Stat.DEF ], -2)
.soundBased(), .soundBased()
.reflectable(),
new SelfStatusMove(Moves.DOUBLE_TEAM, Type.NORMAL, -1, 15, -1, 0, 1) new SelfStatusMove(Moves.DOUBLE_TEAM, Type.NORMAL, -1, 15, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.EVA ], 1, true), .attr(StatStageChangeAttr, [ Stat.EVA ], 1, true),
new SelfStatusMove(Moves.RECOVER, Type.NORMAL, -1, 5, -1, 0, 1) new SelfStatusMove(Moves.RECOVER, Type.NORMAL, -1, 5, -1, 0, 1)
@ -8574,9 +8669,11 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.MINIMIZED, true, false) .attr(AddBattlerTagAttr, BattlerTagType.MINIMIZED, true, false)
.attr(StatStageChangeAttr, [ Stat.EVA ], 2, true), .attr(StatStageChangeAttr, [ Stat.EVA ], 2, true),
new StatusMove(Moves.SMOKESCREEN, Type.NORMAL, 100, 20, -1, 0, 1) new StatusMove(Moves.SMOKESCREEN, Type.NORMAL, 100, 20, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.reflectable(),
new StatusMove(Moves.CONFUSE_RAY, Type.GHOST, 100, 10, -1, 0, 1) new StatusMove(Moves.CONFUSE_RAY, Type.GHOST, 100, 10, -1, 0, 1)
.attr(ConfuseAttr), .attr(ConfuseAttr)
.reflectable(),
new SelfStatusMove(Moves.WITHDRAW, Type.WATER, -1, 40, -1, 0, 1) new SelfStatusMove(Moves.WITHDRAW, Type.WATER, -1, 40, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new SelfStatusMove(Moves.DEFENSE_CURL, Type.NORMAL, -1, 40, -1, 0, 1) new SelfStatusMove(Moves.DEFENSE_CURL, Type.NORMAL, -1, 40, -1, 0, 1)
@ -8637,7 +8734,8 @@ export function initMoves() {
new SelfStatusMove(Moves.AMNESIA, Type.PSYCHIC, -1, 20, -1, 0, 1) new SelfStatusMove(Moves.AMNESIA, Type.PSYCHIC, -1, 20, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 2, true), .attr(StatStageChangeAttr, [ Stat.SPDEF ], 2, true),
new StatusMove(Moves.KINESIS, Type.PSYCHIC, 80, 15, -1, 0, 1) new StatusMove(Moves.KINESIS, Type.PSYCHIC, 80, 15, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.reflectable(),
new SelfStatusMove(Moves.SOFT_BOILED, Type.NORMAL, -1, 5, -1, 0, 1) new SelfStatusMove(Moves.SOFT_BOILED, Type.NORMAL, -1, 5, -1, 0, 1)
.attr(HealAttr, 0.5) .attr(HealAttr, 0.5)
.triageMove(), .triageMove(),
@ -8647,14 +8745,16 @@ export function initMoves() {
.condition(failOnGravityCondition) .condition(failOnGravityCondition)
.recklessMove(), .recklessMove(),
new StatusMove(Moves.GLARE, Type.NORMAL, 100, 30, -1, 0, 1) new StatusMove(Moves.GLARE, Type.NORMAL, 100, 30, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.reflectable(),
new AttackMove(Moves.DREAM_EATER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 15, -1, 0, 1) new AttackMove(Moves.DREAM_EATER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 15, -1, 0, 1)
.attr(HitHealAttr) .attr(HitHealAttr)
.condition(targetSleptOrComatoseCondition) .condition(targetSleptOrComatoseCondition)
.triageMove(), .triageMove(),
new StatusMove(Moves.POISON_GAS, Type.POISON, 90, 40, -1, 0, 1) new StatusMove(Moves.POISON_GAS, Type.POISON, 90, 40, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.BARRAGE, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) new AttackMove(Moves.BARRAGE, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1)
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false) .makesContact(false)
@ -8663,7 +8763,8 @@ export function initMoves() {
.attr(HitHealAttr) .attr(HitHealAttr)
.triageMove(), .triageMove(),
new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1) new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP), .attr(StatusEffectAttr, StatusEffect.SLEEP)
.reflectable(),
new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1)
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.attr(HighCritAttr) .attr(HighCritAttr)
@ -8682,9 +8783,11 @@ export function initMoves() {
.punchingMove(), .punchingMove(),
new StatusMove(Moves.SPORE, Type.GRASS, 100, 15, -1, 0, 1) new StatusMove(Moves.SPORE, Type.GRASS, 100, 15, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.powderMove(), .powderMove()
.reflectable(),
new StatusMove(Moves.FLASH, Type.NORMAL, 100, 20, -1, 0, 1) new StatusMove(Moves.FLASH, Type.NORMAL, 100, 20, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1)
.reflectable(),
new AttackMove(Moves.PSYWAVE, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) new AttackMove(Moves.PSYWAVE, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1)
.attr(RandomLevelDamageAttr), .attr(RandomLevelDamageAttr),
new SelfStatusMove(Moves.SPLASH, Type.NORMAL, -1, 40, -1, 0, 1) new SelfStatusMove(Moves.SPLASH, Type.NORMAL, -1, 40, -1, 0, 1)
@ -8743,7 +8846,8 @@ export function initMoves() {
.attr(StealHeldItemChanceAttr, 0.3), .attr(StealHeldItemChanceAttr, 0.3),
new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2) new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2)
.condition(failIfGhostTypeCondition) .condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
.reflectable(),
new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2) new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(IgnoreAccuracyAttr), .attr(IgnoreAccuracyAttr),
new StatusMove(Moves.NIGHTMARE, Type.GHOST, 100, 15, -1, 0, 2) new StatusMove(Moves.NIGHTMARE, Type.GHOST, 100, 15, -1, 0, 2)
@ -8774,12 +8878,14 @@ export function initMoves() {
new StatusMove(Moves.COTTON_SPORE, Type.GRASS, 100, 40, -1, 0, 2) new StatusMove(Moves.COTTON_SPORE, Type.GRASS, 100, 40, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.SPD ], -2) .attr(StatStageChangeAttr, [ Stat.SPD ], -2)
.powderMove() .powderMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2) new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2)
.attr(LowHpPowerAttr), .attr(LowHpPowerAttr),
new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2) new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2)
.ignoresSubstitute() .ignoresSubstitute()
.attr(ReducePpMoveAttr, 4), .attr(ReducePpMoveAttr, 4)
.reflectable(),
new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2) new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2)
.attr(StatusEffectAttr, StatusEffect.FREEZE) .attr(StatusEffectAttr, StatusEffect.FREEZE)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -8789,10 +8895,12 @@ export function initMoves() {
new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2) new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2)
.punchingMove(), .punchingMove(),
new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2) new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.SPD ], -2), .attr(StatStageChangeAttr, [ Stat.SPD ], -2)
.reflectable(),
new AttackMove(Moves.FEINT_ATTACK, Type.DARK, MoveCategory.PHYSICAL, 60, -1, 20, -1, 0, 2), new AttackMove(Moves.FEINT_ATTACK, Type.DARK, MoveCategory.PHYSICAL, 60, -1, 20, -1, 0, 2),
new StatusMove(Moves.SWEET_KISS, Type.FAIRY, 75, 10, -1, 0, 2) new StatusMove(Moves.SWEET_KISS, Type.FAIRY, 75, 10, -1, 0, 2)
.attr(ConfuseAttr), .attr(ConfuseAttr)
.reflectable(),
new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2) new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2)
.attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => { .attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => {
globalScene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })); globalScene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }));
@ -8807,13 +8915,15 @@ export function initMoves() {
.ballBombMove(), .ballBombMove(),
new StatusMove(Moves.SPIKES, Type.GROUND, -1, 20, -1, 0, 2) new StatusMove(Moves.SPIKES, Type.GROUND, -1, 20, -1, 0, 2)
.attr(AddArenaTrapTagAttr, ArenaTagType.SPIKES) .attr(AddArenaTrapTagAttr, ArenaTagType.SPIKES)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE)
.reflectable(),
new AttackMove(Moves.ZAP_CANNON, Type.ELECTRIC, MoveCategory.SPECIAL, 120, 50, 5, 100, 0, 2) new AttackMove(Moves.ZAP_CANNON, Type.ELECTRIC, MoveCategory.SPECIAL, 120, 50, 5, 100, 0, 2)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.ballBombMove(), .ballBombMove(),
new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2) new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2) new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2)
.ignoresProtect() .ignoresProtect()
.attr(DestinyBondAttr) .attr(DestinyBondAttr)
@ -8859,7 +8969,8 @@ export function initMoves() {
.attr(ProtectAttr, BattlerTagType.ENDURING) .attr(ProtectAttr, BattlerTagType.ENDURING)
.condition(failIfLastCondition), .condition(failIfLastCondition),
new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2) new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.ATK ], -2), .attr(StatStageChangeAttr, [ Stat.ATK ], -2)
.reflectable(),
new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2) new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2)
.partial() // Does not lock the user, also does not increase damage properly .partial() // Does not lock the user, also does not increase damage properly
.attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL), .attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL),
@ -8867,7 +8978,8 @@ export function initMoves() {
.attr(SurviveDamageAttr), .attr(SurviveDamageAttr),
new StatusMove(Moves.SWAGGER, Type.NORMAL, 85, 15, -1, 0, 2) new StatusMove(Moves.SWAGGER, Type.NORMAL, 85, 15, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.ATK ], 2) .attr(StatStageChangeAttr, [ Stat.ATK ], 2)
.attr(ConfuseAttr), .attr(ConfuseAttr)
.reflectable(),
new SelfStatusMove(Moves.MILK_DRINK, Type.NORMAL, -1, 5, -1, 0, 2) new SelfStatusMove(Moves.MILK_DRINK, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(HealAttr, 0.5) .attr(HealAttr, 0.5)
.triageMove(), .triageMove(),
@ -8880,11 +8992,13 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2) new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2)
.condition(failIfGhostTypeCondition) .condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
.reflectable(),
new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2) new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.INFATUATED) .attr(AddBattlerTagAttr, BattlerTagType.INFATUATED)
.ignoresSubstitute() .ignoresSubstitute()
.condition((user, target, move) => user.isOppositeGender(target)), .condition((user, target, move) => user.isOppositeGender(target))
.reflectable(),
new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2) new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2)
.attr(BypassSleepAttr) .attr(BypassSleepAttr)
.attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false) .attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false)
@ -8931,7 +9045,8 @@ export function initMoves() {
new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
.ignoresSubstitute() .ignoresSubstitute()
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target)), .condition((user, target, move) => new EncoreTag(user.id).canAdd(target))
.reflectable(),
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(), // No effect implemented .partial(), // No effect implemented
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)
@ -8952,7 +9067,8 @@ export function initMoves() {
.attr(RemoveArenaTrapAttr), .attr(RemoveArenaTrapAttr),
new StatusMove(Moves.SWEET_SCENT, Type.NORMAL, 100, 20, -1, 0, 2) new StatusMove(Moves.SWEET_SCENT, Type.NORMAL, 100, 20, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.EVA ], -2) .attr(StatStageChangeAttr, [ Stat.EVA ], -2)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.IRON_TAIL, Type.STEEL, MoveCategory.PHYSICAL, 100, 75, 15, 30, 0, 2) new AttackMove(Moves.IRON_TAIL, Type.STEEL, MoveCategory.PHYSICAL, 100, 75, 15, 30, 0, 2)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1), .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.METAL_CLAW, Type.STEEL, MoveCategory.PHYSICAL, 50, 95, 35, 10, 0, 2) new AttackMove(Moves.METAL_CLAW, Type.STEEL, MoveCategory.PHYSICAL, 50, 95, 35, 10, 0, 2)
@ -9040,12 +9156,15 @@ export function initMoves() {
new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3)
.ignoresSubstitute() .ignoresSubstitute()
.edgeCase() // Incomplete implementation because of Uproar's partial implementation .edgeCase() // Incomplete implementation because of Uproar's partial implementation
.attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1)
.reflectable(),
new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1)
.attr(ConfuseAttr), .attr(ConfuseAttr)
.reflectable(),
new StatusMove(Moves.WILL_O_WISP, Type.FIRE, 85, 15, -1, 0, 3) new StatusMove(Moves.WILL_O_WISP, Type.FIRE, 85, 15, -1, 0, 3)
.attr(StatusEffectAttr, StatusEffect.BURN), .attr(StatusEffectAttr, StatusEffect.BURN)
.reflectable(),
new StatusMove(Moves.MEMENTO, Type.DARK, 100, 10, -1, 0, 3) new StatusMove(Moves.MEMENTO, Type.DARK, 100, 10, -1, 0, 3)
.attr(SacrificialAttrOnHit) .attr(SacrificialAttrOnHit)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -2), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -2),
@ -9069,7 +9188,8 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false),
new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3) new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3)
.ignoresSubstitute() .ignoresSubstitute()
.attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4), .attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4)
.reflectable(),
new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3)
.attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND)
.ignoresSubstitute() .ignoresSubstitute()
@ -9092,7 +9212,12 @@ export function initMoves() {
new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3) new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true),
new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3) new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3)
.unimplemented(), .attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, true, 0)
.condition(failIfLastCondition)
// Interactions with stomping tantrum, instruct, and other moves that
// rely on move history
// Also will not reflect roar / whirlwind if the target has ForceSwitchOutImmunityAbAttr
.edgeCase(),
new SelfStatusMove(Moves.RECYCLE, Type.NORMAL, -1, 10, -1, 0, 3) new SelfStatusMove(Moves.RECYCLE, Type.NORMAL, -1, 10, -1, 0, 3)
.unimplemented(), .unimplemented(),
new AttackMove(Moves.REVENGE, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 3) new AttackMove(Moves.REVENGE, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 3)
@ -9101,7 +9226,8 @@ export function initMoves() {
.attr(RemoveScreensAttr), .attr(RemoveScreensAttr),
new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3) new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3)
.attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) .attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true)
.condition((user, target, move) => !target.status && !target.isSafeguarded(user)), .condition((user, target, move) => !target.status && !target.isSafeguarded(user))
.reflectable(),
new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false), .attr(RemoveHeldItemAttr, false),
@ -9145,7 +9271,8 @@ export function initMoves() {
.ballBombMove(), .ballBombMove(),
new StatusMove(Moves.FEATHER_DANCE, Type.FLYING, 100, 15, -1, 0, 3) new StatusMove(Moves.FEATHER_DANCE, Type.FLYING, 100, 15, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK ], -2) .attr(StatStageChangeAttr, [ Stat.ATK ], -2)
.danceMove(), .danceMove()
.reflectable(),
new StatusMove(Moves.TEETER_DANCE, Type.NORMAL, 100, 20, -1, 0, 3) new StatusMove(Moves.TEETER_DANCE, Type.NORMAL, 100, 20, -1, 0, 3)
.attr(ConfuseAttr) .attr(ConfuseAttr)
.danceMove() .danceMove()
@ -9191,7 +9318,8 @@ export function initMoves() {
.attr(PartyStatusCureAttr, i18next.t("moveTriggers:soothingAromaWaftedThroughArea"), Abilities.SAP_SIPPER) .attr(PartyStatusCureAttr, i18next.t("moveTriggers:soothingAromaWaftedThroughArea"), Abilities.SAP_SIPPER)
.target(MoveTarget.PARTY), .target(MoveTarget.PARTY),
new StatusMove(Moves.FAKE_TEARS, Type.DARK, 100, 20, -1, 0, 3) new StatusMove(Moves.FAKE_TEARS, Type.DARK, 100, 20, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2)
.reflectable(),
new AttackMove(Moves.AIR_CUTTER, Type.FLYING, MoveCategory.SPECIAL, 60, 95, 25, -1, 0, 3) new AttackMove(Moves.AIR_CUTTER, Type.FLYING, MoveCategory.SPECIAL, 60, 95, 25, -1, 0, 3)
.attr(HighCritAttr) .attr(HighCritAttr)
.slicingMove() .slicingMove()
@ -9202,7 +9330,8 @@ export function initMoves() {
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE), .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE),
new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3) new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3) new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.makesContact(false), .makesContact(false),
@ -9211,12 +9340,15 @@ export function initMoves() {
.windMove(), .windMove(),
new StatusMove(Moves.METAL_SOUND, Type.STEEL, 85, 40, -1, 0, 3) new StatusMove(Moves.METAL_SOUND, Type.STEEL, 85, 40, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.GRASS_WHISTLE, Type.GRASS, 55, 15, -1, 0, 3) new StatusMove(Moves.GRASS_WHISTLE, Type.GRASS, 55, 15, -1, 0, 3)
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.TICKLE, Type.NORMAL, 100, 20, -1, 0, 3) new StatusMove(Moves.TICKLE, Type.NORMAL, 100, 20, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1)
.reflectable(),
new SelfStatusMove(Moves.COSMIC_POWER, Type.PSYCHIC, -1, 20, -1, 0, 3) new SelfStatusMove(Moves.COSMIC_POWER, Type.PSYCHIC, -1, 20, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true),
new AttackMove(Moves.WATER_SPOUT, Type.WATER, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 3) new AttackMove(Moves.WATER_SPOUT, Type.WATER, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 3)
@ -9254,7 +9386,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3) new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3)
.condition(failIfGhostTypeCondition) .condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
.reflectable(),
new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3) new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1) .attr(StatStageChangeAttr, [ Stat.ATK ], 1)
.soundBased() .soundBased()
@ -9317,7 +9450,8 @@ export function initMoves() {
.target(MoveTarget.BOTH_SIDES), .target(MoveTarget.BOTH_SIDES),
new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4) new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4)
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4) new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4)
.attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1)
.attr(HealStatusEffectAttr, false, StatusEffect.SLEEP), .attr(HealStatusEffectAttr, false, StatusEffect.SLEEP),
@ -9363,6 +9497,7 @@ export function initMoves() {
new AttackMove(Moves.ASSURANCE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 4) new AttackMove(Moves.ASSURANCE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 4)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.turnData.damageTaken > 0 ? 2 : 1), .attr(MovePowerMultiplierAttr, (user, target, move) => target.turnData.damageTaken > 0 ? 2 : 1),
new StatusMove(Moves.EMBARGO, Type.DARK, 100, 15, -1, 0, 4) new StatusMove(Moves.EMBARGO, Type.DARK, 100, 15, -1, 0, 4)
.reflectable()
.unimplemented(), .unimplemented(),
new AttackMove(Moves.FLING, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4) new AttackMove(Moves.FLING, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4)
.makesContact(false) .makesContact(false)
@ -9382,14 +9517,16 @@ export function initMoves() {
.attr(LessPPMorePowerAttr), .attr(LessPPMorePowerAttr),
new StatusMove(Moves.HEAL_BLOCK, Type.PSYCHIC, 100, 15, -1, 0, 4) new StatusMove(Moves.HEAL_BLOCK, Type.PSYCHIC, 100, 15, -1, 0, 4)
.attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, true, 5) .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, true, 5)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.WRING_OUT, Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 4) new AttackMove(Moves.WRING_OUT, Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 4)
.attr(OpponentHighHpPowerAttr, 120) .attr(OpponentHighHpPowerAttr, 120)
.makesContact(), .makesContact(),
new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4) new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4)
.attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true), .attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true),
new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4) new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4)
.attr(SuppressAbilitiesAttr), .attr(SuppressAbilitiesAttr)
.reflectable(),
new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4) new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4)
.attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true) .attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true)
.target(MoveTarget.USER_SIDE), .target(MoveTarget.USER_SIDE),
@ -9411,12 +9548,14 @@ export function initMoves() {
new AttackMove(Moves.LAST_RESORT, Type.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4) new AttackMove(Moves.LAST_RESORT, Type.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4)
.attr(LastResortAttr), .attr(LastResortAttr),
new StatusMove(Moves.WORRY_SEED, Type.GRASS, 100, 10, -1, 0, 4) new StatusMove(Moves.WORRY_SEED, Type.GRASS, 100, 10, -1, 0, 4)
.attr(AbilityChangeAttr, Abilities.INSOMNIA), .attr(AbilityChangeAttr, Abilities.INSOMNIA)
.reflectable(),
new AttackMove(Moves.SUCKER_PUNCH, Type.DARK, MoveCategory.PHYSICAL, 70, 100, 5, -1, 1, 4) new AttackMove(Moves.SUCKER_PUNCH, Type.DARK, MoveCategory.PHYSICAL, 70, 100, 5, -1, 1, 4)
.condition((user, target, move) => globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS), // TODO: is this bang correct? .condition((user, target, move) => globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS), // TODO: is this bang correct?
new StatusMove(Moves.TOXIC_SPIKES, Type.POISON, -1, 20, -1, 0, 4) new StatusMove(Moves.TOXIC_SPIKES, Type.POISON, -1, 20, -1, 0, 4)
.attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES) .attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE)
.reflectable(),
new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4) new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4)
.attr(SwapStatStagesAttr, BATTLE_STATS) .attr(SwapStatStagesAttr, BATTLE_STATS)
.ignoresSubstitute(), .ignoresSubstitute(),
@ -9528,7 +9667,8 @@ export function initMoves() {
.attr(ClearTerrainAttr) .attr(ClearTerrainAttr)
.attr(RemoveScreensAttr, false) .attr(RemoveScreensAttr, false)
.attr(RemoveArenaTrapAttr, true) .attr(RemoveArenaTrapAttr, true)
.attr(RemoveArenaTagsAttr, [ ArenaTagType.MIST, ArenaTagType.SAFEGUARD ], false), .attr(RemoveArenaTagsAttr, [ ArenaTagType.MIST, ArenaTagType.SAFEGUARD ], false)
.reflectable(),
new StatusMove(Moves.TRICK_ROOM, Type.PSYCHIC, -1, 5, -1, -7, 4) new StatusMove(Moves.TRICK_ROOM, Type.PSYCHIC, -1, 5, -1, -7, 4)
.attr(AddArenaTagAttr, ArenaTagType.TRICK_ROOM, 5) .attr(AddArenaTagAttr, ArenaTagType.TRICK_ROOM, 5)
.ignoresProtect() .ignoresProtect()
@ -9566,10 +9706,12 @@ export function initMoves() {
new StatusMove(Moves.CAPTIVATE, Type.NORMAL, 100, 20, -1, 0, 4) new StatusMove(Moves.CAPTIVATE, Type.NORMAL, 100, 20, -1, 0, 4)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2)
.condition((user, target, move) => target.isOppositeGender(user)) .condition((user, target, move) => target.isOppositeGender(user))
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new StatusMove(Moves.STEALTH_ROCK, Type.ROCK, -1, 20, -1, 0, 4) new StatusMove(Moves.STEALTH_ROCK, Type.ROCK, -1, 20, -1, 0, 4)
.attr(AddArenaTrapTagAttr, ArenaTagType.STEALTH_ROCK) .attr(AddArenaTrapTagAttr, ArenaTagType.STEALTH_ROCK)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE)
.reflectable(),
new AttackMove(Moves.GRASS_KNOT, Type.GRASS, MoveCategory.SPECIAL, -1, 100, 20, -1, 0, 4) new AttackMove(Moves.GRASS_KNOT, Type.GRASS, MoveCategory.SPECIAL, -1, 100, 20, -1, 0, 4)
.attr(WeightPowerAttr) .attr(WeightPowerAttr)
.makesContact(), .makesContact(),
@ -9613,7 +9755,8 @@ export function initMoves() {
.attr(TrapAttr, BattlerTagType.MAGMA_STORM), .attr(TrapAttr, BattlerTagType.MAGMA_STORM),
new StatusMove(Moves.DARK_VOID, Type.DARK, 80, 10, -1, 0, 4) //Accuracy from Generations 4-6 new StatusMove(Moves.DARK_VOID, Type.DARK, 80, 10, -1, 0, 4) //Accuracy from Generations 4-6
.attr(StatusEffectAttr, StatusEffect.SLEEP) .attr(StatusEffectAttr, StatusEffect.SLEEP)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new AttackMove(Moves.SEED_FLARE, Type.GRASS, MoveCategory.SPECIAL, 120, 85, 5, 40, 0, 4) new AttackMove(Moves.SEED_FLARE, Type.GRASS, MoveCategory.SPECIAL, 120, 85, 5, 40, 0, 4)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4) new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4)
@ -9653,7 +9796,8 @@ export function initMoves() {
.condition((_user, target, _move) => !(target.species.speciesId === Species.GENGAR && target.getFormKey() === "mega")) .condition((_user, target, _move) => !(target.species.speciesId === Species.GENGAR && target.getFormKey() === "mega"))
.condition((_user, target, _move) => Utils.isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && Utils.isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING))) .condition((_user, target, _move) => Utils.isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && Utils.isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING)))
.attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3) .attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3)
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3), .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3)
.reflectable(),
new StatusMove(Moves.MAGIC_ROOM, Type.PSYCHIC, -1, 10, -1, 0, 5) new StatusMove(Moves.MAGIC_ROOM, Type.PSYCHIC, -1, 10, -1, 0, 5)
.ignoresProtect() .ignoresProtect()
.target(MoveTarget.BOTH_SIDES) .target(MoveTarget.BOTH_SIDES)
@ -9686,7 +9830,8 @@ export function initMoves() {
.attr(ElectroBallPowerAttr) .attr(ElectroBallPowerAttr)
.ballBombMove(), .ballBombMove(),
new StatusMove(Moves.SOAK, Type.WATER, 100, 20, -1, 0, 5) new StatusMove(Moves.SOAK, Type.WATER, 100, 20, -1, 0, 5)
.attr(ChangeTypeAttr, Type.WATER), .attr(ChangeTypeAttr, Type.WATER)
.reflectable(),
new AttackMove(Moves.FLAME_CHARGE, Type.FIRE, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 5) new AttackMove(Moves.FLAME_CHARGE, Type.FIRE, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 5)
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true), .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true),
new SelfStatusMove(Moves.COIL, Type.POISON, -1, 20, -1, 0, 5) new SelfStatusMove(Moves.COIL, Type.POISON, -1, 20, -1, 0, 5)
@ -9699,9 +9844,11 @@ export function initMoves() {
new AttackMove(Moves.FOUL_PLAY, Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, -1, 0, 5) new AttackMove(Moves.FOUL_PLAY, Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, -1, 0, 5)
.attr(TargetAtkUserAtkAttr), .attr(TargetAtkUserAtkAttr),
new StatusMove(Moves.SIMPLE_BEAM, Type.NORMAL, 100, 15, -1, 0, 5) new StatusMove(Moves.SIMPLE_BEAM, Type.NORMAL, 100, 15, -1, 0, 5)
.attr(AbilityChangeAttr, Abilities.SIMPLE), .attr(AbilityChangeAttr, Abilities.SIMPLE)
.reflectable(),
new StatusMove(Moves.ENTRAINMENT, Type.NORMAL, 100, 15, -1, 0, 5) new StatusMove(Moves.ENTRAINMENT, Type.NORMAL, 100, 15, -1, 0, 5)
.attr(AbilityGiveAttr), .attr(AbilityGiveAttr)
.reflectable(),
new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5) new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5)
.ignoresProtect() .ignoresProtect()
.ignoresSubstitute() .ignoresSubstitute()
@ -9739,7 +9886,8 @@ export function initMoves() {
new StatusMove(Moves.HEAL_PULSE, Type.PSYCHIC, -1, 10, -1, 0, 5) new StatusMove(Moves.HEAL_PULSE, Type.PSYCHIC, -1, 10, -1, 0, 5)
.attr(HealAttr, 0.5, false, false) .attr(HealAttr, 0.5, false, false)
.pulseMove() .pulseMove()
.triageMove(), .triageMove()
.reflectable(),
new AttackMove(Moves.HEX, Type.GHOST, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 5) new AttackMove(Moves.HEX, Type.GHOST, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 5)
.attr( .attr(
MovePowerMultiplierAttr, MovePowerMultiplierAttr,
@ -9942,7 +10090,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }),
new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6)
.attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB)
.target(MoveTarget.ENEMY_SIDE), .target(MoveTarget.ENEMY_SIDE)
.reflectable(),
new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6) new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6)
.attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ), .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ),
new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
@ -9950,10 +10099,12 @@ export function initMoves() {
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
.ignoresProtect(), .ignoresProtect(),
new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GHOST), .attr(AddTypeAttr, Type.GHOST)
.reflectable(),
new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6) new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6)
.attr(AddArenaTagAttr, ArenaTagType.ION_DELUGE) .attr(AddArenaTagAttr, ArenaTagType.ION_DELUGE)
.target(MoveTarget.BOTH_SIDES), .target(MoveTarget.BOTH_SIDES),
@ -9962,7 +10113,8 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.triageMove(), .triageMove(),
new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6) new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6)
.attr(AddTypeAttr, Type.GRASS), .attr(AddTypeAttr, Type.GRASS)
.reflectable(),
new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6) new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6)
.windMove() .windMove()
.makesContact(false) .makesContact(false)
@ -9976,9 +10128,11 @@ export function initMoves() {
new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY }) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY })
.attr(ForceSwitchOutAttr, true) .attr(ForceSwitchOutAttr, true)
.soundBased(), .soundBased()
.reflectable(),
new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6)
.attr(InvertStatsAttr), .attr(InvertStatsAttr)
.reflectable(),
new AttackMove(Moves.DRAINING_KISS, Type.FAIRY, MoveCategory.SPECIAL, 50, 100, 10, -1, 0, 6) new AttackMove(Moves.DRAINING_KISS, Type.FAIRY, MoveCategory.SPECIAL, 50, 100, 10, -1, 0, 6)
.attr(HitHealAttr, 0.75) .attr(HitHealAttr, 0.75)
.makesContact() .makesContact()
@ -10017,10 +10171,12 @@ export function initMoves() {
.condition(failIfLastCondition), .condition(failIfLastCondition),
new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6) new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.ignoresSubstitute(), .ignoresSubstitute()
.reflectable(),
new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6) new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.soundBased(), .soundBased()
.reflectable(),
new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true }) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true })
.makesContact(false) .makesContact(false)
@ -10047,14 +10203,17 @@ export function initMoves() {
.condition(failIfSingleBattle) .condition(failIfSingleBattle)
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2), .attr(StatStageChangeAttr, [ Stat.SPATK ], -2)
.reflectable(),
new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC }) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC })
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES)
.reflectable(),
new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6)
.attr(AddBattlerTagAttr, BattlerTagType.POWDER, false, true) .attr(AddBattlerTagAttr, BattlerTagType.POWDER, false, true)
.ignoresSubstitute() .ignoresSubstitute()
.powderMove(), .powderMove()
.reflectable(),
new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
.chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" }))
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true), .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true),
@ -10076,7 +10235,8 @@ export function initMoves() {
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY),
new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6) new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1), .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.reflectable(),
new AttackMove(Moves.NUZZLE, Type.ELECTRIC, MoveCategory.PHYSICAL, 20, 100, 20, 100, 0, 6) new AttackMove(Moves.NUZZLE, Type.ELECTRIC, MoveCategory.PHYSICAL, 20, 100, 20, 100, 0, 6)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6) new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6)
@ -10220,13 +10380,15 @@ export function initMoves() {
.punchingMove(), .punchingMove(),
new StatusMove(Moves.FLORAL_HEALING, Type.FAIRY, -1, 10, -1, 0, 7) new StatusMove(Moves.FLORAL_HEALING, Type.FAIRY, -1, 10, -1, 0, 7)
.attr(BoostHealAttr, 0.5, 2 / 3, true, false, (user, target, move) => globalScene.arena.terrain?.terrainType === TerrainType.GRASSY) .attr(BoostHealAttr, 0.5, 2 / 3, true, false, (user, target, move) => globalScene.arena.terrain?.terrainType === TerrainType.GRASSY)
.triageMove(), .triageMove()
.reflectable(),
new AttackMove(Moves.HIGH_HORSEPOWER, Type.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7), new AttackMove(Moves.HIGH_HORSEPOWER, Type.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7),
new StatusMove(Moves.STRENGTH_SAP, Type.GRASS, 100, 10, -1, 0, 7) new StatusMove(Moves.STRENGTH_SAP, Type.GRASS, 100, 10, -1, 0, 7)
.attr(HitHealAttr, null, Stat.ATK) .attr(HitHealAttr, null, Stat.ATK)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.condition((user, target, move) => target.getStatStage(Stat.ATK) > -6) .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6)
.triageMove(), .triageMove()
.reflectable(),
new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7)
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]) .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
@ -10236,10 +10398,12 @@ export function initMoves() {
.makesContact(false), .makesContact(false),
new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7) new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7)
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false) .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false)
.condition(failIfSingleBattle), .condition(failIfSingleBattle)
.reflectable(),
new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7) new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7)
.attr(StatusEffectAttr, StatusEffect.POISON) .attr(StatusEffectAttr, StatusEffect.POISON)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1), .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.reflectable(),
new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
@ -10283,7 +10447,8 @@ export function initMoves() {
(user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct? (user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct?
.attr(HealAttr, 0.5) .attr(HealAttr, 0.5)
.attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
.triageMove(), .triageMove()
.reflectable(),
new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7)
.danceMove() .danceMove()
.attr(MatchUserTypeAttr), .attr(MatchUserTypeAttr),
@ -10365,14 +10530,15 @@ export function initMoves() {
new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7) new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7)
.attr(RechargeAttr), .attr(RechargeAttr),
new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7) new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7)
.ignoresSubstitute() .attr(SpectralThiefAttr)
.partial(), // Does not steal stats .ignoresSubstitute(),
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
.ignoresAbilities(), .ignoresAbilities(),
new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
.ignoresAbilities(), .ignoresAbilities(),
new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1)
.reflectable(),
new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.NATURES_MADNESS, Type.FAIRY, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 7) new AttackMove(Moves.NATURES_MADNESS, Type.FAIRY, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 7)
@ -10491,10 +10657,12 @@ export function initMoves() {
.condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat .condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat
new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8) new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false), .attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false)
.reflectable(),
new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8) new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8)
.attr(ChangeTypeAttr, Type.PSYCHIC) .attr(ChangeTypeAttr, Type.PSYCHIC)
.powderMove(), .powderMove()
.reflectable(),
new AttackMove(Moves.DRAGON_DARTS, Type.DRAGON, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 8) new AttackMove(Moves.DRAGON_DARTS, Type.DRAGON, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 8)
.attr(MultiHitAttr, MultiHitType._2) .attr(MultiHitAttr, MultiHitType._2)
.makesContact(false) .makesContact(false)
@ -10671,6 +10839,7 @@ export function initMoves() {
.makesContact(false), .makesContact(false),
new StatusMove(Moves.CORROSIVE_GAS, Type.POISON, 100, 40, -1, 0, 8) new StatusMove(Moves.CORROSIVE_GAS, Type.POISON, 100, 40, -1, 0, 8)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.reflectable()
.unimplemented(), .unimplemented(),
new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8) new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1)
@ -10905,8 +11074,7 @@ export function initMoves() {
.attr(TeraMoveCategoryAttr) .attr(TeraMoveCategoryAttr)
.attr(TeraBlastTypeAttr) .attr(TeraBlastTypeAttr)
.attr(TeraBlastPowerAttr) .attr(TeraBlastPowerAttr)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized && user.isOfType(Type.STELLAR) }) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized && user.isOfType(Type.STELLAR) }),
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.SILK_TRAP) .attr(ProtectAttr, BattlerTagType.SILK_TRAP)
.condition(failIfLastCondition), .condition(failIfLastCondition),
@ -10916,8 +11084,7 @@ export function initMoves() {
.attr(ConfuseAttr) .attr(ConfuseAttr)
.recklessMove(), .recklessMove(),
new AttackMove(Moves.LAST_RESPECTS, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9) new AttackMove(Moves.LAST_RESPECTS, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9)
.partial() // Counter resets every wave instead of on arena reset .attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 100))
.attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? globalScene.currentBattle.playerFaints : globalScene.currentBattle.enemyFaints, 100))
.makesContact(false), .makesContact(false),
new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),

View File

@ -41,8 +41,6 @@ export const FunAndGamesEncounter: MysteryEncounter =
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion to play .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion to play
.withAutoHideIntroVisuals(false) .withAutoHideIntroVisuals(false)
// Allows using move without a visible enemy pokemon
.withBattleAnimationsWithoutTargets(true)
// The Wobbuffet won't use moves // The Wobbuffet won't use moves
.withSkipEnemyBattleTurns(true) .withSkipEnemyBattleTurns(true)
// Will skip COMMAND selection menu and go straight to FIGHT (move select) menu // Will skip COMMAND selection menu and go straight to FIGHT (move select) menu

View File

@ -897,16 +897,21 @@ export function getRandomEncounterSpecies(level: number, isBoss: boolean = false
let bossSpecies: PokemonSpecies; let bossSpecies: PokemonSpecies;
let isEventEncounter = false; let isEventEncounter = false;
const eventEncounters = globalScene.eventManager.getEventEncounters(); const eventEncounters = globalScene.eventManager.getEventEncounters();
let formIndex;
if (eventEncounters.length > 0 && randSeedInt(2) === 1) { if (eventEncounters.length > 0 && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(eventEncounters); const eventEncounter = randSeedItem(eventEncounters);
const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, !eventEncounter.blockEvolution, isBoss, globalScene.gameMode); const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, !eventEncounter.blockEvolution, isBoss, globalScene.gameMode);
isEventEncounter = true; isEventEncounter = true;
bossSpecies = getPokemonSpecies(levelSpecies); bossSpecies = getPokemonSpecies(levelSpecies);
formIndex = eventEncounter.formIndex;
} else { } else {
bossSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, level, 0, getPartyLuckValue(globalScene.getPlayerParty()), isBoss); bossSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, level, 0, getPartyLuckValue(globalScene.getPlayerParty()), isBoss);
} }
const ret = new EnemyPokemon(bossSpecies, level, TrainerSlot.NONE, isBoss); const ret = new EnemyPokemon(bossSpecies, level, TrainerSlot.NONE, isBoss);
if (formIndex) {
ret.formIndex = formIndex;
}
//Reroll shiny for event encounters //Reroll shiny for event encounters
if (isEventEncounter && !ret.shiny) { if (isEventEncounter && !ret.shiny) {

View File

@ -94,4 +94,5 @@ export enum BattlerTagType {
PSYCHO_SHIFT = "PSYCHO_SHIFT", PSYCHO_SHIFT = "PSYCHO_SHIFT",
ENDURE_TOKEN = "ENDURE_TOKEN", ENDURE_TOKEN = "ENDURE_TOKEN",
POWDER = "POWDER", POWDER = "POWDER",
MAGIC_COAT = "MAGIC_COAT",
} }

View File

@ -45,6 +45,11 @@ export class Arena {
public ignoreAbilities: boolean; public ignoreAbilities: boolean;
public ignoringEffectSource: BattlerIndex | null; public ignoringEffectSource: BattlerIndex | null;
public playerTerasUsed: number; public playerTerasUsed: number;
/**
* Saves the number of times a party pokemon faints during a arena encounter.
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
*/
public playerFaints: number;
private lastTimeOfDay: TimeOfDay; private lastTimeOfDay: TimeOfDay;
@ -53,13 +58,14 @@ export class Arena {
public readonly eventTarget: EventTarget = new EventTarget(); public readonly eventTarget: EventTarget = new EventTarget();
constructor(biome: Biome, bgm: string) { constructor(biome: Biome, bgm: string, playerFaints: number = 0) {
this.biomeType = biome; this.biomeType = biome;
this.tags = []; this.tags = [];
this.bgm = bgm; this.bgm = bgm;
this.trainerPool = biomeTrainerPools[biome]; this.trainerPool = biomeTrainerPools[biome];
this.updatePoolsForTimeOfDay(); this.updatePoolsForTimeOfDay();
this.playerTerasUsed = 0; this.playerTerasUsed = 0;
this.playerFaints = playerFaints;
} }
init() { init() {
@ -690,6 +696,7 @@ export class Arena {
this.trySetWeather(WeatherType.NONE, false); this.trySetWeather(WeatherType.NONE, false);
} }
this.trySetTerrain(TerrainType.NONE, false, true); this.trySetTerrain(TerrainType.NONE, false, true);
this.resetPlayerFaintCount();
this.removeAllTags(); this.removeAllTags();
} }
@ -775,6 +782,10 @@ export class Arena {
return 0; return 0;
} }
} }
resetPlayerFaintCount(): void {
this.playerFaints = 0;
}
} }
export function getBiomeKey(biome: Biome): string { export function getBiomeKey(biome: Biome): string {

View File

@ -7,7 +7,40 @@ import { variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant"; import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
import type Move from "#app/data/move"; import type Move from "#app/data/move";
import { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr, HpSplitAttr } from "#app/data/move"; import {
HighCritAttr,
StatChangeBeforeDmgCalcAttr,
HitsTagAttr,
applyMoveAttrs,
FixedDamageAttr,
VariableAtkAttr,
allMoves,
MoveCategory,
TypelessAttr,
CritOnlyAttr,
getMoveTargets,
OneHitKOAttr,
VariableMoveTypeAttr,
VariableDefAttr,
AttackMove,
ModifiedDamageAttr,
VariableMoveTypeMultiplierAttr,
IgnoreOpponentStatStagesAttr,
SacrificialAttr,
VariableMoveCategoryAttr,
CounterDamageAttr,
StatStageChangeAttr,
RechargeAttr,
IgnoreWeatherTypeDebuffAttr,
BypassBurnDamageReductionAttr,
SacrificialAttrOnHit,
OneHitKOAccuracyAttr,
RespectAttackTypeImmunityAttr,
MoveTarget,
CombinedPledgeStabBoostAttr,
VariableMoveTypeChartAttr,
HpSplitAttr
} from "#app/data/move";
import type { PokemonSpeciesForm } from "#app/data/pokemon-species"; import type { PokemonSpeciesForm } from "#app/data/pokemon-species";
import { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
@ -962,11 +995,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation. * @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation.
* @param isCritical determines whether a critical hit has occurred or not (`false` by default) * @param isCritical determines whether a critical hit has occurred or not (`false` by default)
* @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering * @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering
* @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false`
* @returns the final in-battle value of a stat * @returns the final in-battle value of a stat
*/ */
getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true, ignoreHeldItems: boolean = false): number {
const statValue = new Utils.NumberHolder(this.getStat(stat, false)); const statValue = new Utils.NumberHolder(this.getStat(stat, false));
globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); if (!ignoreHeldItems) {
globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue);
}
// The Ruin abilities here are never ignored, but they reveal themselves on summon anyway // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway
const fieldApplied = new Utils.BooleanHolder(false); const fieldApplied = new Utils.BooleanHolder(false);
@ -980,7 +1016,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue, simulated); applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue, simulated);
} }
let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated); let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated, ignoreHeldItems);
switch (stat) { switch (stat) {
case Stat.ATK: case Stat.ATK:
@ -1078,6 +1114,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats);
if (this.isFusion()) { if (this.isFusion()) {
const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats;
applyChallenges(globalScene.gameMode, ChallengeType.FLIP_STAT, this, fusionBaseStats);
for (const s of PERMANENT_STATS) { for (const s of PERMANENT_STATS) {
baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2);
} }
@ -2519,9 +2557,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default) * @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default)
* @param isCritical determines whether a critical hit has occurred or not (`false` by default) * @param isCritical determines whether a critical hit has occurred or not (`false` by default)
* @param simulated determines whether effects are applied without altering game state (`true` by default) * @param simulated determines whether effects are applied without altering game state (`true` by default)
* @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false`
* @return the stat stage multiplier to be used for effective stat calculation * @return the stat stage multiplier to be used for effective stat calculation
*/ */
getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true, ignoreHeldItems: boolean = false): number {
const statStage = new Utils.IntegerHolder(this.getStatStage(stat)); const statStage = new Utils.IntegerHolder(this.getStatStage(stat));
const ignoreStatStage = new Utils.BooleanHolder(false); const ignoreStatStage = new Utils.BooleanHolder(false);
@ -2548,7 +2587,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!ignoreStatStage.value) { if (!ignoreStatStage.value) {
const statStageMultiplier = new Utils.NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value)); const statStageMultiplier = new Utils.NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value));
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); if (!ignoreHeldItems) {
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier);
}
return Math.min(statStageMultiplier.value, 4); return Math.min(statStageMultiplier.value, 4);
} }
return 1; return 1;
@ -2937,6 +2978,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
isCritical = false; isCritical = false;
} }
/**
* Applies stat changes from {@linkcode move} and gives it to {@linkcode source}
* before damage calculation
*/
applyMoveAttrs(StatChangeBeforeDmgCalcAttr, source, this, move);
const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, isCritical, false); const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, isCritical, false);
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag; const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag;
@ -4408,8 +4455,12 @@ export class PlayerPokemon extends Pokemon {
].filter(d => !!d); ].filter(d => !!d);
const amount = new Utils.NumberHolder(friendship); const amount = new Utils.NumberHolder(friendship);
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
const candyFriendshipMultiplier = globalScene.eventManager.getClassicFriendshipMultiplier(); const candyFriendshipMultiplier = globalScene.gameMode.isClassic ? globalScene.eventManager.getClassicFriendshipMultiplier() : 1;
const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * (globalScene.gameMode.isClassic ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1))); const fusionReduction = fusionStarterSpeciesId
? globalScene.eventManager.areFusionsBoosted() ? 1.5 // Divide candy gain for fusions by 1.5 during events
: 2 // 2 for fusions outside events
: 1; // 1 for non-fused mons
const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * candyFriendshipMultiplier / fusionReduction));
// Add friendship to this PlayerPokemon // Add friendship to this PlayerPokemon
this.friendship = Math.min(this.friendship + amount.value, 255); this.friendship = Math.min(this.friendship + amount.value, 255);

View File

@ -250,9 +250,9 @@ export class LoadingScene extends SceneBase {
} }
const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ]; const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ];
if (lang && availableLangs.includes(lang)) { if (lang && availableLangs.includes(lang)) {
this.loadImage("yearofthesnakeevent-" + lang, "events"); this.loadImage("valentines2025event-" + lang, "events");
} else { } else {
this.loadImage("yearofthesnakeevent-en", "events"); this.loadImage("valentines2025event-en", "events");
} }
this.loadAtlas("statuses", ""); this.loadAtlas("statuses", "");

View File

@ -1735,7 +1735,16 @@ const modifierPool: ModifierPool = {
}, 4), }, 4),
new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3),
new WeightedModifierType(modifierTypes.TERA_SHARD, (party: Pokemon[]) => party.filter(p => !(p.hasSpecies(Species.TERAPAGOS) || p.hasSpecies(Species.OGERPON) || p.hasSpecies(Species.SHEDINJA))).length > 0 ? 1 : 0), new WeightedModifierType(modifierTypes.TERA_SHARD, (party: Pokemon[]) => party.filter(p => !(p.hasSpecies(Species.TERAPAGOS) || p.hasSpecies(Species.OGERPON) || p.hasSpecies(Species.SHEDINJA))).length > 0 ? 1 : 0),
new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 4 : 0), new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => {
if (party.filter(p => !p.fusionSpecies).length > 1) {
if (globalScene.gameMode.isSplicedOnly) {
return 4;
} else if (globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) {
return 1;
}
}
return 0;
}, 4),
new WeightedModifierType(modifierTypes.VOUCHER, (_party: Pokemon[], rerollCount: number) => !globalScene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1), new WeightedModifierType(modifierTypes.VOUCHER, (_party: Pokemon[], rerollCount: number) => !globalScene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1),
].map(m => { ].map(m => {
m.setTier(ModifierTier.GREAT); return m; m.setTier(ModifierTier.GREAT); return m;
@ -1894,7 +1903,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.MULTI_LENS, 18), new WeightedModifierType(modifierTypes.MULTI_LENS, 18),
new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (_party: Pokemon[], rerollCount: number) => new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (_party: Pokemon[], rerollCount: number) =>
!globalScene.gameMode.isDaily && !globalScene.gameMode.isEndless && !globalScene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5), !globalScene.gameMode.isDaily && !globalScene.gameMode.isEndless && !globalScene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5),
new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24), new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !(globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) && !globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24),
new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, () => (globalScene.gameMode.isDaily || (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE))) ? 1 : 0, 1), new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, () => (globalScene.gameMode.isDaily || (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE))) ? 1 : 0, 1),
].map(m => { ].map(m => {
m.setTier(ModifierTier.MASTER); return m; m.setTier(ModifierTier.MASTER); return m;
@ -2553,7 +2562,7 @@ export function getPartyLuckValue(party: Pokemon[]): number {
return DailyLuck.value; return DailyLuck.value;
} }
const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies(); const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies();
const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0) const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 3 : 0) : 0)
.reduce((total: number, value: number) => total += value, 0), 0, 14); .reduce((total: number, value: number) => total += value, 0), 0, 14);
return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14); return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14);
} }

View File

@ -96,10 +96,9 @@ export class FaintPhase extends PokemonPhase {
doFaint(): void { doFaint(): void {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
// Track total times pokemon have been KO'd for Last Respects/Supreme Overlord
// Track total times pokemon have been KO'd for supreme overlord/last respects
if (pokemon.isPlayer()) { if (pokemon.isPlayer()) {
globalScene.currentBattle.playerFaints += 1; globalScene.arena.playerFaints += 1;
globalScene.currentBattle.playerFaintsHistory.push({ pokemon: pokemon, turn: globalScene.currentBattle.turn }); globalScene.currentBattle.playerFaintsHistory.push({ pokemon: pokemon, turn: globalScene.currentBattle.turn });
} else { } else {
globalScene.currentBattle.enemyFaints += 1; globalScene.currentBattle.enemyFaints += 1;

View File

@ -249,7 +249,8 @@ export class GameOverPhase extends BattlePhase {
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)), challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)),
mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1, mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1,
mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData,
playerFaints: globalScene.arena.playerFaints
} as SessionSaveData; } as SessionSaveData;
} }
} }

View File

@ -12,6 +12,7 @@ import {
PostAttackAbAttr, PostAttackAbAttr,
PostDamageAbAttr, PostDamageAbAttr,
PostDefendAbAttr, PostDefendAbAttr,
ReflectStatusMoveAbAttr,
TypeImmunityAbAttr, TypeImmunityAbAttr,
} from "#app/data/ability"; } from "#app/data/ability";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
@ -31,6 +32,7 @@ import {
AttackMove, AttackMove,
DelayedAttackAttr, DelayedAttackAttr,
FlinchAttr, FlinchAttr,
getMoveTargets,
HitsTagAttr, HitsTagAttr,
MissEffectAttr, MissEffectAttr,
MoveCategory, MoveCategory,
@ -47,7 +49,7 @@ import {
} from "#app/data/move"; } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { Type } from "#enums/type"; import { Type } from "#enums/type";
import type { PokemonMove } from "#app/field/pokemon"; import { PokemonMove } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { HitResult, MoveResult } from "#app/field/pokemon"; import { HitResult, MoveResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
@ -60,17 +62,27 @@ import {
} from "#app/modifier/modifier"; } from "#app/modifier/modifier";
import { PokemonPhase } from "#app/phases/pokemon-phase"; import { PokemonPhase } from "#app/phases/pokemon-phase";
import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils"; import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils";
import { type nil } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import type { Moves } from "#enums/moves"; import type { Moves } from "#enums/moves";
import i18next from "i18next"; import i18next from "i18next";
import type { Phase } from "#app/phase";
import { ShowAbilityPhase } from "./show-ability-phase";
import { MovePhase } from "./move-phase";
import { MoveEndPhase } from "./move-end-phase";
export class MoveEffectPhase extends PokemonPhase { export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove; public move: PokemonMove;
protected targets: BattlerIndex[]; protected targets: BattlerIndex[];
protected reflected: boolean = false;
constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) { /**
* @param reflected Indicates that the move was reflected by the user due to magic coat or magic bounce
*/
constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove, reflected: boolean = false) {
super(battlerIndex); super(battlerIndex);
this.move = move; this.move = move;
this.reflected = reflected;
/** /**
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies * In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
* with no party members available to switch in, then the right Pokemon takes the index * with no party members available to switch in, then the right Pokemon takes the index
@ -95,6 +107,13 @@ export class MoveEffectPhase extends PokemonPhase {
return super.end(); return super.end();
} }
/** If an enemy used this move, set this as last enemy that used move or ability */
if (!user.isPlayer()) {
globalScene.currentBattle.lastEnemyInvolved = this.fieldIndex;
} else {
globalScene.currentBattle.lastPlayerInvolved = this.fieldIndex;
}
const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr); const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr);
/** If the user was somehow removed from the field and it's not a delayed attack, end this phase */ /** If the user was somehow removed from the field and it's not a delayed attack, end this phase */
if (!user.isOnField()) { if (!user.isOnField()) {
@ -177,12 +196,14 @@ export class MoveEffectPhase extends PokemonPhase {
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) && (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0]?.getTag(SemiInvulnerableTag); && !targets[0]?.getTag(SemiInvulnerableTag);
const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected && targets.some(t => t.hasAbilityWithAttr(ReflectStatusMoveAbAttr) || !!t.getTag(BattlerTagType.MAGIC_COAT));
/** /**
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target * If no targets are left for the move to hit (FAIL), or the invoked move is non-reflectable, single-target
* (and not random target) and failed the hit check against its target (MISS), log the move * (and not random target) and failed the hit check against its target (MISS), log the move
* as FAILed or MISSed (depending on the conditions above) and end this phase. * as FAILed or MISSed (depending on the conditions above) and end this phase.
*/ */
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { if (!hasActiveTargets || (!mayBounce && !move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit(); this.stopMultiHit();
if (hasActiveTargets) { if (hasActiveTargets) {
globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
@ -204,12 +225,21 @@ export class MoveEffectPhase extends PokemonPhase {
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => { new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => {
/** Has the move successfully hit a target (for damage) yet? */ /** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false; let hasHit: boolean = false;
for (const target of targets) {
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) {
continue;
}
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
// and check which target will magic bounce.
const trueTargets: Pokemon[] = move.moveTarget !== MoveTarget.ENEMY_SIDE ? targets : (() => {
const magicCoatTargets = targets.filter(t => t.getTag(BattlerTagType.MAGIC_COAT) || t.hasAbilityWithAttr(ReflectStatusMoveAbAttr));
// only magic coat effect cares about order
if (!mayBounce || magicCoatTargets.length === 0) {
return [ targets[0] ];
}
return [ magicCoatTargets[0] ];
})();
const queuedPhases: Phase[] = [];
for (const target of trueTargets) {
/** The {@linkcode ArenaTagSide} to which the target belongs */ /** The {@linkcode ArenaTagSide} to which the target belongs */
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
@ -222,7 +252,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
const isProtected = ( const isProtected = !([ MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.move.getMove().moveTarget)) && (
bypassIgnoreProtect.value bypassIgnoreProtect.value
|| !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
&& (hasConditionalProtectApplied.value && (hasConditionalProtectApplied.value
@ -231,13 +261,39 @@ export class MoveEffectPhase extends PokemonPhase {
|| (this.move.getMove().category !== MoveCategory.STATUS || (this.move.getMove().category !== MoveCategory.STATUS
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
/** Is the target hidden by the effects of its Commander ability? */
const isCommanding = globalScene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon() === target;
/** Is the target reflecting status moves from the magic coat move? */
const isReflecting = !!target.getTag(BattlerTagType.MAGIC_COAT);
/** Is the target's magic bounce ability not ignored and able to reflect this move? */
const canMagicBounce = !isReflecting && !move.checkFlag(MoveFlags.IGNORE_ABILITIES, user, target) && target.hasAbilityWithAttr(ReflectStatusMoveAbAttr);
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
/** Is the target reflecting the effect, not protected, and not in an semi-invulnerable state?*/
const willBounce = (!isProtected && !this.reflected && !isCommanding
&& move.hasFlag(MoveFlags.REFLECTABLE)
&& (isReflecting || canMagicBounce)
&& !semiInvulnerableTag);
// If the move will bounce, then queue the bounce and move on to the next target
if (!target.switchOutStatus && willBounce) {
const newTargets = move.isMultiTarget() ? getMoveTargets(target, move.id).targets : [ user.getBattlerIndex() ];
if (!isReflecting) {
queuedPhases.push(new ShowAbilityPhase(target.getBattlerIndex(), target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr)));
}
queuedPhases.push(new MovePhase(target, newTargets, new PokemonMove(move.id, 0, 0, true), true, true, true));
continue;
}
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !target.getTag(SemiInvulnerableTag); && !semiInvulnerableTag;
/** Is the target hidden by the effects of its Commander ability? */
const isCommanding = globalScene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon() === target;
/** /**
* If the move missed a target, stop all future hits against that target * If the move missed a target, stop all future hits against that target
@ -364,6 +420,10 @@ export class MoveEffectPhase extends PokemonPhase {
applyAttrs.push(k); applyAttrs.push(k);
} }
// Apply queued phases
if (queuedPhases.length) {
globalScene.appendToPhase(queuedPhases, MoveEndPhase);
}
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
@ -585,12 +645,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
} }
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { if (this.checkBypassAccAndInvuln(target)) {
return true;
}
// If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match
if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) {
return true; return true;
} }
@ -598,15 +653,12 @@ export class MoveEffectPhase extends PokemonPhase {
return true; return true;
} }
if (target.getTag(BattlerTagType.TELEKINESIS) && !target.getTag(SemiInvulnerableTag) && !this.move.getMove().hasAttr(OneHitKOAttr)) { const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
if (target.getTag(BattlerTagType.TELEKINESIS) && !semiInvulnerableTag && !this.move.getMove().hasAttr(OneHitKOAttr)) {
return true; return true;
} }
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); if (semiInvulnerableTag && !this.checkBypassSemiInvuln(semiInvulnerableTag)) {
if (semiInvulnerableTag
&& !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)
&& !(this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON))
) {
return false; return false;
} }
@ -622,6 +674,52 @@ export class MoveEffectPhase extends PokemonPhase {
return rand < (moveAccuracy * accuracyMultiplier); return rand < (moveAccuracy * accuracyMultiplier);
} }
/**
* Check whether the move should bypass *both* the accuracy *and* semi-invulnerable states.
* @param target - The {@linkcode Pokemon} targeted by the invoked move
* @returns `true` if the move should bypass accuracy and semi-invulnerability
*
* Accuracy and semi-invulnerability can be bypassed by:
* - An ability like {@linkcode Abilities.NO_GUARD | No Guard}
* - A poison type using {@linkcode Moves.TOXIC | Toxic}
* - A move like {@linkcode Moves.LOCK_ON | Lock-On} or {@linkcode Moves.MIND_READER | Mind Reader}.
*
* Does *not* check against effects {@linkcode Moves.GLAIVE_RUSH | Glaive Rush} status (which
* should not bypass semi-invulnerability), or interactions like Earthquake hitting against Dig,
* (which should not bypass the accuracy check).
*
* @see {@linkcode hitCheck}
*/
public checkBypassAccAndInvuln(target: Pokemon) {
const user = this.getUserPokemon();
if (!user) {
return false;
}
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) {
return true;
}
if ((this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON))) {
return true;
}
// TODO: Fix lock on / mind reader check.
if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) {
return true;
}
}
/**
* Check whether the move is able to ignore the given `semiInvulnerableTag`
* @param semiInvulnerableTag - The semiInvulnerbale tag to check against
* @returns `true` if the move can ignore the semi-invulnerable state
*/
public checkBypassSemiInvuln(semiInvulnerableTag: SemiInvulnerableTag | nil): boolean {
if (!semiInvulnerableTag) {
return false;
}
const move = this.move.getMove();
return move.getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType);
}
/** @returns The {@linkcode Pokemon} using this phase's invoked move */ /** @returns The {@linkcode Pokemon} using this phase's invoked move */
public getUserPokemon(): Pokemon | null { public getUserPokemon(): Pokemon | null {
if (this.battlerIndex > BattlerIndex.ENEMY_2) { if (this.battlerIndex > BattlerIndex.ENEMY_2) {

View File

@ -58,6 +58,7 @@ export class MovePhase extends BattlePhase {
protected ignorePp: boolean; protected ignorePp: boolean;
protected failed: boolean = false; protected failed: boolean = false;
protected cancelled: boolean = false; protected cancelled: boolean = false;
protected reflected: boolean = false;
public get pokemon(): Pokemon { public get pokemon(): Pokemon {
return this._pokemon; return this._pokemon;
@ -84,10 +85,12 @@ export class MovePhase extends BattlePhase {
} }
/** /**
* @param followUp Indicates that the move being uses is a "follow-up" - for example, a move being used by Metronome or Dancer. * @param followUp Indicates that the move being used is a "follow-up" - for example, a move being used by Metronome or Dancer.
* Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc. * Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc.
* @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce.
* Reflected moves cannot be reflected again and will not trigger Dancer.
*/ */
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false) { constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false) {
super(); super();
this.pokemon = pokemon; this.pokemon = pokemon;
@ -95,6 +98,7 @@ export class MovePhase extends BattlePhase {
this.move = move; this.move = move;
this.followUp = followUp; this.followUp = followUp;
this.ignorePp = ignorePp; this.ignorePp = ignorePp;
this.reflected = reflected;
} }
/** /**
@ -140,7 +144,7 @@ export class MovePhase extends BattlePhase {
} }
// Check move to see if arena.ignoreAbilities should be true. // Check move to see if arena.ignoreAbilities should be true.
if (!this.followUp) { if (!this.followUp || this.reflected) {
if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) { if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) {
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
} }
@ -335,7 +339,7 @@ export class MovePhase extends BattlePhase {
*/ */
if (success) { if (success) {
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
globalScene.unshiftPhase(new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move)); globalScene.unshiftPhase(new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move, this.reflected));
} else { } else {
if ([ Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE ].includes(this.move.moveId)) { if ([ Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE ].includes(this.move.moveId)) {
@ -543,7 +547,7 @@ export class MovePhase extends BattlePhase {
return; return;
} }
globalScene.queueMessage(i18next.t("battle:useMove", { globalScene.queueMessage(i18next.t(this.reflected ? "battle:magicCoatActivated" : "battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName() moveName: this.move.getName()
}), 500); }), 500);

View File

@ -17,6 +17,14 @@ export class ShowAbilityPhase extends PokemonPhase {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (pokemon) { if (pokemon) {
if (!pokemon.isPlayer()) {
/** If its an enemy pokemon, list it as last enemy to use ability or move */
globalScene.currentBattle.lastEnemyInvolved = pokemon.getBattlerIndex() % 2;
} else {
globalScene.currentBattle.lastPlayerInvolved = pokemon.getBattlerIndex() % 2;
}
globalScene.abilityBar.showAbility(pokemon, this.passive); globalScene.abilityBar.showAbility(pokemon, this.passive);
if (pokemon?.battleData) { if (pokemon?.battleData) {

View File

@ -1,7 +1,8 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { BattlerIndex } from "#app/battle"; import type { BattlerIndex } from "#app/battle";
import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability"; import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, ReflectStatStageChangeAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability";
import { ArenaTagSide, MistTag } from "#app/data/arena-tag"; import { ArenaTagSide, MistTag } from "#app/data/arena-tag";
import type { ArenaTag } from "#app/data/arena-tag";
import type Pokemon from "#app/field/pokemon"; import type 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";
@ -10,6 +11,8 @@ 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";
import { OctolockTag } from "#app/data/battler-tags";
import { ArenaTagType } from "#app/enums/arena-tag-type";
export type StatStageChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void; export type StatStageChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void;
@ -21,9 +24,11 @@ export class StatStageChangePhase extends PokemonPhase {
private ignoreAbilities: boolean; private ignoreAbilities: boolean;
private canBeCopied: boolean; private canBeCopied: boolean;
private onChange: StatStageChangeCallback | null; private onChange: StatStageChangeCallback | null;
private comingFromMirrorArmorUser: boolean;
private comingFromStickyWeb: boolean;
constructor(battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: number, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null) { constructor(battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: number, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null, comingFromMirrorArmorUser: boolean = false, comingFromStickyWeb: boolean = false) {
super(battlerIndex); super(battlerIndex);
this.selfTarget = selfTarget; this.selfTarget = selfTarget;
@ -33,6 +38,8 @@ export class StatStageChangePhase extends PokemonPhase {
this.ignoreAbilities = ignoreAbilities; this.ignoreAbilities = ignoreAbilities;
this.canBeCopied = canBeCopied; this.canBeCopied = canBeCopied;
this.onChange = onChange; this.onChange = onChange;
this.comingFromMirrorArmorUser = comingFromMirrorArmorUser;
this.comingFromStickyWeb = comingFromStickyWeb;
} }
start() { start() {
@ -41,12 +48,44 @@ export class StatStageChangePhase extends PokemonPhase {
if (this.stats.length > 1) { if (this.stats.length > 1) {
for (let i = 0; i < this.stats.length; i++) { for (let i = 0; i < this.stats.length; i++) {
const stat = [ this.stats[i] ]; const stat = [ this.stats[i] ];
globalScene.unshiftPhase(new StatStageChangePhase(this.battlerIndex, this.selfTarget, stat, this.stages, this.showMessage, this.ignoreAbilities, this.canBeCopied, this.onChange)); globalScene.unshiftPhase(new StatStageChangePhase(this.battlerIndex, this.selfTarget, stat, this.stages, this.showMessage, this.ignoreAbilities, this.canBeCopied, this.onChange, this.comingFromMirrorArmorUser));
} }
return this.end(); return this.end();
} }
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
let opponentPokemon: Pokemon | undefined;
/** Gets the position of last enemy or player pokemon that used ability or move, primarily for double battles involving Mirror Armor */
if (pokemon.isPlayer()) {
/** If this SSCP is not from sticky web, then we find the opponent pokemon that last did something */
if (!this.comingFromStickyWeb) {
opponentPokemon = globalScene.getEnemyField()[globalScene.currentBattle.lastEnemyInvolved];
} else {
/** If this SSCP is from sticky web, then check if pokemon that last sucessfully used sticky web is on field */
const stickyTagID = globalScene.arena.findTagsOnSide(
(t: ArenaTag) => t.tagType === ArenaTagType.STICKY_WEB,
ArenaTagSide.PLAYER)[0].sourceId;
globalScene.getEnemyField().forEach((e) => {
if (e.id === stickyTagID) {
opponentPokemon = e;
}
});
}
} else {
if (!this.comingFromStickyWeb) {
opponentPokemon = globalScene.getPlayerField()[globalScene.currentBattle.lastPlayerInvolved];
} else {
const stickyTagID = globalScene.arena.findTagsOnSide(
(t: ArenaTag) => t.tagType === ArenaTagType.STICKY_WEB,
ArenaTagSide.ENEMY)[0].sourceId;
globalScene.getPlayerField().forEach((e) => {
if (e.id === stickyTagID) {
opponentPokemon = e;
}
});
}
}
if (!pokemon.isActive(true)) { if (!pokemon.isActive(true)) {
return this.end(); return this.end();
@ -70,6 +109,11 @@ export class StatStageChangePhase extends PokemonPhase {
if (!cancelled.value && !this.selfTarget && stages.value < 0) { if (!cancelled.value && !this.selfTarget && stages.value < 0) {
applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate);
/** Potential stat reflection due to Mirror Armor, does not apply to Octolock end of turn effect */
if (opponentPokemon !== undefined && !pokemon.findTag(t => t instanceof OctolockTag) && !this.comingFromMirrorArmorUser) {
applyPreStatStageChangeAbAttrs(ReflectStatStageChangeAbAttr, pokemon, stat, cancelled, simulate, opponentPokemon, this.stages);
}
} }
// If one stat stage decrease is cancelled, simulate the rest of the applications // If one stat stage decrease is cancelled, simulate the rest of the applications

View File

@ -141,6 +141,10 @@ export interface SessionSaveData {
challenges: ChallengeData[]; challenges: ChallengeData[];
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
mysteryEncounterSaveData: MysteryEncounterSaveData; mysteryEncounterSaveData: MysteryEncounterSaveData;
/**
* Counts the amount of pokemon fainted in your party during the current arena encounter.
*/
playerFaints: number;
} }
interface Unlocks { interface Unlocks {
@ -964,7 +968,8 @@ export class GameData {
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)), challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)),
mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1, mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1,
mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData,
playerFaints: globalScene.arena.playerFaints
} as SessionSaveData; } as SessionSaveData;
} }
@ -1056,7 +1061,7 @@ export class GameData {
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData); globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData);
globalScene.newArena(sessionData.arena.biome); globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints);
const battleType = sessionData.battleType || 0; const battleType = sessionData.battleType || 0;
const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null;

View File

@ -0,0 +1,351 @@
import { BattlerIndex } from "#app/battle";
import { allAbilities } from "#app/data/ability";
import { ArenaTagSide } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Stat } from "#app/enums/stat";
import { StatusEffect } from "#app/enums/status-effect";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Magic Bounce", () => {
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
.ability(Abilities.BALL_FETCH)
.battleType("single")
.moveset( [ Moves.GROWL, Moves.SPLASH ])
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.MAGIC_BOUNCE)
.enemyMoveset(Moves.SPLASH);
});
it("should reflect basic status moves", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should not bounce moves while the target is in the semi-invulnerable state", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.override.moveset([ Moves.GROWL ]);
game.override.enemyMoveset( [ Moves.FLY ]);
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.FLY);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0);
});
it("should individually bounce back multi-target moves", async () => {
game.override.battleType("double");
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.GROWL, 0);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
const user = game.scene.getPlayerField()[0];
expect(user.getStatStage(Stat.ATK)).toBe(-2);
});
it("should still bounce back a move that would otherwise fail", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6);
game.override.moveset([ Moves.GROWL ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should not bounce back a move that was just bounced", async () => {
game.override.ability(Abilities.MAGIC_BOUNCE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should receive the stat change after reflecting a move back to a mirror armor user", async () => {
game.override.ability(Abilities.MIRROR_ARMOR);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should not bounce back a move from a mold breaker user", async () => {
game.override.ability(Abilities.MOLD_BREAKER);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should bounce back a spread status move against both pokemon", async () => {
game.override.battleType("double");
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
game.override.enemyMoveset([ Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.GROWL, 0);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -2)).toBeTruthy();
});
it("should only bounce spikes back once in doubles when both targets have magic bounce", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.override.moveset([ Moves.SPIKES ]);
game.move.select(Moves.SPIKES);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined();
});
it("should bounce spikes even when the target is protected", async () => {
game.override.moveset([ Moves.SPIKES ]);
game.override.enemyMoveset([ Moves.PROTECT ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.SPIKES);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
});
it("should not bounce spikes when the target is in the semi-invulnerable state", async () => {
game.override.moveset([ Moves.SPIKES ]);
game.override.enemyMoveset([ Moves.FLY ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.SPIKES);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)!["layers"]).toBe(1);
});
it("should not bounce back curse", async() => {
game.override.starterSpecies(Species.GASTLY);
await game.classicMode.startBattle([ Species.GASTLY ]);
game.override.moveset([ Moves.CURSE ]);
game.move.select(Moves.CURSE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined();
});
it("should not cause encore to be interrupted after bouncing", async () => {
game.override.moveset([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]);
game.override.enemyMoveset([ Moves.TACKLE, Moves.GROWL ]);
// game.override.ability(Abilities.MOLD_BREAKER);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
// Give the player MOLD_BREAKER for this turn to bypass Magic Bounce.
vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]);
// turn 1
game.move.select(Moves.ENCORE);
await game.forceEnemyMove(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE);
// turn 2
vi.spyOn(playerPokemon, "getAbility").mockRestore();
game.move.select(Moves.GROWL);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE);
expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE);
});
// TODO: encore is failing if the last move was virtual.
it.todo("should not cause the bounced move to count for encore", async () => {
game.override.moveset([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]);
game.override.enemyMoveset([ Moves.GROWL, Moves.TACKLE ]);
game.override.enemyAbility(Abilities.MAGIC_BOUNCE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
// turn 1
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
// Give the player MOLD_BREAKER for this turn to bypass Magic Bounce.
vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]);
// turn 2
game.move.select(Moves.ENCORE);
await game.forceEnemyMove(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE);
expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE);
});
// TODO: stomping tantrum should consider moves that were bounced.
it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => {
game.override.battleType("single");
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.override.moveset([ Moves.STOMPING_TANTRUM, Moves.CHARM ]);
const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM];
vi.spyOn(stomping_tantrum, "calculateBattlePower");
game.move.select(Moves.CHARM);
await game.toNextTurn();
game.move.select(Moves.STOMPING_TANTRUM);
await game.phaseInterceptor.to("BerryPhase");
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
});
// TODO: stomping tantrum should consider moves that were bounced.
it.todo("should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", async () => {
game.override.enemyMoveset([ Moves.STOMPING_TANTRUM, Moves.SPLASH, Moves.CHARM ]);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM];
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(stomping_tantrum, "calculateBattlePower");
game.move.select(Moves.SPORE);
await game.forceEnemyMove(Moves.CHARM);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.getLastXMoves(1)[0].result).toBe("success");
await game.phaseInterceptor.to("BerryPhase");
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
await game.toNextTurn();
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
});
it("should respect immunities when bouncing a move", async () => {
vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
game.override.moveset([ Moves.THUNDER_WAVE, Moves.GROWL ]);
game.override.ability(Abilities.SOUNDPROOF);
await game.classicMode.startBattle([ Species.PHANPY ]);
// Turn 1 - thunder wave immunity test
game.move.select(Moves.THUNDER_WAVE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
// Turn 2 - soundproof immunity test
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0);
});
it("should bounce back a move before the accuracy check", async () => {
game.override.moveset([ Moves.SPORE ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const attacker = game.scene.getPlayerPokemon()!;
vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.SLEEP);
});
it("should take the accuracy of the magic bounce user into account", async () => {
game.override.moveset([ Moves.SPORE ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const opponent = game.scene.getEnemyPokemon()!;
vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
});
it("should always apply the leftmost available target's magic bounce when bouncing moves like sticky webs in doubles", async () => {
game.override.battleType("double");
game.override.moveset([ Moves.STICKY_WEB, Moves.SPLASH, Moves.TRICK_ROOM ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
const [ enemy_1, enemy_2 ] = game.scene.getEnemyField();
// set speed just incase logic erroneously checks for speed order
enemy_1.setStat(Stat.SPD, enemy_2.getStat(Stat.SPD) + 1);
// turn 1
game.move.select(Moves.STICKY_WEB, 0);
game.move.select(Moves.TRICK_ROOM, 1);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.getBattlerIndex()).toBe(BattlerIndex.ENEMY);
game.scene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER, true);
// turn 2
game.move.select(Moves.STICKY_WEB, 0);
game.move.select(Moves.TRICK_ROOM, 1);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.getBattlerIndex()).toBe(BattlerIndex.ENEMY);
});
it("should not bounce back status moves that hit through semi-invulnerable states", async () => {
game.override.moveset([ Moves.TOXIC, Moves.CHARM ]);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
game.move.select(Moves.TOXIC);
await game.forceEnemyMove(Moves.FLY);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.TOXIC);
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
game.override.ability(Abilities.NO_GUARD);
game.move.select(Moves.CHARM);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-2);
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0);
});
});

View File

@ -0,0 +1,315 @@
import { Stat } from "#enums/stat";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { BattlerIndex } from "#app/battle";
// TODO: When Magic Bounce is implemented, make a test for its interaction with mirror guard, use screech
describe("Ability - Mirror Armor", () => {
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")
.enemySpecies(Species.RATTATA)
.enemyMoveset([ Moves.SPLASH, Moves.STICKY_WEB, Moves.TICKLE, Moves.OCTOLOCK ])
.enemyAbility(Abilities.BALL_FETCH)
.startingLevel(2000)
.moveset([ Moves.SPLASH, Moves.STICKY_WEB, Moves.TICKLE, Moves.OCTOLOCK ])
.ability(Abilities.BALL_FETCH);
});
it("Player side + single battle Intimidate - opponent loses stats", async () => {
game.override.ability(Abilities.MIRROR_ARMOR);
game.override.enemyAbility(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
// Enemy has intimidate, enemy should lose -1 atk
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
expect(userPokemon.getStatStage(Stat.ATK)).toBe(0);
});
it("Enemy side + single battle Intimidate - player loses stats", async () => {
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
game.override.ability(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
// Enemy has intimidate, enemy should lose -1 atk
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(userPokemon.getStatStage(Stat.ATK)).toBe(-1);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
});
it("Player side + double battle Intimidate - opponents each lose -2 atk", async () => {
game.override.battleType("double");
game.override.ability(Abilities.MIRROR_ARMOR);
game.override.enemyAbility(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
const [ enemy1, enemy2 ] = game.scene.getEnemyField();
const [ player1, player2 ] = game.scene.getPlayerField();
// Enemy has intimidate, enemy should lose -2 atk each
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2);
await game.toNextTurn();
expect(enemy1.getStatStage(Stat.ATK)).toBe(-2);
expect(enemy2.getStatStage(Stat.ATK)).toBe(-2);
expect(player1.getStatStage(Stat.ATK)).toBe(0);
expect(player2.getStatStage(Stat.ATK)).toBe(0);
});
it("Enemy side + double battle Intimidate - players each lose -2 atk", async () => {
game.override.battleType("double");
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
game.override.ability(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
const [ enemy1, enemy2 ] = game.scene.getEnemyField();
const [ player1, player2 ] = game.scene.getPlayerField();
// Enemy has intimidate, enemy should lose -1 atk
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2);
await game.toNextTurn();
expect(enemy1.getStatStage(Stat.ATK)).toBe(0);
expect(enemy2.getStatStage(Stat.ATK)).toBe(0);
expect(player1.getStatStage(Stat.ATK)).toBe(-2);
expect(player2.getStatStage(Stat.ATK)).toBe(-2);
});
it("Player side + single battle Intimidate + Tickle - opponent loses stats", async () => {
game.override.ability(Abilities.MIRROR_ARMOR);
game.override.enemyAbility(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
// Enemy has intimidate and uses tickle, enemy receives -2 atk and -1 defense
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2);
expect(userPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(userPokemon.getStatStage(Stat.DEF)).toBe(0);
});
it("Player side + double battle Intimidate + Tickle - opponents each lose -3 atk, -1 def", async () => {
game.override.battleType("double");
game.override.ability(Abilities.MIRROR_ARMOR);
game.override.enemyAbility(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
const [ enemy1, enemy2 ] = game.scene.getEnemyField();
const [ player1, player2 ] = game.scene.getPlayerField();
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER_2);
await game.toNextTurn();
expect(player1.getStatStage(Stat.ATK)).toBe(0);
expect(player1.getStatStage(Stat.DEF)).toBe(0);
expect(player2.getStatStage(Stat.ATK)).toBe(0);
expect(player2.getStatStage(Stat.DEF)).toBe(0);
expect(enemy1.getStatStage(Stat.ATK)).toBe(-3);
expect(enemy1.getStatStage(Stat.DEF)).toBe(-1);
expect(enemy2.getStatStage(Stat.ATK)).toBe(-3);
expect(enemy2.getStatStage(Stat.DEF)).toBe(-1);
});
it("Enemy side + single battle Intimidate + Tickle - player loses stats", async () => {
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
game.override.ability(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
// Enemy has intimidate and uses tickle, enemy receives -2 atk and -1 defense
game.move.select(Moves.TICKLE);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(userPokemon.getStatStage(Stat.DEF)).toBe(-1);
expect(userPokemon.getStatStage(Stat.ATK)).toBe(-2);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
});
it("Player side + single battle Intimidate + oppoenent has white smoke - no one loses stats", async () => {
game.override.enemyAbility(Abilities.WHITE_SMOKE);
game.override.ability(Abilities.MIRROR_ARMOR);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
// Enemy has intimidate and uses tickle, enemy has white smoke, no one loses stats
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(userPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(userPokemon.getStatStage(Stat.DEF)).toBe(0);
});
it("Enemy side + single battle Intimidate + player has white smoke - no one loses stats", async () => {
game.override.ability(Abilities.WHITE_SMOKE);
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
// Enemy has intimidate and uses tickle, enemy has white smoke, no one loses stats
game.move.select(Moves.TICKLE);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(userPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(userPokemon.getStatStage(Stat.DEF)).toBe(0);
});
it("Player side + single battle + opponent uses octolock - does not interact with mirror armor, player loses stats", async () => {
game.override.ability(Abilities.MIRROR_ARMOR);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
// Enemy uses octolock, player loses stats at end of turn
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.OCTOLOCK, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
expect(userPokemon.getStatStage(Stat.DEF)).toBe(-1);
expect(userPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
});
it("Enemy side + single battle + player uses octolock - does not interact with mirror armor, opponent loses stats", async () => {
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
// Player uses octolock, enemy loses stats at end of turn
game.move.select(Moves.OCTOLOCK);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(userPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(userPokemon.getStatStage(Stat.SPDEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
});
it("Both sides have mirror armor - does not loop, player loses attack", async () => {
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
game.override.ability(Abilities.MIRROR_ARMOR);
game.override.ability(Abilities.INTIMIDATE);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(userPokemon.getStatStage(Stat.ATK)).toBe(-1);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
});
it("Single battle + sticky web applied player side - player switches out and enemy should lose -1 speed", async () => {
game.override.ability(Abilities.MIRROR_ARMOR);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
const userPokemon = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.STICKY_WEB, BattlerIndex.PLAYER);
await game.toNextTurn();
game.doSwitchPokemon(1);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.toNextTurn();
expect(userPokemon.getStatStage(Stat.SPD)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(-1);
});
it("Double battle + sticky web applied player side - player switches out and enemy 1 should lose -1 speed", async () => {
game.override.battleType("double");
game.override.ability(Abilities.MIRROR_ARMOR);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
const [ enemy1, enemy2 ] = game.scene.getEnemyField();
const [ player1, player2 ] = game.scene.getPlayerField();
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.STICKY_WEB, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2);
await game.toNextTurn();
game.doSwitchPokemon(2);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2);
await game.toNextTurn();
expect(enemy1.getStatStage(Stat.SPD)).toBe(-1);
expect(enemy2.getStatStage(Stat.SPD)).toBe(0);
expect(player1.getStatStage(Stat.SPD)).toBe(0);
expect(player2.getStatStage(Stat.SPD)).toBe(0);
});
});

View File

@ -0,0 +1,66 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Nature } from "#enums/nature";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { BattlerIndex } from "#app/battle";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Protosynthesis", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SPLASH, Moves.TACKLE ])
.ability(Abilities.PROTOSYNTHESIS)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should not consider temporary items when determining which stat to boost", async() => {
// Mew has uniform base stats
game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.DEF }])
.enemyMoveset(Moves.SUNNY_DAY)
.startingLevel(100)
.enemyLevel(100);
await game.classicMode.startBattle([ Species.MEW ]);
const mew = game.scene.getPlayerPokemon()!;
// Nature of starting mon is randomized. We need to fix it to a neutral nature for the automated test.
mew.setNature(Nature.HARDY);
const enemy = game.scene.getEnemyPokemon()!;
const def_before_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true);
const atk_before_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true);
const initialHp = enemy.hp;
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.toNextTurn();
const unboosted_dmg = initialHp - enemy.hp;
enemy.hp = initialHp;
const def_after_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true);
const atk_after_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true);
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.toNextTurn();
const boosted_dmg = initialHp - enemy.hp;
expect(boosted_dmg).toBeGreaterThan(unboosted_dmg);
expect(def_after_boost).toEqual(def_before_boost);
expect(atk_after_boost).toBeGreaterThan(atk_before_boost);
});
});

View File

@ -53,11 +53,11 @@ describe("Abilities - Shield Dust", () => {
expect(move.id).toBe(Moves.AIR_SLASH); expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new NumberHolder(move.chance); const chance = new NumberHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); await applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance); await applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance);
expect(chance.value).toBe(0); expect(chance.value).toBe(0);
}, 20000); });
//TODO King's Rock Interaction Unit Test //TODO King's Rock Interaction Unit Test
}); });

View File

@ -0,0 +1,178 @@
import { Moves } from "#app/enums/moves";
import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species";
import { BattlerIndex } from "#app/battle";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { allMoves } from "#app/data/move";
describe("Abilities - Supreme Overlord", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const move = allMoves[Moves.TACKLE];
const basePower = move.power;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyLevel(100)
.startingLevel(1)
.enemyAbility(Abilities.BALL_FETCH)
.ability(Abilities.SUPREME_OVERLORD)
.enemyMoveset([ Moves.SPLASH ])
.moveset([ Moves.TACKLE, Moves.EXPLOSION, Moves.LUNAR_DANCE ]);
vi.spyOn(move, "calculateBattlePower");
});
it("should increase Power by 20% if 2 Pokemon are fainted in the party", async() => {
await game.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(2);
await game.toNextTurn();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase);
expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.2);
});
it("should increase Power by 30% if an ally fainted twice and another one once", async () => {
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
/**
* Bulbasur faints once
*/
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
/**
* Charmander faints once
*/
game.doRevivePokemon(1);
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
/**
* Bulbasur faints twice
*/
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(2);
await game.toNextTurn();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase);
expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.3);
});
it("should maintain its power during next battle if it is within the same arena encounter", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.startingWave(1)
.enemyLevel(1)
.startingLevel(100);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
/**
* The first Pokemon faints and another Pokemon in the party is selected.
*/
game.move.select(Moves.LUNAR_DANCE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
/**
* Enemy Pokemon faints and new wave is entered.
*/
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextWave();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower * 1.1);
});
it("should reset playerFaints count if we enter new trainer battle", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.startingWave(4)
.enemyLevel(1)
.startingLevel(100);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
game.move.select(Moves.LUNAR_DANCE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextWave();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower);
});
it("should reset playerFaints count if we enter new biome", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.startingWave(10)
.enemyLevel(1)
.startingLevel(100);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
game.move.select(Moves.LUNAR_DANCE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextWave();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower);
});
});

View File

@ -45,9 +45,9 @@ describe("Abilities - Unseen Fist", () => {
it( it(
"should not apply if the source has Long Reach", "should not apply if the source has Long Reach",
() => { async () => {
game.override.passiveAbility(Abilities.LONG_REACH); game.override.passiveAbility(Abilities.LONG_REACH);
testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false); await testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false);
} }
); );
@ -67,7 +67,7 @@ describe("Abilities - Unseen Fist", () => {
game.override.enemyLevel(1); game.override.enemyLevel(1);
game.override.moveset([ Moves.TACKLE ]); game.override.moveset([ Moves.TACKLE ]);
await game.startBattle(); await game.classicMode.startBattle();
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id); enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id);
@ -86,7 +86,7 @@ async function testUnseenFistHitResult(game: GameManager, attackMove: Moves, pro
game.override.moveset([ attackMove ]); game.override.moveset([ attackMove ]);
game.override.enemyMoveset([ protectMove, protectMove, protectMove, protectMove ]); game.override.enemyMoveset([ protectMove, protectMove, protectMove, protectMove ]);
await game.startBattle(); await game.classicMode.startBattle();
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
expect(leadPokemon).not.toBe(undefined); expect(leadPokemon).not.toBe(undefined);

View File

@ -20,8 +20,8 @@ const pokemonName = "PKM";
const sourceText = "SOURCE"; const sourceText = "SOURCE";
describe("Status Effect Messages", () => { describe("Status Effect Messages", () => {
beforeAll(() => { beforeAll(async () => {
i18next.init(); await i18next.init();
}); });
describe("NONE", () => { describe("NONE", () => {

View File

@ -40,10 +40,10 @@ describe("Evolution", () => {
eevee.abilityIndex = 2; eevee.abilityIndex = 2;
trapinch.abilityIndex = 2; trapinch.abilityIndex = 2;
eevee.evolve(pokemonEvolutions[Species.EEVEE][6], eevee.getSpeciesForm()); await eevee.evolve(pokemonEvolutions[Species.EEVEE][6], eevee.getSpeciesForm());
expect(eevee.abilityIndex).toBe(2); expect(eevee.abilityIndex).toBe(2);
trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm()); await trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm());
expect(trapinch.abilityIndex).toBe(1); expect(trapinch.abilityIndex).toBe(1);
}); });
@ -55,10 +55,10 @@ describe("Evolution", () => {
bulbasaur.abilityIndex = 0; bulbasaur.abilityIndex = 0;
charmander.abilityIndex = 1; charmander.abilityIndex = 1;
bulbasaur.evolve(pokemonEvolutions[Species.BULBASAUR][0], bulbasaur.getSpeciesForm()); await bulbasaur.evolve(pokemonEvolutions[Species.BULBASAUR][0], bulbasaur.getSpeciesForm());
expect(bulbasaur.abilityIndex).toBe(0); expect(bulbasaur.abilityIndex).toBe(0);
charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm()); await charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm());
expect(charmander.abilityIndex).toBe(1); expect(charmander.abilityIndex).toBe(1);
}); });
@ -68,7 +68,7 @@ describe("Evolution", () => {
const squirtle = game.scene.getPlayerPokemon()!; const squirtle = game.scene.getPlayerPokemon()!;
squirtle.abilityIndex = 5; squirtle.abilityIndex = 5;
squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm()); await squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm());
expect(squirtle.abilityIndex).toBe(0); expect(squirtle.abilityIndex).toBe(0);
}); });
@ -80,7 +80,7 @@ describe("Evolution", () => {
nincada.metBiome = -1; nincada.metBiome = -1;
nincada.gender = 1; nincada.gender = 1;
nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm()); await nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm());
const ninjask = game.scene.getPlayerParty()[0]; const ninjask = game.scene.getPlayerParty()[0];
const shedinja = game.scene.getPlayerParty()[1]; const shedinja = game.scene.getPlayerParty()[1];
expect(ninjask.abilityIndex).toBe(2); expect(ninjask.abilityIndex).toBe(2);

View File

@ -31,7 +31,7 @@ describe("Items - Light Ball", () => {
it("LIGHT_BALL activates in battle correctly", async() => { it("LIGHT_BALL activates in battle correctly", async() => {
game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "LIGHT_BALL" }]); game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "LIGHT_BALL" }]);
const consoleSpy = vi.spyOn(console, "log"); const consoleSpy = vi.spyOn(console, "log");
await game.startBattle([ await game.classicMode.startBattle([
Species.PIKACHU Species.PIKACHU
]); ]);
@ -64,7 +64,7 @@ describe("Items - Light Ball", () => {
}); });
it("LIGHT_BALL held by PIKACHU", async() => { it("LIGHT_BALL held by PIKACHU", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.PIKACHU Species.PIKACHU
]); ]);
@ -83,7 +83,7 @@ describe("Items - Light Ball", () => {
expect(spAtkValue.value / spAtkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
@ -92,7 +92,7 @@ describe("Items - Light Ball", () => {
}, 20000); }, 20000);
it("LIGHT_BALL held by fused PIKACHU (base)", async() => { it("LIGHT_BALL held by fused PIKACHU (base)", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.PIKACHU, Species.PIKACHU,
Species.MAROWAK Species.MAROWAK
]); ]);
@ -122,7 +122,7 @@ describe("Items - Light Ball", () => {
expect(spAtkValue.value / spAtkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
@ -161,7 +161,7 @@ describe("Items - Light Ball", () => {
expect(spAtkValue.value / spAtkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
@ -189,7 +189,7 @@ describe("Items - Light Ball", () => {
expect(spAtkValue.value / spAtkStat).toBe(1); expect(spAtkValue.value / spAtkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);

View File

@ -31,7 +31,7 @@ describe("Items - Metal Powder", () => {
it("METAL_POWDER activates in battle correctly", async() => { it("METAL_POWDER activates in battle correctly", async() => {
game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "METAL_POWDER" }]); game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "METAL_POWDER" }]);
const consoleSpy = vi.spyOn(console, "log"); const consoleSpy = vi.spyOn(console, "log");
await game.startBattle([ await game.classicMode.startBattle([
Species.DITTO Species.DITTO
]); ]);
@ -79,7 +79,7 @@ describe("Items - Metal Powder", () => {
expect(defValue.value / defStat).toBe(1); expect(defValue.value / defStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
expect(defValue.value / defStat).toBe(2); expect(defValue.value / defStat).toBe(2);
@ -112,7 +112,7 @@ describe("Items - Metal Powder", () => {
expect(defValue.value / defStat).toBe(1); expect(defValue.value / defStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
expect(defValue.value / defStat).toBe(2); expect(defValue.value / defStat).toBe(2);
@ -145,7 +145,7 @@ describe("Items - Metal Powder", () => {
expect(defValue.value / defStat).toBe(1); expect(defValue.value / defStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
expect(defValue.value / defStat).toBe(2); expect(defValue.value / defStat).toBe(2);
@ -167,7 +167,7 @@ describe("Items - Metal Powder", () => {
expect(defValue.value / defStat).toBe(1); expect(defValue.value / defStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
expect(defValue.value / defStat).toBe(1); expect(defValue.value / defStat).toBe(1);

View File

@ -31,7 +31,7 @@ describe("Items - Quick Powder", () => {
it("QUICK_POWDER activates in battle correctly", async() => { it("QUICK_POWDER activates in battle correctly", async() => {
game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]); game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]);
const consoleSpy = vi.spyOn(console, "log"); const consoleSpy = vi.spyOn(console, "log");
await game.startBattle([ await game.classicMode.startBattle([
Species.DITTO Species.DITTO
]); ]);
@ -64,7 +64,7 @@ describe("Items - Quick Powder", () => {
}); });
it("QUICK_POWDER held by DITTO", async() => { it("QUICK_POWDER held by DITTO", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.DITTO Species.DITTO
]); ]);
@ -79,14 +79,14 @@ describe("Items - Quick Powder", () => {
expect(spdValue.value / spdStat).toBe(1); expect(spdValue.value / spdStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
expect(spdValue.value / spdStat).toBe(2); expect(spdValue.value / spdStat).toBe(2);
}, 20000); });
it("QUICK_POWDER held by fused DITTO (base)", async() => { it("QUICK_POWDER held by fused DITTO (base)", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.DITTO, Species.DITTO,
Species.MAROWAK Species.MAROWAK
]); ]);
@ -112,14 +112,14 @@ describe("Items - Quick Powder", () => {
expect(spdValue.value / spdStat).toBe(1); expect(spdValue.value / spdStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
expect(spdValue.value / spdStat).toBe(2); expect(spdValue.value / spdStat).toBe(2);
}, 20000); });
it("QUICK_POWDER held by fused DITTO (part)", async() => { it("QUICK_POWDER held by fused DITTO (part)", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.MAROWAK, Species.MAROWAK,
Species.DITTO Species.DITTO
]); ]);
@ -145,14 +145,14 @@ describe("Items - Quick Powder", () => {
expect(spdValue.value / spdStat).toBe(1); expect(spdValue.value / spdStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
expect(spdValue.value / spdStat).toBe(2); expect(spdValue.value / spdStat).toBe(2);
}, 20000); });
it("QUICK_POWDER not held by DITTO", async() => { it("QUICK_POWDER not held by DITTO", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.MAROWAK Species.MAROWAK
]); ]);
@ -167,9 +167,9 @@ describe("Items - Quick Powder", () => {
expect(spdValue.value / spdStat).toBe(1); expect(spdValue.value / spdStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
expect(spdValue.value / spdStat).toBe(1); expect(spdValue.value / spdStat).toBe(1);
}, 20000); });
}); });

View File

@ -31,7 +31,7 @@ describe("Items - Thick Club", () => {
it("THICK_CLUB activates in battle correctly", async() => { it("THICK_CLUB activates in battle correctly", async() => {
game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]); game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]);
const consoleSpy = vi.spyOn(console, "log"); const consoleSpy = vi.spyOn(console, "log");
await game.startBattle([ await game.classicMode.startBattle([
Species.CUBONE Species.CUBONE
]); ]);
@ -64,7 +64,7 @@ describe("Items - Thick Club", () => {
}); });
it("THICK_CLUB held by CUBONE", async() => { it("THICK_CLUB held by CUBONE", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.CUBONE Species.CUBONE
]); ]);
@ -79,14 +79,14 @@ describe("Items - Thick Club", () => {
expect(atkValue.value / atkStat).toBe(1); expect(atkValue.value / atkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
expect(atkValue.value / atkStat).toBe(2); expect(atkValue.value / atkStat).toBe(2);
}, 20000); });
it("THICK_CLUB held by MAROWAK", async() => { it("THICK_CLUB held by MAROWAK", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.MAROWAK Species.MAROWAK
]); ]);
@ -101,14 +101,14 @@ describe("Items - Thick Club", () => {
expect(atkValue.value / atkStat).toBe(1); expect(atkValue.value / atkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
expect(atkValue.value / atkStat).toBe(2); expect(atkValue.value / atkStat).toBe(2);
}, 20000); });
it("THICK_CLUB held by ALOLA_MAROWAK", async() => { it("THICK_CLUB held by ALOLA_MAROWAK", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.ALOLA_MAROWAK Species.ALOLA_MAROWAK
]); ]);
@ -123,18 +123,18 @@ describe("Items - Thick Club", () => {
expect(atkValue.value / atkStat).toBe(1); expect(atkValue.value / atkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
expect(atkValue.value / atkStat).toBe(2); expect(atkValue.value / atkStat).toBe(2);
}, 20000); });
it("THICK_CLUB held by fused CUBONE line (base)", async() => { it("THICK_CLUB held by fused CUBONE line (base)", async() => {
// Randomly choose from the Cubone line // Randomly choose from the Cubone line
const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ]; const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ];
const randSpecies = Utils.randInt(species.length); const randSpecies = Utils.randInt(species.length);
await game.startBattle([ await game.classicMode.startBattle([
species[randSpecies], species[randSpecies],
Species.PIKACHU Species.PIKACHU
]); ]);
@ -160,18 +160,18 @@ describe("Items - Thick Club", () => {
expect(atkValue.value / atkStat).toBe(1); expect(atkValue.value / atkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
expect(atkValue.value / atkStat).toBe(2); expect(atkValue.value / atkStat).toBe(2);
}, 20000); });
it("THICK_CLUB held by fused CUBONE line (part)", async() => { it("THICK_CLUB held by fused CUBONE line (part)", async() => {
// Randomly choose from the Cubone line // Randomly choose from the Cubone line
const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ]; const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ];
const randSpecies = Utils.randInt(species.length); const randSpecies = Utils.randInt(species.length);
await game.startBattle([ await game.classicMode.startBattle([
Species.PIKACHU, Species.PIKACHU,
species[randSpecies] species[randSpecies]
]); ]);
@ -197,14 +197,14 @@ describe("Items - Thick Club", () => {
expect(atkValue.value / atkStat).toBe(1); expect(atkValue.value / atkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
expect(atkValue.value / atkStat).toBe(2); expect(atkValue.value / atkStat).toBe(2);
}, 20000); });
it("THICK_CLUB not held by CUBONE", async() => { it("THICK_CLUB not held by CUBONE", async() => {
await game.startBattle([ await game.classicMode.startBattle([
Species.PIKACHU Species.PIKACHU
]); ]);
@ -219,9 +219,9 @@ describe("Items - Thick Club", () => {
expect(atkValue.value / atkStat).toBe(1); expect(atkValue.value / atkStat).toBe(1);
// Giving Eviolite to party member and testing if it applies // Giving Eviolite to party member and testing if it applies
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
expect(atkValue.value / atkStat).toBe(1); expect(atkValue.value / atkStat).toBe(1);
}, 20000); });
}); });

View File

@ -45,14 +45,10 @@ describe("Moves - Dragon Rage", () => {
game.override.enemyPassiveAbility(Abilities.BALL_FETCH); game.override.enemyPassiveAbility(Abilities.BALL_FETCH);
game.override.enemyLevel(100); game.override.enemyLevel(100);
await game.startBattle(); await game.classicMode.startBattle();
partyPokemon = game.scene.getPlayerParty()[0]; partyPokemon = game.scene.getPlayerParty()[0];
enemyPokemon = game.scene.getEnemyPokemon()!; enemyPokemon = game.scene.getEnemyPokemon()!;
// remove berries
game.scene.removePartyMemberModifiers(0);
game.scene.clearEnemyHeldItemModifiers();
}); });
it("ignores weaknesses", async () => { it("ignores weaknesses", async () => {

View File

@ -41,14 +41,10 @@ describe("Moves - Fissure", () => {
game.override.enemyPassiveAbility(Abilities.BALL_FETCH); game.override.enemyPassiveAbility(Abilities.BALL_FETCH);
game.override.enemyLevel(100); game.override.enemyLevel(100);
await game.startBattle(); await game.classicMode.startBattle();
partyPokemon = game.scene.getPlayerParty()[0]; partyPokemon = game.scene.getPlayerParty()[0];
enemyPokemon = game.scene.getEnemyPokemon()!; enemyPokemon = game.scene.getEnemyPokemon()!;
// remove berries
game.scene.removePartyMemberModifiers(0);
game.scene.clearEnemyHeldItemModifiers();
}); });
it("ignores damage modification from abilities, for example FUR_COAT", async () => { it("ignores damage modification from abilities, for example FUR_COAT", async () => {

View File

@ -0,0 +1,219 @@
import { Moves } from "#enums/moves";
import { BattlerIndex } from "#app/battle";
import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import { allMoves } from "#app/data/move";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Last Respects", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const move = allMoves[Moves.LAST_RESPECTS];
const basePower = move.power;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.disableCrits()
.moveset([ Moves.LAST_RESPECTS, Moves.EXPLOSION, Moves.LUNAR_DANCE ])
.ability(Abilities.BALL_FETCH)
.enemyAbility(Abilities.BALL_FETCH)
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH)
.startingLevel(1)
.enemyLevel(100);
vi.spyOn(move, "calculateBattlePower");
});
it("should have 150 power if 2 allies faint before using move", async () => {
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
/**
* Bulbasur faints once
*/
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
/**
* Charmander faints once
*/
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(2);
await game.toNextTurn();
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase);
expect(move.calculateBattlePower).toHaveReturnedWith(basePower + (2 * 50));
});
it("should have 200 power if an ally fainted twice and another one once", async () => {
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
/**
* Bulbasur faints once
*/
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
/**
* Charmander faints once
*/
game.doRevivePokemon(1);
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
/**
* Bulbasur faints twice
*/
game.move.select(Moves.EXPLOSION);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
game.doSelectPartyPokemon(2);
await game.toNextTurn();
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase);
expect(move.calculateBattlePower).toHaveReturnedWith(basePower + (3 * 50));
});
it("should maintain its power for the player during the next battle if it is within the same arena encounter", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.startingWave(1)
.enemyLevel(1)
.startingLevel(100)
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
/**
* The first Pokemon faints and another Pokemon in the party is selected.
*/
game.move.select(Moves.LUNAR_DANCE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
/**
* Enemy Pokemon faints and new wave is entered.
*/
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextWave();
expect(game.scene.arena.playerFaints).toBe(1);
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower + (1 * 50));
});
it("should reset enemyFaints count on progressing to the next wave.", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.startingWave(1)
.enemyLevel(1)
.startingLevel(100)
.enemyMoveset(Moves.LAST_RESPECTS)
.moveset([ Moves.LUNAR_DANCE, Moves.LAST_RESPECTS, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
/**
* The first Pokemon faints and another Pokemon in the party is selected.
*/
game.move.select(Moves.LUNAR_DANCE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
/**
* Enemy Pokemon faints and new wave is entered.
*/
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextWave();
expect(game.scene.currentBattle.enemyFaints).toBe(0);
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower);
});
it("should reset playerFaints count if we enter new trainer battle", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.startingWave(4)
.enemyLevel(1)
.startingLevel(100);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
game.move.select(Moves.LUNAR_DANCE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextWave();
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower);
});
it("should reset playerFaints count if we enter new biome", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.startingWave(10)
.enemyLevel(1)
.startingLevel(100);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
game.move.select(Moves.LUNAR_DANCE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextWave();
game.move.select(Moves.LAST_RESPECTS);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower);
});
});

View File

@ -0,0 +1,286 @@
import { BattlerIndex } from "#app/battle";
import { ArenaTagSide } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import { ArenaTagType } from "#app/enums/arena-tag-type";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Stat } from "#app/enums/stat";
import { StatusEffect } from "#app/enums/status-effect";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Magic Coat", () => {
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
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.MAGIC_COAT);
});
it("should fail if the user goes last in the turn", async () => {
game.override.moveset([ Moves.PROTECT ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.PROTECT);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should fail if called again in the same turn due to moves like instruct", async () => {
game.override.moveset([ Moves.INSTRUCT ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.INSTRUCT);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should not reflect moves used on the next turn", async () => {
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
game.override.enemyMoveset([ Moves.MAGIC_COAT, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
// turn 1
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.MAGIC_COAT);
await game.toNextTurn();
// turn 2
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should reflect basic status moves", async () => {
game.override.moveset([ Moves.GROWL ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should individually bounce back multi-target moves when used by both targets in doubles", async () => {
game.override.battleType("double");
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.GROWL, 0);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
const user = game.scene.getPlayerField()[0];
expect(user.getStatStage(Stat.ATK)).toBe(-2);
});
it("should bounce back a spread status move against both pokemon", async () => {
game.override.battleType("double");
game.override.moveset([ Moves.GROWL, Moves.SPLASH ]);
game.override.enemyMoveset([ Moves.SPLASH, Moves.MAGIC_COAT ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.GROWL, 0);
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.SPLASH);
await game.forceEnemyMove(Moves.MAGIC_COAT);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -1)).toBeTruthy();
});
it("should still bounce back a move that would otherwise fail", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6);
game.override.moveset([ Moves.GROWL ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should not bounce back a move that was just bounced", async () => {
game.override.battleType("double");
game.override.ability(Abilities.MAGIC_BOUNCE);
game.override.moveset([ Moves.GROWL, Moves.MAGIC_COAT ]);
game.override.enemyMoveset([ Moves.SPLASH, Moves.MAGIC_COAT ]);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]);
game.move.select(Moves.MAGIC_COAT, 0);
game.move.select(Moves.GROWL, 1);
await game.forceEnemyMove(Moves.MAGIC_COAT);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyField()[0].getStatStage(Stat.ATK)).toBe(0);
});
// todo while Mirror Armor is not implemented
it.todo("should receive the stat change after reflecting a move back to a mirror armor user", async () => {
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should still bounce back a move from a mold breaker user", async () => {
game.override.ability(Abilities.MOLD_BREAKER);
game.override.moveset([ Moves.GROWL ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(0);
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should only bounce spikes back once when both targets use magic coat in doubles", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.override.moveset([ Moves.SPIKES ]);
game.move.select(Moves.SPIKES);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1);
expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined();
});
it("should not bounce back curse", async() => {
game.override.starterSpecies(Species.GASTLY);
await game.classicMode.startBattle([ Species.GASTLY ]);
game.override.moveset([ Moves.CURSE ]);
game.move.select(Moves.CURSE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined();
});
// TODO: encore is failing if the last move was virtual.
it.todo("should not cause the bounced move to count for encore", async () => {
game.override.moveset([ Moves.GROWL, Moves.ENCORE ]);
game.override.enemyMoveset([ Moves.MAGIC_COAT, Moves.TACKLE ]);
game.override.enemyAbility(Abilities.MAGIC_BOUNCE);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
// turn 1
game.move.select(Moves.GROWL);
await game.forceEnemyMove(Moves.MAGIC_COAT);
await game.toNextTurn();
// turn 2
game.move.select(Moves.ENCORE);
await game.forceEnemyMove(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE);
expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE);
});
// TODO: stomping tantrum should consider moves that were bounced.
it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => {
game.override.battleType("single");
await game.classicMode.startBattle([ Species.MAGIKARP ]);
game.override.moveset([ Moves.STOMPING_TANTRUM, Moves.CHARM ]);
const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM];
vi.spyOn(stomping_tantrum, "calculateBattlePower");
game.move.select(Moves.CHARM);
await game.toNextTurn();
game.move.select(Moves.STOMPING_TANTRUM);
await game.phaseInterceptor.to("BerryPhase");
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
});
// TODO: stomping tantrum should consider moves that were bounced.
it.todo("should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", async () => {
game.override.enemyMoveset([ Moves.STOMPING_TANTRUM, Moves.SPLASH, Moves.CHARM ]);
await game.classicMode.startBattle([ Species.BULBASAUR ]);
const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM];
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(stomping_tantrum, "calculateBattlePower");
game.move.select(Moves.SPORE);
await game.forceEnemyMove(Moves.CHARM);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.getLastXMoves(1)[0].result).toBe("success");
await game.phaseInterceptor.to("BerryPhase");
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
await game.toNextTurn();
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
});
it("should respect immunities when bouncing a move", async () => {
vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
game.override.moveset([ Moves.THUNDER_WAVE, Moves.GROWL ]);
game.override.ability(Abilities.SOUNDPROOF);
await game.classicMode.startBattle([ Species.PHANPY ]);
// Turn 1 - thunder wave immunity test
game.move.select(Moves.THUNDER_WAVE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
// Turn 2 - soundproof immunity test
game.move.select(Moves.GROWL);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0);
});
it("should bounce back a move before the accuracy check", async () => {
game.override.moveset([ Moves.SPORE ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const attacker = game.scene.getPlayerPokemon()!;
vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.SLEEP);
});
it("should take the accuracy of the magic bounce user into account", async () => {
game.override.moveset([ Moves.SPORE ]);
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const opponent = game.scene.getEnemyPokemon()!;
vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0);
game.move.select(Moves.SPORE);
await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
});
});

View File

@ -0,0 +1,224 @@
import { Abilities } from "#enums/abilities";
import { BattlerIndex } from "#app/battle";
import { Stat } from "#enums/stat";
import { allMoves } from "#app/data/move";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Spectral Thief", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.enemySpecies(Species.SHUCKLE)
.enemyLevel(100)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH)
.moveset([ Moves.SPECTRAL_THIEF, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.disableCrits;
});
it("should steal max possible positive stat changes and ignore negative ones.", async () => {
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.setStatStage(Stat.ATK, 6);
enemy.setStatStage(Stat.DEF, -6);
enemy.setStatStage(Stat.SPATK, 6);
enemy.setStatStage(Stat.SPDEF, -6);
enemy.setStatStage(Stat.SPD, 3);
player.setStatStage(Stat.ATK, 4);
player.setStatStage(Stat.DEF, 1);
player.setStatStage(Stat.SPATK, 0);
player.setStatStage(Stat.SPDEF, 0);
player.setStatStage(Stat.SPD, -2);
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
/**
* enemy has +6 ATK and player +4 => player only steals +2
* enemy has -6 DEF and player 1 => player should not steal
* enemy has +6 SPATK and player 0 => player only steals +6
* enemy has -6 SPDEF and player 0 => player should not steal
* enemy has +3 SPD and player -2 => player only steals +3
*/
expect(player.getStatStages()).toEqual([ 6, 1, 6, 0, 1, 0, 0 ]);
expect(enemy.getStatStages()).toEqual([ 4, -6, 0, -6, 0, 0, 0 ]);
});
it("should steal stat stages before dmg calculation", async () => {
game.override
.enemySpecies(Species.MAGIKARP)
.enemyLevel(50);
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
const moveToCheck = allMoves[Moves.SPECTRAL_THIEF];
const dmgBefore = enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage;
enemy.setStatStage(Stat.ATK, 6);
player.setStatStage(Stat.ATK, 0);
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
expect(dmgBefore).toBeLessThan(enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage);
});
it("should steal stat stages as a negative value with Contrary.", async () => {
game.override
.ability(Abilities.CONTRARY);
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.setStatStage(Stat.ATK, 6);
player.setStatStage(Stat.ATK, 0);
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStatStage(Stat.ATK)).toEqual(-6);
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
});
it("should steal double the stat stages with Simple.", async () => {
game.override
.ability(Abilities.SIMPLE);
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.setStatStage(Stat.ATK, 3);
player.setStatStage(Stat.ATK, 0);
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStatStage(Stat.ATK)).toEqual(6);
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
});
it("should steal the stat stages through Clear Body.", async () => {
game.override
.enemyAbility(Abilities.CLEAR_BODY);
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.setStatStage(Stat.ATK, 3);
player.setStatStage(Stat.ATK, 0);
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStatStage(Stat.ATK)).toEqual(3);
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
});
it("should steal the stat stages through White Smoke.", async () => {
game.override
.enemyAbility(Abilities.WHITE_SMOKE);
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.setStatStage(Stat.ATK, 3);
player.setStatStage(Stat.ATK, 0);
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStatStage(Stat.ATK)).toEqual(3);
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
});
it("should steal the stat stages through Hyper Cutter.", async () => {
game.override
.enemyAbility(Abilities.HYPER_CUTTER);
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.setStatStage(Stat.ATK, 3);
player.setStatStage(Stat.ATK, 0);
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStatStage(Stat.ATK)).toEqual(3);
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
});
it("should bypass Substitute.", async () => {
game.override
.enemyMoveset(Moves.SUBSTITUTE);
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.setStatStage(Stat.ATK, 3);
player.setStatStage(Stat.ATK, 0);
game.move.select(Moves.SPECTRAL_THIEF);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStatStage(Stat.ATK)).toEqual(3);
expect(enemy.getStatStage(Stat.ATK)).toEqual(0);
expect(enemy.hp).toBeLessThan(enemy.getMaxHp() - 1);
});
it("should get blocked by protect.", async () => {
game.override
.enemyMoveset(Moves.PROTECT);
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
enemy.setStatStage(Stat.ATK, 3);
player.setStatStage(Stat.ATK, 0);
game.move.select(Moves.SPECTRAL_THIEF);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStatStage(Stat.ATK)).toEqual(0);
expect(enemy.getStatStage(Stat.ATK)).toEqual(3);
expect(enemy.hp).toBe(enemy.getMaxHp());
});
});

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { allMoves } from "#app/data/move"; import { allMoves, TeraMoveCategoryAttr } from "#app/data/move";
import { Type } from "#enums/type"; import { Type } from "#enums/type";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { HitResult } from "#app/field/pokemon"; import { HitResult } from "#app/field/pokemon";
@ -14,6 +14,7 @@ describe("Moves - Tera Blast", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
const moveToCheck = allMoves[Moves.TERA_BLAST]; const moveToCheck = allMoves[Moves.TERA_BLAST];
const teraBlastAttr = moveToCheck.getAttrs(TeraMoveCategoryAttr)[0];
beforeAll(() => { beforeAll(() => {
phaserGame = new Phaser.Game({ phaserGame = new Phaser.Game({
@ -36,8 +37,8 @@ describe("Moves - Tera Blast", () => {
.ability(Abilities.BALL_FETCH) .ability(Abilities.BALL_FETCH)
.enemySpecies(Species.MAGIKARP) .enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.STURDY)
.enemyLevel(20); .enemyLevel(50);
vi.spyOn(moveToCheck, "calculateBattlePower"); vi.spyOn(moveToCheck, "calculateBattlePower");
}); });
@ -91,9 +92,7 @@ describe("Moves - Tera Blast", () => {
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE); expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
}); });
// Currently abilities are bugged and can't see when a move's category is changed it("uses the higher ATK for damage calculation", async () => {
it.todo("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => {
game.override.enemyAbility(Abilities.TOXIC_DEBRIS);
await game.startBattle(); await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
@ -101,10 +100,79 @@ describe("Moves - Tera Blast", () => {
playerPokemon.stats[Stat.SPATK] = 1; playerPokemon.stats[Stat.SPATK] = 1;
playerPokemon.isTerastallized = true; playerPokemon.isTerastallized = true;
vi.spyOn(teraBlastAttr, "apply");
game.move.select(Moves.TERA_BLAST); game.move.select(Moves.TERA_BLAST);
await game.phaseInterceptor.to("TurnEndPhase"); await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); expect(teraBlastAttr.apply).toHaveLastReturnedWith(true);
}, 20000); });
it("uses the higher SPATK for damage calculation", async () => {
await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 1;
playerPokemon.stats[Stat.SPATK] = 100;
vi.spyOn(teraBlastAttr, "apply");
game.move.select(Moves.TERA_BLAST);
await game.toNextTurn();
expect(teraBlastAttr.apply).toHaveLastReturnedWith(false);
});
it("should stay as a special move if ATK turns lower than SPATK mid-turn", async () => {
game.override.enemyMoveset([ Moves.CHARM ]);
await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 51;
playerPokemon.stats[Stat.SPATK] = 50;
vi.spyOn(teraBlastAttr, "apply");
game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(teraBlastAttr.apply).toHaveLastReturnedWith(false);
});
it("does not change its move category from stat changes due to held items", async () => {
game.override
.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }])
.starterSpecies(Species.CUBONE);
await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 50;
playerPokemon.stats[Stat.SPATK] = 51;
vi.spyOn(teraBlastAttr, "apply");
game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(teraBlastAttr.apply).toHaveLastReturnedWith(false);
});
it("does not change its move category from stat changes due to abilities", async () => {
game.override.ability(Abilities.HUGE_POWER);
await game.startBattle();
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.stats[Stat.ATK] = 50;
playerPokemon.stats[Stat.SPATK] = 51;
vi.spyOn(teraBlastAttr, "apply");
game.move.select(Moves.TERA_BLAST);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(teraBlastAttr.apply).toHaveLastReturnedWith(false);
});
it("causes stat drops if user is Stellar tera type", async () => { it("causes stat drops if user is Stellar tera type", async () => {
await game.startBattle(); await game.startBattle();

View File

@ -132,7 +132,7 @@ describe("Moves - Toxic Spikes", () => {
const sessionData : SessionSaveData = gameData["getSessionSaveData"](); const sessionData : SessionSaveData = gameData["getSessionSaveData"]();
localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true)); localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true));
const recoveredData : SessionSaveData = gameData.parseSessionData(decrypt(localStorage.getItem("sessionTestData")!, true)); const recoveredData : SessionSaveData = gameData.parseSessionData(decrypt(localStorage.getItem("sessionTestData")!, true));
gameData.loadSession(0, recoveredData); await gameData.loadSession(0, recoveredData);
expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags); expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags);
localStorage.removeItem("sessionTestData"); localStorage.removeItem("sessionTestData");

View File

@ -48,12 +48,12 @@ describe("Mystery Encounter Utils", () => {
expect(result.species.speciesId).toBe(Species.ARCEUS); expect(result.species.speciesId).toBe(Species.ARCEUS);
}); });
it("gets a fainted pokemon from player party if isAllowedInBattle is false", () => { it("gets a fainted pokemon from player party if isAllowedInBattle is false", async () => {
// Both pokemon fainted // Both pokemon fainted
scene.getPlayerParty().forEach(p => { scene.getPlayerParty().forEach(p => {
p.hp = 0; p.hp = 0;
p.trySetStatus(StatusEffect.FAINT); p.trySetStatus(StatusEffect.FAINT);
p.updateInfo(); void p.updateInfo();
}); });
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
@ -68,12 +68,12 @@ describe("Mystery Encounter Utils", () => {
expect(result.species.speciesId).toBe(Species.ARCEUS); expect(result.species.speciesId).toBe(Species.ARCEUS);
}); });
it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", () => { it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", async () => {
// Only faint 1st pokemon // Only faint 1st pokemon
const party = scene.getPlayerParty(); const party = scene.getPlayerParty();
party[0].hp = 0; party[0].hp = 0;
party[0].trySetStatus(StatusEffect.FAINT); party[0].trySetStatus(StatusEffect.FAINT);
party[0].updateInfo(); await party[0].updateInfo();
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
game.override.seed("random"); game.override.seed("random");
@ -87,12 +87,12 @@ describe("Mystery Encounter Utils", () => {
expect(result.species.speciesId).toBe(Species.MANAPHY); expect(result.species.speciesId).toBe(Species.MANAPHY);
}); });
it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", () => { it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", async () => {
// Only faint 1st pokemon // Only faint 1st pokemon
const party = scene.getPlayerParty(); const party = scene.getPlayerParty();
party[0].hp = 0; party[0].hp = 0;
party[0].trySetStatus(StatusEffect.FAINT); party[0].trySetStatus(StatusEffect.FAINT);
party[0].updateInfo(); await party[0].updateInfo();
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
game.override.seed("random"); game.override.seed("random");
@ -106,12 +106,12 @@ describe("Mystery Encounter Utils", () => {
expect(result.species.speciesId).toBe(Species.MANAPHY); expect(result.species.speciesId).toBe(Species.MANAPHY);
}); });
it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", () => { it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", async () => {
// Only faint 1st pokemon // Only faint 1st pokemon
const party = scene.getPlayerParty(); const party = scene.getPlayerParty();
party[0].hp = 0; party[0].hp = 0;
party[0].trySetStatus(StatusEffect.FAINT); party[0].trySetStatus(StatusEffect.FAINT);
party[0].updateInfo(); await party[0].updateInfo();
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
game.override.seed("random"); game.override.seed("random");
@ -152,12 +152,12 @@ describe("Mystery Encounter Utils", () => {
expect(result.species.speciesId).toBe(Species.ARCEUS); expect(result.species.speciesId).toBe(Species.ARCEUS);
}); });
it("returns highest level unfainted if unfainted is true", () => { it("returns highest level unfainted if unfainted is true", async () => {
const party = scene.getPlayerParty(); const party = scene.getPlayerParty();
party[0].level = 100; party[0].level = 100;
party[0].hp = 0; party[0].hp = 0;
party[0].trySetStatus(StatusEffect.FAINT); party[0].trySetStatus(StatusEffect.FAINT);
party[0].updateInfo(); await party[0].updateInfo();
party[1].level = 10; party[1].level = 10;
const result = getHighestLevelPlayerPokemon(true); const result = getHighestLevelPlayerPokemon(true);
@ -191,12 +191,12 @@ describe("Mystery Encounter Utils", () => {
expect(result.species.speciesId).toBe(Species.ARCEUS); expect(result.species.speciesId).toBe(Species.ARCEUS);
}); });
it("returns lowest level unfainted if unfainted is true", () => { it("returns lowest level unfainted if unfainted is true", async () => {
const party = scene.getPlayerParty(); const party = scene.getPlayerParty();
party[0].level = 10; party[0].level = 10;
party[0].hp = 0; party[0].hp = 0;
party[0].trySetStatus(StatusEffect.FAINT); party[0].trySetStatus(StatusEffect.FAINT);
party[0].updateInfo(); await party[0].updateInfo();
party[1].level = 100; party[1].level = 100;
const result = getLowestLevelPlayerPokemon(true); const result = getLowestLevelPlayerPokemon(true);

View File

@ -2,8 +2,6 @@ import { BerryType } from "#app/enums/berry-type";
import { Button } from "#app/enums/buttons"; import { Button } from "#app/enums/buttons";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species"; import { Species } from "#app/enums/species";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import PartyUiHandler, { PartyUiMode } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PartyUiMode } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
@ -12,7 +10,6 @@ import Phaser from "phaser";
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("UI - Transfer Items", () => { describe("UI - Transfer Items", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
@ -41,7 +38,7 @@ describe("UI - Transfer Items", () => {
game.override.enemySpecies(Species.MAGIKARP); game.override.enemySpecies(Species.MAGIKARP);
game.override.enemyMoveset([ Moves.SPLASH ]); game.override.enemyMoveset([ Moves.SPLASH ]);
await game.startBattle([ Species.RAYQUAZA, Species.RAYQUAZA, Species.RAYQUAZA ]); await game.classicMode.startBattle([ Species.RAYQUAZA, Species.RAYQUAZA, Species.RAYQUAZA ]);
game.move.select(Moves.DRAGON_CLAW); game.move.select(Moves.DRAGON_CLAW);
@ -52,10 +49,10 @@ describe("UI - Transfer Items", () => {
handler.setCursor(1); handler.setCursor(1);
handler.processInput(Button.ACTION); handler.processInput(Button.ACTION);
game.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER); void game.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER);
}); });
await game.phaseInterceptor.to(BattleEndPhase); await game.phaseInterceptor.to("BattleEndPhase");
}); });
it("check red tint for held item limit in transfer menu", async () => { it("check red tint for held item limit in transfer menu", async () => {
@ -72,7 +69,7 @@ describe("UI - Transfer Items", () => {
game.phaseInterceptor.unlock(); game.phaseInterceptor.unlock();
}); });
await game.phaseInterceptor.to(SelectModifierPhase); await game.phaseInterceptor.to("SelectModifierPhase");
}, 20000); }, 20000);
it("check transfer option for pokemon to transfer to", async () => { it("check transfer option for pokemon to transfer to", async () => {
@ -91,6 +88,6 @@ describe("UI - Transfer Items", () => {
game.phaseInterceptor.unlock(); game.phaseInterceptor.unlock();
}); });
await game.phaseInterceptor.to(SelectModifierPhase); await game.phaseInterceptor.to("SelectModifierPhase");
}, 20000); }, 20000);
}); });

View File

@ -27,6 +27,7 @@ interface EventBanner {
interface EventEncounter { interface EventEncounter {
species: Species; species: Species;
blockEvolution?: boolean; blockEvolution?: boolean;
formIndex?: number;
} }
interface EventMysteryEncounterTier { interface EventMysteryEncounterTier {
@ -49,6 +50,7 @@ interface TimedEvent extends EventBanner {
weather?: WeatherPoolEntry[]; weather?: WeatherPoolEntry[];
mysteryEncounterTierChanges?: EventMysteryEncounterTier[]; mysteryEncounterTierChanges?: EventMysteryEncounterTier[];
luckBoostedSpecies?: Species[]; luckBoostedSpecies?: Species[];
boostFusions?: boolean; //MODIFIER REWORK PLEASE
} }
const timedEvents: TimedEvent[] = [ const timedEvents: TimedEvent[] = [
@ -144,6 +146,40 @@ const timedEvents: TimedEvent[] = [
Species.ROARING_MOON, Species.ROARING_MOON,
Species.BLOODMOON_URSALUNA Species.BLOODMOON_URSALUNA
] ]
},
{
name: "Valentine",
eventType: EventType.SHINY,
startDate: new Date(Date.UTC(2025, 1, 10)),
endDate: new Date(Date.UTC(2025, 1, 21)),
boostFusions: true,
shinyMultiplier: 2,
bannerKey: "valentines2025event-",
scale: 0.21,
availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ],
eventEncounters: [
{ species: Species.NIDORAN_F },
{ species: Species.NIDORAN_M },
{ species: Species.IGGLYBUFF },
{ species: Species.SMOOCHUM },
{ species: Species.VOLBEAT },
{ species: Species.ILLUMISE },
{ species: Species.ROSELIA },
{ species: Species.LUVDISC },
{ species: Species.WOOBAT },
{ species: Species.FRILLISH },
{ species: Species.ALOMOMOLA },
{ species: Species.FURFROU, formIndex: 1 }, // Heart trim
{ species: Species.ESPURR },
{ species: Species.SPRITZEE },
{ species: Species.SWIRLIX },
{ species: Species.APPLIN },
{ species: Species.MILCERY },
{ species: Species.INDEEDEE },
{ species: Species.TANDEMAUS },
{ species: Species.ENAMORUS }
],
luckBoostedSpecies: [ Species.LUVDISC ]
} }
]; ];
@ -297,6 +333,10 @@ export class TimedEventManager {
}); });
return ret; return ret;
} }
areFusionsBoosted(): boolean {
return timedEvents.some((te) => this.isActive(te) && te.boostFusions);
}
} }
export class TimedEventDisplay extends Phaser.GameObjects.Container { export class TimedEventDisplay extends Phaser.GameObjects.Container {

View File

@ -6,7 +6,7 @@ import { addWindow } from "./ui-theme";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { argbFromRgba } from "@material/material-color-utilities"; import { argbFromRgba } from "@material/material-color-utilities";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText";
export interface OptionSelectConfig { export interface OptionSelectConfig {
xOffset?: number; xOffset?: number;

View File

@ -1,7 +1,17 @@
import type { Variant } from "#app/data/variant";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { isNullOrUndefined } from "#app/utils";
import type PokemonSpecies from "../data/pokemon-species"; import type PokemonSpecies from "../data/pokemon-species";
import { addTextObject, TextStyle } from "./text"; import { addTextObject, TextStyle } from "./text";
interface SpeciesDetails {
shiny?: boolean,
formIndex?: number
female?: boolean,
variant?: Variant
}
export class PokedexMonContainer extends Phaser.GameObjects.Container { export class PokedexMonContainer extends Phaser.GameObjects.Container {
public species: PokemonSpecies; public species: PokemonSpecies;
public icon: Phaser.GameObjects.Sprite; public icon: Phaser.GameObjects.Sprite;
@ -19,16 +29,34 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
public tmMove2Icon: Phaser.GameObjects.Image; public tmMove2Icon: Phaser.GameObjects.Image;
public passive1Icon: Phaser.GameObjects.Image; public passive1Icon: Phaser.GameObjects.Image;
public passive2Icon: Phaser.GameObjects.Image; public passive2Icon: Phaser.GameObjects.Image;
public passive1OverlayIcon: Phaser.GameObjects.Image;
public passive2OverlayIcon: Phaser.GameObjects.Image;
public cost: number = 0; public cost: number = 0;
constructor(species: PokemonSpecies) { constructor(species: PokemonSpecies, options: SpeciesDetails = {}) {
super(globalScene, 0, 0); super(globalScene, 0, 0);
this.species = species; this.species = species;
const { shiny, formIndex, female, variant } = options;
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true); const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true);
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
if (!isNullOrUndefined(formIndex)) {
defaultProps.formIndex = formIndex;
}
if (!isNullOrUndefined(shiny)) {
defaultProps.shiny = shiny;
}
if (!isNullOrUndefined(variant)) {
defaultProps.variant = variant;
}
if (!isNullOrUndefined(female)) {
defaultProps.female = female;
}
// starter passive bg // starter passive bg
const starterPassiveBg = globalScene.add.image(2, 5, "passive_bg"); const starterPassiveBg = globalScene.add.image(2, 5, "passive_bg");
starterPassiveBg.setOrigin(0, 0); starterPassiveBg.setOrigin(0, 0);
@ -137,7 +165,7 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
this.tmMove2Icon = tmMove2Icon; this.tmMove2Icon = tmMove2Icon;
// move icons // passive icons
const passive1Icon = globalScene.add.image(3, 3, "candy"); const passive1Icon = globalScene.add.image(3, 3, "candy");
passive1Icon.setOrigin(0, 0); passive1Icon.setOrigin(0, 0);
passive1Icon.setScale(0.25); passive1Icon.setScale(0.25);
@ -145,13 +173,27 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
this.add(passive1Icon); this.add(passive1Icon);
this.passive1Icon = passive1Icon; this.passive1Icon = passive1Icon;
// move icons const passive1OverlayIcon = globalScene.add.image(12, 12, "candy_overlay");
passive1OverlayIcon.setOrigin(0, 0);
passive1OverlayIcon.setScale(0.25);
passive1OverlayIcon.setVisible(false);
this.add(passive1OverlayIcon);
this.passive1OverlayIcon = passive1OverlayIcon;
// passive icons
const passive2Icon = globalScene.add.image(12, 3, "candy"); const passive2Icon = globalScene.add.image(12, 3, "candy");
passive2Icon.setOrigin(0, 0); passive2Icon.setOrigin(0, 0);
passive2Icon.setScale(0.25); passive2Icon.setScale(0.25);
passive2Icon.setVisible(false); passive2Icon.setVisible(false);
this.add(passive2Icon); this.add(passive2Icon);
this.passive2Icon = passive2Icon; this.passive2Icon = passive2Icon;
const passive2OverlayIcon = globalScene.add.image(12, 12, "candy_overlay");
passive2OverlayIcon.setOrigin(0, 0);
passive2OverlayIcon.setScale(0.25);
passive2OverlayIcon.setVisible(false);
this.add(passive2OverlayIcon);
this.passive2OverlayIcon = passive2OverlayIcon;
} }
checkIconId(female, formIndex, shiny, variant) { checkIconId(female, formIndex, shiny, variant) {

View File

@ -43,7 +43,6 @@ import type { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { EggSourceType } from "#enums/egg-source-types"; import { EggSourceType } from "#enums/egg-source-types";
import { StarterContainer } from "#app/ui/starter-container";
import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters";
import { BooleanHolder, capitalizeString, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, rgbHexToRgba, toReadableString } from "#app/utils"; import { BooleanHolder, capitalizeString, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, rgbHexToRgba, toReadableString } from "#app/utils";
import type { Nature } from "#enums/nature"; import type { Nature } from "#enums/nature";
@ -128,7 +127,6 @@ interface SpeciesDetails {
formIndex?: number formIndex?: number
female?: boolean, female?: boolean,
variant?: number, variant?: number,
forSeen?: boolean, // default = false
} }
enum MenuOptions { enum MenuOptions {
@ -147,8 +145,6 @@ enum MenuOptions {
export default class PokedexPageUiHandler extends MessageUiHandler { export default class PokedexPageUiHandler extends MessageUiHandler {
private starterSelectContainer: Phaser.GameObjects.Container; private starterSelectContainer: Phaser.GameObjects.Container;
private shinyOverlay: Phaser.GameObjects.Image; private shinyOverlay: Phaser.GameObjects.Image;
private starterContainers: StarterContainer[] = [];
private filteredStarterContainers: StarterContainer[] = [];
private pokemonNumberText: Phaser.GameObjects.Text; private pokemonNumberText: Phaser.GameObjects.Text;
private pokemonSprite: Phaser.GameObjects.Sprite; private pokemonSprite: Phaser.GameObjects.Sprite;
private pokemonNameText: Phaser.GameObjects.Text; private pokemonNameText: Phaser.GameObjects.Text;
@ -199,6 +195,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
private allSpecies: PokemonSpecies[] = []; private allSpecies: PokemonSpecies[] = [];
private species: PokemonSpecies; private species: PokemonSpecies;
private starterId: number;
private formIndex: number; private formIndex: number;
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>(); private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
private levelMoves: LevelMoves; private levelMoves: LevelMoves;
@ -312,10 +309,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.speciesLoaded.set(species.speciesId, false); this.speciesLoaded.set(species.speciesId, false);
this.allSpecies.push(species); this.allSpecies.push(species);
const starterContainer = new StarterContainer(species).setVisible(false);
this.starterContainers.push(starterContainer);
starterBoxContainer.add(starterContainer);
} }
this.starterSelectContainer.add(starterBoxContainer); this.starterSelectContainer.add(starterBoxContainer);
@ -513,7 +506,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale; this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale;
this.menuBg = addWindow( this.menuBg = addWindow(
(globalScene.game.canvas.width / 6) - (this.optionSelectText.displayWidth + 25), (globalScene.game.canvas.width / 6 - 83),
0, 0,
this.optionSelectText.displayWidth + 19 + 24 * this.scale, this.optionSelectText.displayWidth + 19 + 24 * this.scale,
(globalScene.game.canvas.height / 6) - 2 (globalScene.game.canvas.height / 6) - 2
@ -555,8 +548,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
// Filter bar sits above everything, except the message box // Filter bar sits above everything, except the message box
this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer);
this.updateInstructions();
} }
show(args: any[]): boolean { show(args: any[]): boolean {
@ -603,6 +594,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
const species = this.species; const species = this.species;
const formIndex = this.formIndex ?? 0; const formIndex = this.formIndex ?? 0;
this.starterId = this.getStarterSpeciesId(this.species.speciesId);
const allEvolutions = pokemonEvolutions.hasOwnProperty(species.speciesId) ? pokemonEvolutions[species.speciesId] : []; const allEvolutions = pokemonEvolutions.hasOwnProperty(species.speciesId) ? pokemonEvolutions[species.speciesId] : [];
if (species.forms.length > 0) { if (species.forms.length > 0) {
@ -629,17 +622,19 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.baseTotal = species.baseTotal; this.baseTotal = species.baseTotal;
} }
this.eggMoves = speciesEggMoves[this.getStarterSpeciesId(species.speciesId)] ?? []; this.eggMoves = speciesEggMoves[this.starterId] ?? [];
this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)].eggMoves & (1 << em)) !== 0); this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.starterId].eggMoves & (1 << em)) !== 0);
const formKey = this.species?.forms.length > 0 ? this.species.forms[this.formIndex].formKey : ""; const formKey = this.species?.forms.length > 0 ? this.species.forms[this.formIndex].formKey : "";
this.tmMoves = speciesTmMoves[species.speciesId]?.filter(m => Array.isArray(m) ? (m[0] === formKey ? true : false ) : true) this.tmMoves = speciesTmMoves[species.speciesId]?.filter(m => Array.isArray(m) ? (m[0] === formKey ? true : false ) : true)
.map(m => Array.isArray(m) ? m[1] : m).sort((a, b) => allMoves[a].name > allMoves[b].name ? 1 : -1) ?? []; .map(m => Array.isArray(m) ? m[1] : m).sort((a, b) => allMoves[a].name > allMoves[b].name ? 1 : -1) ?? [];
const passives = starterPassiveAbilities[this.getStarterSpeciesId(species.speciesId)]; const passiveId = starterPassiveAbilities.hasOwnProperty(species.speciesId) ? species.speciesId :
starterPassiveAbilities.hasOwnProperty(this.starterId) ? this.starterId : pokemonPrevolutions[this.starterId];
const passives = starterPassiveAbilities[passiveId];
this.passive = (this.formIndex in passives) ? passives[formIndex] : passives[0]; this.passive = (this.formIndex in passives) ? passives[formIndex] : passives[0];
const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)]; const starterData = globalScene.gameData.starterData[this.starterId];
const abilityAttr = starterData.abilityAttr; const abilityAttr = starterData.abilityAttr;
this.hasPassive = starterData.passiveAttr > 0; this.hasPassive = starterData.passiveAttr > 0;
@ -655,9 +650,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
const allBiomes = catchableSpecies[species.speciesId] ?? []; const allBiomes = catchableSpecies[species.speciesId] ?? [];
this.preBiomes = this.sanitizeBiomes( this.preBiomes = this.sanitizeBiomes(
(catchableSpecies[this.getStarterSpeciesId(species.speciesId)] ?? []) (catchableSpecies[this.starterId] ?? [])
.filter(b => !allBiomes.some(bm => (b.biome === bm.biome && b.tier === bm.tier)) && !(b.biome === Biome.TOWN)), .filter(b => !allBiomes.some(bm => (b.biome === bm.biome && b.tier === bm.tier)) && !(b.biome === Biome.TOWN)),
this.getStarterSpeciesId(species.speciesId)); this.starterId);
this.biomes = this.sanitizeBiomes(allBiomes, species.speciesId); this.biomes = this.sanitizeBiomes(allBiomes, species.speciesId);
const allFormChanges = pokemonFormChanges.hasOwnProperty(species.speciesId) ? pokemonFormChanges[species.speciesId] : []; const allFormChanges = pokemonFormChanges.hasOwnProperty(species.speciesId) ? pokemonFormChanges[species.speciesId] : [];
@ -799,39 +794,43 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
const hasShiny = caughtAttr & DexAttr.SHINY; const hasShiny = caughtAttr & DexAttr.SHINY;
const hasNonShiny = caughtAttr & DexAttr.NON_SHINY; const hasNonShiny = caughtAttr & DexAttr.NON_SHINY;
if (starterAttributes.shiny && !hasShiny) { if (!hasShiny || (starterAttributes.shiny === undefined && hasNonShiny)) {
// shiny form wasn't unlocked, purging shiny and variant setting // shiny form wasn't unlocked, purging shiny and variant setting
starterAttributes.shiny = false; starterAttributes.shiny = false;
starterAttributes.variant = 0; starterAttributes.variant = 0;
} else if (starterAttributes.shiny === false && !hasNonShiny) { } else if (!hasNonShiny || (starterAttributes.shiny === undefined && hasShiny)) {
// non shiny form wasn't unlocked, purging shiny setting starterAttributes.shiny = true;
starterAttributes.shiny = false; starterAttributes.variant = 0;
} }
if (starterAttributes.variant !== undefined) { const unlockedVariants = [
const unlockedVariants = [ hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT,
hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, hasShiny && caughtAttr & DexAttr.VARIANT_2,
hasShiny && caughtAttr & DexAttr.VARIANT_2, hasShiny && caughtAttr & DexAttr.VARIANT_3
hasShiny && caughtAttr & DexAttr.VARIANT_3 ];
]; if (starterAttributes.variant === undefined || isNaN(starterAttributes.variant) || starterAttributes.variant < 0) {
if (isNaN(starterAttributes.variant) || starterAttributes.variant < 0) { starterAttributes.variant = 0;
starterAttributes.variant = 0; } else if (!unlockedVariants[starterAttributes.variant]) {
} else if (!unlockedVariants[starterAttributes.variant]) { let highestValidIndex = -1;
let highestValidIndex = -1; for (let i = 0; i <= starterAttributes.variant && i < unlockedVariants.length; i++) {
for (let i = 0; i <= starterAttributes.variant && i < unlockedVariants.length; i++) { if (unlockedVariants[i] !== 0n) {
if (unlockedVariants[i] !== 0n) { highestValidIndex = i;
highestValidIndex = i;
}
} }
// Set to the highest valid index found or default to 0
starterAttributes.variant = highestValidIndex !== -1 ? highestValidIndex : 0;
} }
// Set to the highest valid index found or default to 0
starterAttributes.variant = highestValidIndex !== -1 ? highestValidIndex : 0;
} }
if (starterAttributes.female !== undefined) { if (starterAttributes.female !== undefined) {
if ((starterAttributes.female && !(caughtAttr & DexAttr.FEMALE)) || (!starterAttributes.female && !(caughtAttr & DexAttr.MALE))) { if ((starterAttributes.female && !(caughtAttr & DexAttr.FEMALE)) || (!starterAttributes.female && !(caughtAttr & DexAttr.MALE))) {
starterAttributes.female = !starterAttributes.female; starterAttributes.female = !starterAttributes.female;
} }
} else {
if (caughtAttr & DexAttr.FEMALE) {
starterAttributes.female = true;
} else if (caughtAttr & DexAttr.MALE) {
starterAttributes.female = false;
}
} }
return starterAttributes; return starterAttributes;
@ -878,7 +877,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
* @returns the id of the corresponding starter * @returns the id of the corresponding starter
*/ */
getStarterSpeciesId(speciesId): number { getStarterSpeciesId(speciesId): number {
if (globalScene.gameData.starterData.hasOwnProperty(speciesId)) { if (speciesId === Species.PIKACHU) {
if ([ 0, 1, 8 ].includes(this.formIndex)) {
return Species.PICHU;
} else {
return Species.PIKACHU;
}
}
if (speciesStarterCosts.hasOwnProperty(speciesId)) {
return speciesId; return speciesId;
} else { } else {
return pokemonStarters[speciesId]; return pokemonStarters[speciesId];
@ -886,7 +892,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
} }
getStarterSpecies(species): PokemonSpecies { getStarterSpecies(species): PokemonSpecies {
if (globalScene.gameData.starterData.hasOwnProperty(species.speciesId)) { if (speciesStarterCosts.hasOwnProperty(species.speciesId)) {
return species; return species;
} else { } else {
return allSpecies.find(sp => sp.speciesId === pokemonStarters[species.speciesId]) ?? species; return allSpecies.find(sp => sp.speciesId === pokemonStarters[species.speciesId]) ?? species;
@ -970,7 +976,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
} }
} else { } else {
const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(this.species.speciesId)]; const starterData = globalScene.gameData.starterData[this.starterId];
// prepare persistent starter data to store changes // prepare persistent starter data to store changes
const starterAttributes = this.starterAttributes; const starterAttributes = this.starterAttributes;
@ -1126,6 +1132,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
if (!isCaught || !isFormCaught) { if (!isCaught || !isFormCaught) {
error = true; error = true;
} else if (this.tmMoves.length < 1) {
ui.showText(i18next.t("pokedexUiHandler:noTmMoves"));
error = true;
} else { } else {
this.blockInput = true; this.blockInput = true;
@ -1633,90 +1642,55 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
error = true; error = true;
} else { } else {
const ui = this.getUi(); const ui = this.getUi();
ui.showText("");
const options: any[] = []; // TODO: add proper type const options: any[] = []; // TODO: add proper type
const passiveAttr = starterData.passiveAttr; const passiveAttr = starterData.passiveAttr;
const candyCount = starterData.candyCount; const candyCount = starterData.candyCount;
if (!pokemonPrevolutions.hasOwnProperty(this.species.speciesId)) { if (!(passiveAttr & PassiveAttr.UNLOCKED)) {
if (!(passiveAttr & PassiveAttr.UNLOCKED)) { const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.starterId]);
const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)]);
options.push({
label: `x${passiveCost} ${i18next.t("pokedexUiHandler:unlockPassive")} (${allAbilities[this.passive].name})`,
handler: () => {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) {
starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED;
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= passiveCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
globalScene.gameData.saveSystem().then(success => {
if (!success) {
return globalScene.reset(true);
}
});
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
this.setSpeciesDetails(this.species);
globalScene.playSound("se/buy");
return true;
}
return false;
},
item: "candy",
itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)]
});
}
// Reduce cost option
const valueReduction = starterData.valueReduction;
if (valueReduction < valueReductionMax) {
const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)])[valueReduction];
options.push({
label: `x${reductionCost} ${i18next.t("pokedexUiHandler:reduceCost")}`,
handler: () => {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) {
starterData.valueReduction++;
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= reductionCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
globalScene.gameData.saveSystem().then(success => {
if (!success) {
return globalScene.reset(true);
}
});
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
globalScene.playSound("se/buy");
return true;
}
return false;
},
item: "candy",
itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)]
});
}
// Same species egg menu option.
const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)]);
options.push({ options.push({
label: `x${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`, label: `x${passiveCost} ${i18next.t("pokedexUiHandler:unlockPassive")} (${allAbilities[this.passive].name})`,
handler: () => { handler: () => {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost) { if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) {
if (globalScene.gameData.eggs.length >= 99 && !Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) { starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED;
// Egg list full, show error message at the top of the screen and abort
this.showText(i18next.t("egg:tooManyEggs"), undefined, () => this.showText("", 0, () => this.tutorialActive = false), 2000, false, undefined, true);
return false;
}
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= sameSpeciesEggCost; starterData.candyCount -= passiveCost;
} }
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
globalScene.gameData.saveSystem().then(success => {
if (!success) {
return globalScene.reset(true);
}
});
this.setSpeciesDetails(this.species);
globalScene.playSound("se/buy");
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
const egg = new Egg({ scene: globalScene, species: this.species.speciesId, sourceType: EggSourceType.SAME_SPECIES_EGG }); return true;
egg.addEggToGameData(); }
return false;
},
style: this.isPassiveAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
item: "candy",
itemArgs: this.isPassiveAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ]
});
}
// Reduce cost option
const valueReduction = starterData.valueReduction;
if (valueReduction < valueReductionMax) {
const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.starterId])[valueReduction];
options.push({
label: `x${reductionCost} ${i18next.t("pokedexUiHandler:reduceCost")}`,
handler: () => {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) {
starterData.valueReduction++;
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= reductionCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
globalScene.gameData.saveSystem().then(success => { globalScene.gameData.saveSystem().then(success => {
if (!success) { if (!success) {
return globalScene.reset(true); return globalScene.reset(true);
@ -1729,24 +1703,59 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
} }
return false; return false;
}, },
style: this.isValueReductionAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
item: "candy", item: "candy",
itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] itemArgs: this.isValueReductionAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ]
}); });
options.push({ }
label: i18next.t("menu:cancel"),
handler: () => { // Same species egg menu option.
const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]);
options.push({
label: `x${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`,
handler: () => {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost) {
if (globalScene.gameData.eggs.length >= 99 && !Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) {
// Egg list full, show error message at the top of the screen and abort
this.showText(i18next.t("egg:tooManyEggs"), undefined, () => this.showText("", 0, () => this.tutorialActive = false), 2000, false, undefined, true);
return false;
}
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= sameSpeciesEggCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
const egg = new Egg({ scene: globalScene, species: this.species.speciesId, sourceType: EggSourceType.SAME_SPECIES_EGG });
egg.addEggToGameData();
globalScene.gameData.saveSystem().then(success => {
if (!success) {
return globalScene.reset(true);
}
});
ui.setMode(Mode.POKEDEX_PAGE, "refresh"); ui.setMode(Mode.POKEDEX_PAGE, "refresh");
globalScene.playSound("se/buy");
return true; return true;
} }
}); return false;
ui.setModeWithoutClear(Mode.OPTION_SELECT, { },
options: options, style: this.isSameSpeciesEggAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
yOffset: 47 item: "candy",
}); itemArgs: this.isSameSpeciesEggAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ]
success = true; });
} else { options.push({
error = true; label: i18next.t("menu:cancel"),
} handler: () => {
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
return true;
}
});
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: options,
yOffset: 47
});
success = true;
} }
break; break;
case Button.CYCLE_ABILITY: case Button.CYCLE_ABILITY:
@ -1877,9 +1886,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
if (this.isCaught()) { if (this.isCaught()) {
if (isFormCaught) { if (isFormCaught) {
if (!pokemonPrevolutions.hasOwnProperty(this.species.speciesId)) { this.updateButtonIcon(SettingKeyboard.Button_Stats, gamepadType, this.candyUpgradeIconElement, this.candyUpgradeLabel);
this.updateButtonIcon(SettingKeyboard.Button_Stats, gamepadType, this.candyUpgradeIconElement, this.candyUpgradeLabel);
}
if (this.canCycleShiny) { if (this.canCycleShiny) {
this.updateButtonIcon(SettingKeyboard.Button_Cycle_Shiny, gamepadType, this.shinyIconElement, this.shinyLabel); this.updateButtonIcon(SettingKeyboard.Button_Cycle_Shiny, gamepadType, this.shinyIconElement, this.shinyLabel);
} }
@ -1936,16 +1943,51 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
} }
getFriendship(speciesId: number) { getFriendship(speciesId: number) {
let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship; let currentFriendship = globalScene.gameData.starterData[this.starterId].friendship;
if (!currentFriendship || currentFriendship === undefined) { if (!currentFriendship || currentFriendship === undefined) {
currentFriendship = 0; currentFriendship = 0;
} }
const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]); const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.starterId]);
return { currentFriendship, friendshipCap }; return { currentFriendship, friendshipCap };
} }
/**
* Determines if a passive upgrade is available for the current species
* @returns true if the user has enough candies and a passive has not been unlocked already
*/
isPassiveAvailable(): boolean {
// Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.starterId];
return starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[this.starterId])
&& !(starterData.passiveAttr & PassiveAttr.UNLOCKED);
}
/**
* Determines if a value reduction upgrade is available for the current species
* @returns true if the user has enough candies and all value reductions have not been unlocked already
*/
isValueReductionAvailable(): boolean {
// Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.starterId];
return starterData.candyCount >= getValueReductionCandyCounts(speciesStarterCosts[this.starterId])[starterData.valueReduction]
&& starterData.valueReduction < valueReductionMax;
}
/**
* Determines if an same species egg can be bought for the current species
* @returns true if the user has enough candies
*/
isSameSpeciesEggAvailable(): boolean {
// Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.starterId];
return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]);
}
setSpecies() { setSpecies() {
const species = this.species; const species = this.species;
const starterAttributes : StarterAttributes | null = species ? { ...this.starterAttributes } : null; const starterAttributes : StarterAttributes | null = species ? { ...this.starterAttributes } : null;
@ -1967,88 +2009,10 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
if (species && (this.speciesStarterDexEntry?.seenAttr || this.isCaught())) { if (species && (this.speciesStarterDexEntry?.seenAttr || this.isCaught())) {
this.pokemonNumberText.setText(padInt(species.speciesId, 4)); this.pokemonNumberText.setText(padInt(species.speciesId, 4));
if (starterAttributes?.nickname) {
const name = decodeURIComponent(escape(atob(starterAttributes.nickname)));
this.pokemonNameText.setText(name);
} else {
this.pokemonNameText.setText(species.name);
}
if (this.isCaught()) { if (this.isCaught()) {
const colorScheme = starterColors[species.speciesId];
const luck = globalScene.gameData.getDexAttrLuck(this.isCaught());
this.pokemonLuckText.setVisible(!!luck);
this.pokemonLuckText.setText(luck.toString());
this.pokemonLuckText.setTint(getVariantTint(Math.min(luck - 1, 2) as Variant));
this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible);
//Growth translate
let growthReadable = toReadableString(GrowthRate[species.growthRate]);
const growthAux = growthReadable.replace(" ", "_");
if (i18next.exists("growth:" + growthAux)) {
growthReadable = i18next.t("growth:" + growthAux as any);
}
this.pokemonGrowthRateText.setText(growthReadable);
this.pokemonGrowthRateText.setColor(getGrowthRateColor(species.growthRate));
this.pokemonGrowthRateText.setShadowColor(getGrowthRateColor(species.growthRate, true));
this.pokemonGrowthRateLabelText.setVisible(true);
this.pokemonUncaughtText.setVisible(false);
this.pokemonCaughtCountText.setText(`${this.speciesStarterDexEntry?.caughtCount}`);
if (species.speciesId === Species.MANAPHY || species.speciesId === Species.PHIONE) {
this.pokemonHatchedIcon.setFrame("manaphy");
} else {
this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species));
}
this.pokemonHatchedCountText.setText(`${this.speciesStarterDexEntry?.hatchedCount}`);
const defaultDexAttr = this.getCurrentDexProps(species.speciesId); const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
const variant = defaultProps.variant;
const tint = getVariantTint(variant);
this.pokemonShinyIcon.setFrame(getVariantIcon(variant));
this.pokemonShinyIcon.setTint(tint);
this.pokemonShinyIcon.setVisible(defaultProps.shiny);
this.pokemonCaughtHatchedContainer.setVisible(true);
this.pokemonFormText.setVisible(true);
if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) {
this.pokemonCaughtHatchedContainer.setY(16);
this.pokemonShinyIcon.setY(135);
this.pokemonShinyIcon.setFrame(getVariantIcon(variant));
[
this.pokemonCandyContainer,
this.pokemonHatchedIcon,
this.pokemonHatchedCountText
].map(c => c.setVisible(false));
this.pokemonFormText.setY(25);
} else {
this.pokemonCaughtHatchedContainer.setY(25);
this.pokemonShinyIcon.setY(117);
this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0])));
this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1])));
this.pokemonCandyCountText.setText(`x${globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)].candyCount}`);
this.pokemonCandyContainer.setVisible(true);
this.pokemonFormText.setY(42);
this.pokemonHatchedIcon.setVisible(true);
this.pokemonHatchedCountText.setVisible(true);
const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId);
const candyCropY = 16 - (16 * (currentFriendship / friendshipCap));
this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY);
this.pokemonCandyContainer.on("pointerover", () => {
globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true);
this.activeTooltip = "CANDY";
});
this.pokemonCandyContainer.on("pointerout", () => {
globalScene.ui.hideTooltip();
this.activeTooltip = undefined;
});
}
// Set default attributes if for some reason starterAttributes does not exist or attributes missing // Set default attributes if for some reason starterAttributes does not exist or attributes missing
const props: StarterAttributes = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); const props: StarterAttributes = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
if (starterAttributes?.variant && !isNaN(starterAttributes.variant)) { if (starterAttributes?.variant && !isNaN(starterAttributes.variant)) {
@ -2065,12 +2029,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
female: props.female, female: props.female,
variant: props.variant ?? 0, variant: props.variant ?? 0,
}); });
if (this.isFormCaught(this.species, props.form)) {
const speciesForm = getPokemonSpeciesForm(species.speciesId, props.form ?? 0);
this.setTypeIcons(speciesForm.type1, speciesForm.type2);
this.pokemonSprite.clearTint();
}
} else { } else {
this.pokemonGrowthRateText.setText(""); this.pokemonGrowthRateText.setText("");
this.pokemonGrowthRateLabelText.setVisible(false); this.pokemonGrowthRateLabelText.setVisible(false);
@ -2092,7 +2050,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
formIndex: props.formIndex, formIndex: props.formIndex,
female: props.female, female: props.female,
variant: props.variant, variant: props.variant,
forSeen: true
}); });
this.pokemonSprite.setTint(0x808080); this.pokemonSprite.setTint(0x808080);
} }
@ -2123,7 +2080,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, forceUpdate?: boolean): void { setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, forceUpdate?: boolean): void {
let { shiny, formIndex, female, variant } = options; let { shiny, formIndex, female, variant } = options;
const forSeen: boolean = options.forSeen ?? false;
const oldProps = species ? this.starterAttributes : null; const oldProps = species ? this.starterAttributes : null;
// We will only update the sprite if there is a change to form, shiny/variant // We will only update the sprite if there is a change to form, shiny/variant
@ -2194,12 +2150,12 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
} }
const isFormCaught = this.isFormCaught(); const isFormCaught = this.isFormCaught();
const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false;
this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default? this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default?
this.pokemonNumberText.setColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, false)); this.pokemonNumberText.setColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, false));
this.pokemonNumberText.setShadowColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, true)); this.pokemonNumberText.setShadowColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, true));
const assetLoadCancelled = new BooleanHolder(false); const assetLoadCancelled = new BooleanHolder(false);
this.assetLoadCancelled = assetLoadCancelled; this.assetLoadCancelled = assetLoadCancelled;
@ -2221,13 +2177,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonSprite.setVisible(!this.statsMode); this.pokemonSprite.setVisible(!this.statsMode);
} }
const currentFilteredContainer = this.filteredStarterContainers.find(p => p.species.speciesId === species.speciesId);
if (currentFilteredContainer) {
const starterSprite = currentFilteredContainer.icon as Phaser.GameObjects.Sprite;
starterSprite.setTexture(species.getIconAtlasKey(formIndex, shiny, variant), species.getIconId(female!, formIndex, shiny, variant));
currentFilteredContainer.checkIconId(female, formIndex, shiny, variant);
}
const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY); const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY);
const isShinyCaught = !!(caughtAttr & DexAttr.SHINY); const isShinyCaught = !!(caughtAttr & DexAttr.SHINY);
@ -2250,27 +2199,129 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonGenderText.setText(""); this.pokemonGenderText.setText("");
} }
if (caughtAttr) { // Setting the name
if (isFormCaught) { if (isFormCaught || isFormSeen) {
this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => { this.pokemonNameText.setText(species.name);
const crier = (this.species.forms && this.species.forms.length > 0) ? this.species.forms[formIndex ?? this.formIndex] : this.species; } else {
crier.cry(); this.pokemonNameText.setText(species ? "???" : "");
});
this.pokemonSprite.clearTint();
} else {
this.pokemonSprite.setTint(0x000000);
}
} }
if (caughtAttr || forSeen) { // Setting tint of the sprite
if (isFormCaught) {
this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => {
const crier = (this.species.forms && this.species.forms.length > 0) ? this.species.forms[formIndex ?? this.formIndex] : this.species;
crier.cry();
});
this.pokemonSprite.clearTint();
} else if (isFormSeen) {
this.pokemonSprite.setTint(0x808080);
} else {
this.pokemonSprite.setTint(0);
}
// Setting luck text and sparks
if (isFormCaught) {
const luck = globalScene.gameData.getDexAttrLuck(this.isCaught());
this.pokemonLuckText.setVisible(!!luck);
this.pokemonLuckText.setText(luck.toString());
this.pokemonLuckText.setTint(getVariantTint(Math.min(luck - 1, 2) as Variant));
this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible);
} else {
this.pokemonLuckText.setVisible(false);
this.pokemonLuckLabelText.setVisible(false);
}
// Setting growth rate text
if (isFormCaught) {
let growthReadable = toReadableString(GrowthRate[species.growthRate]);
const growthAux = growthReadable.replace(" ", "_");
if (i18next.exists("growth:" + growthAux)) {
growthReadable = i18next.t("growth:" + growthAux as any);
}
this.pokemonGrowthRateText.setText(growthReadable);
this.pokemonGrowthRateText.setColor(getGrowthRateColor(species.growthRate));
this.pokemonGrowthRateText.setShadowColor(getGrowthRateColor(species.growthRate, true));
this.pokemonGrowthRateLabelText.setVisible(true);
} else {
this.pokemonGrowthRateText.setText("");
this.pokemonGrowthRateLabelText.setVisible(false);
}
// Caught and hatched
if (isFormCaught) {
const colorScheme = starterColors[this.starterId];
this.pokemonUncaughtText.setVisible(false);
this.pokemonCaughtCountText.setText(`${this.speciesStarterDexEntry?.caughtCount}`);
if (species.speciesId === Species.MANAPHY || species.speciesId === Species.PHIONE) {
this.pokemonHatchedIcon.setFrame("manaphy");
} else {
this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species));
}
this.pokemonHatchedCountText.setText(`${this.speciesStarterDexEntry?.hatchedCount}`);
const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
const variant = defaultProps.variant;
const tint = getVariantTint(variant);
this.pokemonShinyIcon.setFrame(getVariantIcon(variant));
this.pokemonShinyIcon.setTint(tint);
this.pokemonShinyIcon.setVisible(defaultProps.shiny);
this.pokemonCaughtHatchedContainer.setVisible(true);
this.pokemonCaughtHatchedContainer.setY(25);
this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0])));
this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1])));
this.pokemonCandyCountText.setText(`x${globalScene.gameData.starterData[this.starterId].candyCount}`);
this.pokemonCandyContainer.setVisible(true);
if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) {
this.pokemonShinyIcon.setY(135);
this.pokemonShinyIcon.setFrame(getVariantIcon(variant));
this.pokemonHatchedIcon.setVisible(false);
this.pokemonHatchedCountText.setVisible(false);
this.pokemonFormText.setY(36);
} else {
this.pokemonShinyIcon.setY(117);
this.pokemonHatchedIcon.setVisible(true);
this.pokemonHatchedCountText.setVisible(true);
this.pokemonFormText.setY(42);
const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId);
const candyCropY = 16 - (16 * (currentFriendship / friendshipCap));
this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY);
this.pokemonCandyContainer.on("pointerover", () => {
globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true);
this.activeTooltip = "CANDY";
});
this.pokemonCandyContainer.on("pointerout", () => {
globalScene.ui.hideTooltip();
this.activeTooltip = undefined;
});
}
} else {
this.pokemonUncaughtText.setVisible(true);
this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false);
this.pokemonShinyIcon.setVisible(false);
}
// Setting type icons and form text
if (isFormCaught || isFormSeen) {
const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct? const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct?
this.setTypeIcons(speciesForm.type1, speciesForm.type2); this.setTypeIcons(speciesForm.type1, speciesForm.type2);
this.pokemonFormText.setText(this.getFormString((speciesForm as PokemonForm).formKey, species)); this.pokemonFormText.setText(this.getFormString((speciesForm as PokemonForm).formKey, species));
this.pokemonFormText.setVisible(true);
if (!isFormCaught) {
this.pokemonFormText.setY(18);
}
} else { } else {
this.setTypeIcons(null, null); this.setTypeIcons(null, null);
this.pokemonFormText.setText(""); this.pokemonFormText.setText("");
this.pokemonFormText.setVisible(false);
} }
} else { } else {
this.shinyOverlay.setVisible(false); this.shinyOverlay.setVisible(false);

View File

@ -11,7 +11,7 @@ import { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data
import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters";
import { catchableSpecies } from "#app/data/balance/biomes"; import { catchableSpecies } from "#app/data/balance/biomes";
import { Type } from "#enums/type"; import { Type } from "#enums/type";
import type { DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences } from "#app/system/game-data"; import type { DexAttrProps, DexEntry, StarterAttributes, StarterPreferences } from "#app/system/game-data";
import { AbilityAttr, DexAttr, StarterPrefs } from "#app/system/game-data"; import { AbilityAttr, DexAttr, StarterPrefs } from "#app/system/game-data";
import MessageUiHandler from "#app/ui/message-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler";
import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler";
@ -19,7 +19,6 @@ import { TextStyle, addTextObject } from "#app/ui/text";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { SettingKeyboard } from "#app/system/settings/settings-keyboard";
import { Passive as PassiveAttr } from "#enums/passive"; import { Passive as PassiveAttr } from "#enums/passive";
import type { Moves } from "#enums/moves";
import type { Species } from "#enums/species"; import type { Species } from "#enums/species";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#app/ui/dropdown"; import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#app/ui/dropdown";
@ -42,7 +41,6 @@ import { pokemonStarters } from "#app/data/balance/pokemon-evolutions";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
interface LanguageSetting { interface LanguageSetting {
starterInfoTextSize: string, starterInfoTextSize: string,
instructionTextSize: string, instructionTextSize: string,
@ -139,7 +137,6 @@ interface SpeciesDetails {
variant?: Variant, variant?: Variant,
abilityIndex?: number, abilityIndex?: number,
natureIndex?: number, natureIndex?: number,
forSeen?: boolean, // default = false
} }
export default class PokedexUiHandler extends MessageUiHandler { export default class PokedexUiHandler extends MessageUiHandler {
@ -161,7 +158,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
private filterMode: boolean; private filterMode: boolean;
private filterBarCursor: number = 0; private filterBarCursor: number = 0;
private starterMoveset: StarterMoveset | null;
private scrollCursor: number; private scrollCursor: number;
private allSpecies: PokemonSpecies[] = []; private allSpecies: PokemonSpecies[] = [];
@ -169,7 +165,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>(); private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
private pokerusSpecies: PokemonSpecies[] = []; private pokerusSpecies: PokemonSpecies[] = [];
private speciesStarterDexEntry: DexEntry | null; private speciesStarterDexEntry: DexEntry | null;
private speciesStarterMoves: Moves[];
private assetLoadCancelled: BooleanHolder | null; private assetLoadCancelled: BooleanHolder | null;
public cursorObj: Phaser.GameObjects.Image; public cursorObj: Phaser.GameObjects.Image;
@ -206,6 +201,20 @@ export default class PokedexUiHandler extends MessageUiHandler {
private toggleDecorationsIconElement: Phaser.GameObjects.Sprite; private toggleDecorationsIconElement: Phaser.GameObjects.Sprite;
private toggleDecorationsLabel: Phaser.GameObjects.Text; private toggleDecorationsLabel: Phaser.GameObjects.Text;
private formTrayContainer: Phaser.GameObjects.Container;
private trayBg: Phaser.GameObjects.NineSlice;
private trayForms: PokemonForm[];
private trayContainers: PokedexMonContainer[] = [];
private trayNumIcons: number;
private trayRows: number;
private trayColumns: number;
private trayCursorObj: Phaser.GameObjects.Image;
private trayCursor: number = 0;
private showingTray: boolean = false;
private showFormTrayIconElement: Phaser.GameObjects.Sprite;
private showFormTrayLabel: Phaser.GameObjects.Text;
private canShowFormTray: boolean;
constructor() { constructor() {
super(Mode.POKEDEX); super(Mode.POKEDEX);
} }
@ -425,7 +434,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.cursorObj = globalScene.add.image(0, 0, "select_cursor"); this.cursorObj = globalScene.add.image(0, 0, "select_cursor");
this.cursorObj.setOrigin(0, 0); this.cursorObj.setOrigin(0, 0);
starterBoxContainer.add(this.cursorObj); starterBoxContainer.add(this.cursorObj);
for (const species of allSpecies) { for (const species of allSpecies) {
@ -438,6 +446,20 @@ export default class PokedexUiHandler extends MessageUiHandler {
starterBoxContainer.add(pokemonContainer); starterBoxContainer.add(pokemonContainer);
} }
// Tray to display forms
this.formTrayContainer = globalScene.add.container(0, 0);
this.trayBg = addWindow(0, 0, 0, 0);
this.trayBg.setOrigin(0, 0);
this.formTrayContainer.add(this.trayBg);
this.trayCursorObj = globalScene.add.image(0, 0, "select_cursor");
this.trayCursorObj.setOrigin(0, 0);
this.formTrayContainer.add(this.trayCursorObj);
starterBoxContainer.add(this.formTrayContainer);
starterBoxContainer.bringToTop(this.formTrayContainer);
this.formTrayContainer.setVisible(false);
this.starterSelectContainer.add(starterBoxContainer); this.starterSelectContainer.add(starterBoxContainer);
this.pokemonSprite = globalScene.add.sprite(96, 143, "pkmn__sub"); this.pokemonSprite = globalScene.add.sprite(96, 143, "pkmn__sub");
@ -449,7 +471,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.type1Icon.setOrigin(0, 0); this.type1Icon.setOrigin(0, 0);
this.starterSelectContainer.add(this.type1Icon); this.starterSelectContainer.add(this.type1Icon);
this.type2Icon = globalScene.add.sprite(10, 166, getLocalizedSpriteKey("types")); this.type2Icon = globalScene.add.sprite(28, 158, getLocalizedSpriteKey("types"));
this.type2Icon.setScale(0.5); this.type2Icon.setScale(0.5);
this.type2Icon.setOrigin(0, 0); this.type2Icon.setOrigin(0, 0);
this.starterSelectContainer.add(this.type2Icon); this.starterSelectContainer.add(this.type2Icon);
@ -488,6 +510,17 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.starterSelectContainer.add(this.toggleDecorationsIconElement); this.starterSelectContainer.add(this.toggleDecorationsIconElement);
this.starterSelectContainer.add(this.toggleDecorationsLabel); this.starterSelectContainer.add(this.toggleDecorationsLabel);
this.showFormTrayIconElement = new Phaser.GameObjects.Sprite(globalScene, 6, 168, "keyboard", "F.png");
this.showFormTrayIconElement.setName("sprite-showFormTray-icon-element");
this.showFormTrayIconElement.setScale(0.675);
this.showFormTrayIconElement.setOrigin(0.0, 0.0);
this.showFormTrayLabel = addTextObject(16, 168, i18next.t("pokedexUiHandler:showForms"), TextStyle.PARTY, { fontSize: instructionTextSize });
this.showFormTrayLabel.setName("text-showFormTray-label");
this.showFormTrayIconElement.setVisible(false);
this.showFormTrayLabel.setVisible(false);
this.starterSelectContainer.add(this.showFormTrayIconElement);
this.starterSelectContainer.add(this.showFormTrayLabel);
this.message = addTextObject(8, 8, "", TextStyle.WINDOW, { maxLines: 2 }); this.message = addTextObject(8, 8, "", TextStyle.WINDOW, { maxLines: 2 });
this.message.setOrigin(0, 0); this.message.setOrigin(0, 0);
this.starterSelectMessageBoxContainer.add(this.message); this.starterSelectMessageBoxContainer.add(this.message);
@ -527,7 +560,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.starterPreferences[species.speciesId] = this.initStarterPrefs(species); this.starterPreferences[species.speciesId] = this.initStarterPrefs(species);
if (dexEntry.caughtAttr) { if (dexEntry.caughtAttr || globalScene.dexForDevs) {
icon.clearTint(); icon.clearTint();
} else if (dexEntry.seenAttr) { } else if (dexEntry.seenAttr) {
icon.setTint(0x808080); icon.setTint(0x808080);
@ -860,32 +893,42 @@ export default class PokedexUiHandler extends MessageUiHandler {
} else if (this.filterTextMode && !(this.filterText.getValue(this.filterTextCursor) === this.filterText.defaultText)) { } else if (this.filterTextMode && !(this.filterText.getValue(this.filterTextCursor) === this.filterText.defaultText)) {
this.filterText.resetSelection(this.filterTextCursor); this.filterText.resetSelection(this.filterTextCursor);
success = true; success = true;
} else if (this.showingTray) {
success = this.closeFormTray();
} else { } else {
this.tryExit(); this.tryExit();
success = true; success = true;
} }
} else if (button === Button.STATS) { } else if (button === Button.STATS) {
if (!this.filterMode) { if (!this.filterMode && !this.showingTray) {
this.cursorObj.setVisible(false); this.cursorObj.setVisible(false);
this.setSpecies(null); this.setSpecies(null);
this.filterText.cursorObj.setVisible(false); this.filterText.cursorObj.setVisible(false);
this.filterTextMode = false; this.filterTextMode = false;
this.filterBarCursor = 0; this.filterBarCursor = 0;
this.setFilterMode(true); this.setFilterMode(true);
} else {
error = true;
} }
} else if (button === Button.V) { } else if (button === Button.V) {
if (!this.filterTextMode) { if (!this.filterTextMode && !this.showingTray) {
this.cursorObj.setVisible(false); this.cursorObj.setVisible(false);
this.setSpecies(null); this.setSpecies(null);
this.filterBar.cursorObj.setVisible(false); this.filterBar.cursorObj.setVisible(false);
this.filterMode = false; this.filterMode = false;
this.filterTextCursor = 0; this.filterTextCursor = 0;
this.setFilterTextMode(true); this.setFilterTextMode(true);
} else {
error = true;
} }
} else if (button === Button.CYCLE_SHINY) { } else if (button === Button.CYCLE_SHINY) {
this.showDecorations = !this.showDecorations; if (!this.showingTray) {
this.updateScroll(); this.showDecorations = !this.showDecorations;
success = true; this.updateScroll();
success = true;
} else {
error = true;
}
} else if (this.filterMode) { } else if (this.filterMode) {
switch (button) { switch (button) {
case Button.LEFT: case Button.LEFT:
@ -982,8 +1025,55 @@ export default class PokedexUiHandler extends MessageUiHandler {
success = true; success = true;
break; break;
} }
} else if (this.showingTray) {
if (button === Button.ACTION) {
const formIndex = this.trayForms[this.trayCursor].formIndex;
ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, formIndex, { form: formIndex });
success = true;
} else {
const numberOfForms = this.trayContainers.length;
const numOfRows = Math.ceil(numberOfForms / maxColumns);
const currentRow = Math.floor(this.trayCursor / maxColumns);
switch (button) {
case Button.UP:
if (currentRow > 0) {
success = this.setTrayCursor(this.trayCursor - 9);
} else {
const targetCol = this.trayCursor;
if (numberOfForms % 9 > targetCol) {
success = this.setTrayCursor(numberOfForms - (numberOfForms) % 9 + targetCol);
} else {
success = this.setTrayCursor(Math.max(numberOfForms - (numberOfForms) % 9 + targetCol - 9, 0));
}
}
break;
case Button.DOWN:
if (currentRow < numOfRows - 1) {
success = this.setTrayCursor(this.trayCursor + 9);
} else {
success = this.setTrayCursor(this.trayCursor % 9);
}
break;
case Button.LEFT:
if (this.trayCursor % 9 !== 0) {
success = this.setTrayCursor(this.trayCursor - 1);
} else {
success = this.setTrayCursor(currentRow < numOfRows - 1 ? (currentRow + 1) * maxColumns - 1 : numberOfForms - 1);
}
break;
case Button.RIGHT:
if (this.trayCursor % 9 < (currentRow < numOfRows - 1 ? 8 : (numberOfForms - 1) % 9)) {
success = this.setTrayCursor(this.trayCursor + 1);
} else {
success = this.setTrayCursor(currentRow * 9);
}
break;
case Button.CYCLE_FORM:
success = this.closeFormTray();
break;
}
}
} else { } else {
if (button === Button.ACTION) { if (button === Button.ACTION) {
ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, 0); ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, 0);
success = true; success = true;
@ -1042,6 +1132,12 @@ export default class PokedexUiHandler extends MessageUiHandler {
success = true; success = true;
} }
break; break;
case Button.CYCLE_FORM:
const species = this.filteredPokemonContainers[this.cursor].species;
if (this.canShowFormTray) {
success = this.openFormTray(species);
}
break;
} }
} }
} }
@ -1068,6 +1164,9 @@ export default class PokedexUiHandler extends MessageUiHandler {
case SettingKeyboard.Button_Cycle_Variant: case SettingKeyboard.Button_Cycle_Variant:
iconPath = "V.png"; iconPath = "V.png";
break; break;
case SettingKeyboard.Button_Cycle_Form:
iconPath = "F.png";
break;
case SettingKeyboard.Button_Stats: case SettingKeyboard.Button_Stats:
iconPath = "C.png"; iconPath = "C.png";
break; break;
@ -1145,13 +1244,15 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.validPokemonContainers.forEach(container => { this.validPokemonContainers.forEach(container => {
container.setVisible(false); container.setVisible(false);
container.cost = globalScene.gameData.getSpeciesStarterValue(this.getStarterSpeciesId(container.species.speciesId)); const starterId = this.getStarterSpeciesId(container.species.speciesId);
container.cost = globalScene.gameData.getSpeciesStarterValue(starterId);
// First, ensure you have the caught attributes for the species else default to bigint 0 // First, ensure you have the caught attributes for the species else default to bigint 0
// TODO: This might be removed depending on how accessible we want the pokedex function to be // TODO: This might be removed depending on how accessible we want the pokedex function to be
const caughtAttr = globalScene.gameData.dexData[container.species.speciesId]?.caughtAttr || BigInt(0); const caughtAttr = globalScene.gameData.dexData[container.species.speciesId]?.caughtAttr || BigInt(0);
const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(container.species.speciesId)]; const starterData = globalScene.gameData.starterData[starterId];
const isStarterProgressable = speciesEggMoves.hasOwnProperty(this.getStarterSpeciesId(container.species.speciesId)); const isStarterProgressable = speciesEggMoves.hasOwnProperty(starterId);
// Name filter // Name filter
const selectedName = this.filterText.getValue(FilterTextRow.NAME); const selectedName = this.filterText.getValue(FilterTextRow.NAME);
@ -1162,8 +1263,8 @@ export default class PokedexUiHandler extends MessageUiHandler {
// On the other hand, in some cases it is possible to switch between different forms and combine (Deoxys) // On the other hand, in some cases it is possible to switch between different forms and combine (Deoxys)
const levelMoves = pokemonSpeciesLevelMoves[container.species.speciesId].map(m => allMoves[m[1]].name); const levelMoves = pokemonSpeciesLevelMoves[container.species.speciesId].map(m => allMoves[m[1]].name);
// This always gets egg moves from the starter // This always gets egg moves from the starter
const eggMoves = speciesEggMoves[this.getStarterSpeciesId(container.species.speciesId)]?.map(m => allMoves[m].name) ?? []; const eggMoves = speciesEggMoves[starterId]?.map(m => allMoves[m].name) ?? [];
const tmMoves = speciesTmMoves[this.getStarterSpeciesId(container.species.speciesId)]?.map(m => allMoves[Array.isArray(m) ? m[1] : m].name) ?? []; const tmMoves = speciesTmMoves[starterId]?.map(m => allMoves[Array.isArray(m) ? m[1] : m].name) ?? [];
const selectedMove1 = this.filterText.getValue(FilterTextRow.MOVE_1); const selectedMove1 = this.filterText.getValue(FilterTextRow.MOVE_1);
const selectedMove2 = this.filterText.getValue(FilterTextRow.MOVE_2); const selectedMove2 = this.filterText.getValue(FilterTextRow.MOVE_2);
@ -1185,27 +1286,40 @@ export default class PokedexUiHandler extends MessageUiHandler {
container.tmMove2Icon.setVisible(false); container.tmMove2Icon.setVisible(false);
if (fitsEggMove1 && !fitsLevelMove1) { if (fitsEggMove1 && !fitsLevelMove1) {
container.eggMove1Icon.setVisible(true); container.eggMove1Icon.setVisible(true);
const em1 = eggMoves.findIndex(name => name === selectedMove1);
if ((starterData[starterId].eggMoves & (1 << em1)) === 0) {
container.eggMove1Icon.setTint(0x808080);
} else {
container.eggMove1Icon.clearTint();
}
} else if (fitsTmMove1 && !fitsLevelMove1) { } else if (fitsTmMove1 && !fitsLevelMove1) {
container.tmMove1Icon.setVisible(true); container.tmMove1Icon.setVisible(true);
} }
if (fitsEggMove2 && !fitsLevelMove2) { if (fitsEggMove2 && !fitsLevelMove2) {
container.eggMove2Icon.setVisible(true); container.eggMove2Icon.setVisible(true);
const em2 = eggMoves.findIndex(name => name === selectedMove2);
if ((starterData[starterId].eggMoves & (1 << em2)) === 0) {
container.eggMove2Icon.setTint(0x808080);
} else {
container.eggMove2Icon.clearTint();
}
} else if (fitsTmMove2 && !fitsLevelMove2) { } else if (fitsTmMove2 && !fitsLevelMove2) {
container.tmMove2Icon.setVisible(true); container.tmMove2Icon.setVisible(true);
} }
// Ability filter // Ability filter
const abilities = [ container.species.ability1, container.species.ability2, container.species.abilityHidden ].map(a => allAbilities[a].name); const abilities = [ container.species.ability1, container.species.ability2, container.species.abilityHidden ].map(a => allAbilities[a].name);
const passives = starterPassiveAbilities[this.getStarterSpeciesId(container.species.speciesId)] ?? {} as PassiveAbilities; const passives = starterPassiveAbilities[starterId] ?? {} as PassiveAbilities;
const selectedAbility1 = this.filterText.getValue(FilterTextRow.ABILITY_1); const selectedAbility1 = this.filterText.getValue(FilterTextRow.ABILITY_1);
const fitsFormAbility = container.species.forms.some(form => allAbilities[form.ability1].name === selectedAbility1); const fitsFormAbility1 = container.species.forms.some(form => [ form.ability1, form.ability2, form.abilityHidden ].map(a => allAbilities[a].name).includes(selectedAbility1));
const fitsAbility1 = abilities.includes(selectedAbility1) || fitsFormAbility || selectedAbility1 === this.filterText.defaultText; const fitsAbility1 = abilities.includes(selectedAbility1) || fitsFormAbility1 || selectedAbility1 === this.filterText.defaultText;
const fitsPassive1 = Object.values(passives).some(p => p.name === selectedAbility1); const fitsPassive1 = Object.values(passives).some(p => allAbilities[p].name === selectedAbility1);
const selectedAbility2 = this.filterText.getValue(FilterTextRow.ABILITY_2); const selectedAbility2 = this.filterText.getValue(FilterTextRow.ABILITY_2);
const fitsAbility2 = abilities.includes(selectedAbility2) || fitsFormAbility || selectedAbility2 === this.filterText.defaultText; const fitsFormAbility2 = container.species.forms.some(form => [ form.ability1, form.ability2, form.abilityHidden ].map(a => allAbilities[a].name).includes(selectedAbility2));
const fitsPassive2 = Object.values(passives).some(p => p.name === selectedAbility2); const fitsAbility2 = abilities.includes(selectedAbility2) || fitsFormAbility2 || selectedAbility2 === this.filterText.defaultText;
const fitsPassive2 = Object.values(passives).some(p => allAbilities[p].name === selectedAbility2);
// If both fields have been set to the same ability, show both ability and passive // If both fields have been set to the same ability, show both ability and passive
const fitsAbilities = (fitsAbility1 && (fitsPassive2 || selectedAbility2 === this.filterText.defaultText)) || const fitsAbilities = (fitsAbility1 && (fitsPassive2 || selectedAbility2 === this.filterText.defaultText)) ||
@ -1213,11 +1327,26 @@ export default class PokedexUiHandler extends MessageUiHandler {
container.passive1Icon.setVisible(false); container.passive1Icon.setVisible(false);
container.passive2Icon.setVisible(false); container.passive2Icon.setVisible(false);
if (fitsPassive1) { if (fitsPassive1 || fitsPassive2) {
container.passive1Icon.setVisible(true); if (fitsPassive1) {
} if (starterData.passiveAttr > 0) {
if (fitsPassive2) { container.passive1Icon.clearTint();
container.passive2Icon.setVisible(true); container.passive1OverlayIcon.clearTint();
} else {
container.passive1Icon.setTint(0x808080);
container.passive1OverlayIcon.setTint(0x808080);
}
container.passive1Icon.setVisible(true);
} else {
if (starterData.passiveAttr > 0) {
container.passive2Icon.clearTint();
container.passive2OverlayIcon.clearTint();
} else {
container.passive2Icon.setTint(0x808080);
container.passive2OverlayIcon.setTint(0x808080);
}
container.passive2Icon.setVisible(true);
}
} }
// Gen filter // Gen filter
@ -1236,7 +1365,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
// We get biomes for both the mon and its starters to ensure that evolutions get the correct filters. // We get biomes for both the mon and its starters to ensure that evolutions get the correct filters.
// TODO: We might also need to do it the other way around. // TODO: We might also need to do it the other way around.
const biomes = catchableSpecies[container.species.speciesId].concat(catchableSpecies[this.getStarterSpeciesId(container.species.speciesId)]).map(b => Biome[b.biome]); const biomes = catchableSpecies[container.species.speciesId].concat(catchableSpecies[starterId]).map(b => Biome[b.biome]);
if (biomes.length === 0) { if (biomes.length === 0) {
biomes.push("Uncatchable"); biomes.push("Uncatchable");
} }
@ -1530,6 +1659,8 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.cursorObj.setVisible(!filterMode); this.cursorObj.setVisible(!filterMode);
this.filterBar.cursorObj.setVisible(filterMode); this.filterBar.cursorObj.setVisible(filterMode);
this.pokemonSprite.setVisible(false); this.pokemonSprite.setVisible(false);
this.showFormTrayIconElement.setVisible(false);
this.showFormTrayLabel.setVisible(false);
if (filterMode !== this.filterMode) { if (filterMode !== this.filterMode) {
this.filterMode = filterMode; this.filterMode = filterMode;
@ -1546,6 +1677,8 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.cursorObj.setVisible(!filterTextMode); this.cursorObj.setVisible(!filterTextMode);
this.filterText.cursorObj.setVisible(filterTextMode); this.filterText.cursorObj.setVisible(filterTextMode);
this.pokemonSprite.setVisible(false); this.pokemonSprite.setVisible(false);
this.showFormTrayIconElement.setVisible(false);
this.showFormTrayLabel.setVisible(false);
if (filterTextMode !== this.filterTextMode) { if (filterTextMode !== this.filterTextMode) {
this.filterTextMode = filterTextMode; this.filterTextMode = filterTextMode;
@ -1558,6 +1691,101 @@ export default class PokedexUiHandler extends MessageUiHandler {
return false; return false;
} }
openFormTray(species: PokemonSpecies): boolean {
this.trayForms = species.forms;
this.trayNumIcons = this.trayForms.length;
this.trayRows = Math.floor(this.trayNumIcons / 9) + (this.trayNumIcons % 9 === 0 ? 0 : 1);
this.trayColumns = Math.min(this.trayNumIcons, 9);
const maxColumns = 9;
const onScreenFirstIndex = this.scrollCursor * maxColumns;
const boxCursor = this.cursor - onScreenFirstIndex;
const boxCursorY = Math.floor(boxCursor / maxColumns);
const boxCursorX = boxCursor - boxCursorY * 9;
const spaceBelow = 9 - 1 - boxCursorY;
const spaceRight = 9 - boxCursorX;
const boxPos = calcStarterPosition(this.cursor, this.scrollCursor);
const goUp = this.trayRows <= spaceBelow - 1 ? 0 : 1;
const goLeft = this.trayColumns <= spaceRight ? 0 : 1;
this.trayBg.setSize(13 + this.trayColumns * 17, 8 + this.trayRows * 18);
this.formTrayContainer.setX(
(goLeft ? boxPos.x - 18 * (this.trayColumns - spaceRight) : boxPos.x) - 3
);
this.formTrayContainer.setY(
goUp ? boxPos.y - this.trayBg.height : boxPos.y + 17
);
const dexEntry = globalScene.gameData.dexData[species.speciesId];
const dexAttr = this.getCurrentDexProps(species.speciesId);
const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(this.lastSpecies, dexAttr));
this.trayContainers = [];
this.trayForms.map((f, index) => {
const isFormCaught = dexEntry ? (dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f.formIndex ?? 0)) > 0n : false;
const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(f.formIndex ?? 0)) > 0n : false;
const formContainer = new PokedexMonContainer(species, { formIndex: f.formIndex, female: props.female, shiny: props.shiny, variant: props.variant });
this.iconAnimHandler.addOrUpdate(formContainer.icon, PokemonIconAnimMode.NONE);
// Setting tint, for all saves some caught forms may only show up as seen
if (isFormCaught || globalScene.dexForDevs) {
formContainer.icon.clearTint();
} else if (isFormSeen) {
formContainer.icon.setTint(0x808080);
}
formContainer.setPosition(5 + (index % 9) * 18, 4 + Math.floor(index / 9) * 17);
this.formTrayContainer.add(formContainer);
this.trayContainers.push(formContainer);
});
this.showingTray = true;
this.setTrayCursor(0);
this.formTrayContainer.setVisible(true);
this.showFormTrayIconElement.setVisible(false);
this.showFormTrayLabel.setVisible(false);
return true;
}
closeFormTray(): boolean {
this.trayContainers.forEach(obj => {
this.formTrayContainer.remove(obj, true); // Removes from container and destroys it
});
this.trayContainers = [];
this.formTrayContainer.setVisible(false);
this.showingTray = false;
this.setSpeciesDetails(this.lastSpecies);
return true;
}
setTrayCursor(cursor: number): boolean {
if (!this.showingTray) {
return false;
}
cursor = Phaser.Math.Clamp(this.trayContainers.length - 1, cursor, 0);
const changed = this.trayCursor !== cursor;
if (changed) {
this.trayCursor = cursor;
}
this.trayCursorObj.setPosition(5 + (cursor % 9) * 18, 4 + Math.floor(cursor / 9) * 17);
const species = this.lastSpecies;
const formIndex = this.trayForms[cursor].formIndex;
this.setSpeciesDetails(species, { formIndex: formIndex });
return changed;
}
getFriendship(speciesId: number) { getFriendship(speciesId: number) {
let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship; let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship;
if (!currentFriendship || currentFriendship === undefined) { if (!currentFriendship || currentFriendship === undefined) {
@ -1592,13 +1820,13 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.lastSpecies = species!; // TODO: is this bang correct? this.lastSpecies = species!; // TODO: is this bang correct?
if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr)) { if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr || globalScene.dexForDevs)) {
this.pokemonNumberText.setText(i18next.t("pokedexUiHandler:pokemonNumber") + padInt(species.speciesId, 4)); this.pokemonNumberText.setText(i18next.t("pokedexUiHandler:pokemonNumber") + padInt(species.speciesId, 4));
this.pokemonNameText.setText(species.name); this.pokemonNameText.setText(species.name);
if (this.speciesStarterDexEntry?.caughtAttr) { if (this.speciesStarterDexEntry?.caughtAttr || globalScene.dexForDevs) {
// Pause the animation when the species is selected // Pause the animation when the species is selected
const speciesIndex = this.allSpecies.indexOf(species); const speciesIndex = this.allSpecies.indexOf(species);
@ -1627,9 +1855,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.type1Icon.setVisible(true); this.type1Icon.setVisible(true);
this.type2Icon.setVisible(true); this.type2Icon.setVisible(true);
this.setSpeciesDetails(species, { this.setSpeciesDetails(species);
forSeen: true
});
this.pokemonSprite.setTint(0x808080); this.pokemonSprite.setTint(0x808080);
} }
} else { } else {
@ -1646,7 +1872,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void {
let { shiny, formIndex, female, variant } = options; let { shiny, formIndex, female, variant } = options;
const forSeen: boolean = options.forSeen ?? false;
// We will only update the sprite if there is a change to form, shiny/variant // We will only update the sprite if there is a change to form, shiny/variant
// or gender for species with gender sprite differences // or gender for species with gender sprite differences
@ -1667,34 +1892,33 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.assetLoadCancelled = null; this.assetLoadCancelled = null;
} }
this.starterMoveset = null;
this.speciesStarterMoves = [];
if (species) { if (species) {
const dexEntry = globalScene.gameData.dexData[species.speciesId]; const dexEntry = globalScene.gameData.dexData[species.speciesId];
if (!dexEntry.caughtAttr) { if (!dexEntry.caughtAttr) {
const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId))); const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)));
if (shiny === undefined || shiny !== props.shiny) { if (shiny === undefined) {
shiny = props.shiny; shiny = props.shiny;
} }
if (formIndex === undefined || formIndex !== props.formIndex) { if (formIndex === undefined) {
formIndex = props.formIndex; formIndex = props.formIndex;
} }
if (female === undefined || female !== props.female) { if (female === undefined) {
female = props.female; female = props.female;
} }
if (variant === undefined || variant !== props.variant) { if (variant === undefined) {
variant = props.variant; variant = props.variant;
} }
} }
const isFormCaught = dexEntry ? (dexEntry.caughtAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false;
const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false;
const assetLoadCancelled = new BooleanHolder(false); const assetLoadCancelled = new BooleanHolder(false);
this.assetLoadCancelled = assetLoadCancelled; this.assetLoadCancelled = assetLoadCancelled;
if (shouldUpdateSprite) { if (shouldUpdateSprite) {
species.loadAssets(female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct? species.loadAssets(female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct?
if (assetLoadCancelled.value) { if (assetLoadCancelled.value) {
return; return;
@ -1711,21 +1935,37 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.pokemonSprite.setVisible(!(this.filterMode || this.filterTextMode)); this.pokemonSprite.setVisible(!(this.filterMode || this.filterTextMode));
} }
if (dexEntry.caughtAttr || forSeen) { if (isFormCaught || globalScene.dexForDevs) {
this.pokemonSprite.clearTint();
} else if (isFormSeen) {
this.pokemonSprite.setTint(0x808080);
} else {
this.pokemonSprite.setTint(0);
}
if (isFormCaught || isFormSeen || globalScene.dexForDevs) {
const speciesForm = getPokemonSpeciesForm(species.speciesId, 0); // TODO: always selecting the first form const speciesForm = getPokemonSpeciesForm(species.speciesId, 0); // TODO: always selecting the first form
this.setTypeIcons(speciesForm.type1, speciesForm.type2); this.setTypeIcons(speciesForm.type1, speciesForm.type2);
} else { } else {
this.setTypeIcons(null, null); this.setTypeIcons(null, null);
} }
if (species?.forms?.length > 1) {
if (!this.showingTray) {
this.showFormTrayIconElement.setVisible(true);
this.showFormTrayLabel.setVisible(true);
}
this.canShowFormTray = true;
} else {
this.showFormTrayIconElement.setVisible(false);
this.showFormTrayLabel.setVisible(false);
this.canShowFormTray = false;
}
} else { } else {
this.setTypeIcons(null, null); this.setTypeIcons(null, null);
} }
if (!this.starterMoveset) {
this.starterMoveset = this.speciesStarterMoves.slice(0, 4) as StarterMoveset;
}
} }
setTypeIcons(type1: Type | null, type2: Type | null): void { setTypeIcons(type1: Type | null, type2: Type | null): void {
@ -1784,7 +2024,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
ui.showText(i18next.t("pokedexUiHandler:confirmExit"), null, () => { ui.showText(i18next.t("pokedexUiHandler:confirmExit"), null, () => {
ui.setModeWithoutClear(Mode.CONFIRM, () => { ui.setModeWithoutClear(Mode.CONFIRM, () => {
ui.setMode(Mode.POKEDEX, "refresh"); ui.setMode(Mode.POKEDEX, "refresh");
globalScene.clearPhaseQueue();
this.clearText(); this.clearText();
this.clear(); this.clear();
ui.revertMode(); ui.revertMode();

View File

@ -1981,8 +1981,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
female: starterAttributes.female female: starterAttributes.female
}; };
ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, starterAttributes.form, attributes); ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, starterAttributes.form, attributes);
return true;
}); });
return true;
} }
}); });
options.push({ options.push({

View File

@ -96,6 +96,9 @@ export default class SummaryUiHandler extends UiHandler {
private friendshipText: Phaser.GameObjects.Text; private friendshipText: Phaser.GameObjects.Text;
private friendshipIcon: Phaser.GameObjects.Sprite; private friendshipIcon: Phaser.GameObjects.Sprite;
private friendshipOverlay: Phaser.GameObjects.Sprite; private friendshipOverlay: Phaser.GameObjects.Sprite;
private permStatsContainer: Phaser.GameObjects.Container;
private ivContainer: Phaser.GameObjects.Container;
private statsContainer: Phaser.GameObjects.Container;
private descriptionScrollTween: Phaser.Tweens.Tween | null; private descriptionScrollTween: Phaser.Tweens.Tween | null;
private moveCursorBlinkTimer: Phaser.Time.TimerEvent | null; private moveCursorBlinkTimer: Phaser.Time.TimerEvent | null;
@ -535,6 +538,10 @@ export default class SummaryUiHandler extends UiHandler {
this.passiveContainer.nameText?.setVisible(!this.passiveContainer.descriptionText?.visible); this.passiveContainer.nameText?.setVisible(!this.passiveContainer.descriptionText?.visible);
this.passiveContainer.descriptionText?.setVisible(!this.passiveContainer.descriptionText.visible); this.passiveContainer.descriptionText?.setVisible(!this.passiveContainer.descriptionText.visible);
this.passiveContainer.labelImage.setVisible(!this.passiveContainer.labelImage.visible); this.passiveContainer.labelImage.setVisible(!this.passiveContainer.labelImage.visible);
} else if (this.cursor === Page.STATS) {
//Show IVs
this.permStatsContainer.setVisible(!this.permStatsContainer.visible);
this.ivContainer.setVisible(!this.ivContainer.visible);
} }
} else if (button === Button.CANCEL) { } else if (button === Button.CANCEL) {
if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) { if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) {
@ -878,8 +885,13 @@ export default class SummaryUiHandler extends UiHandler {
profileContainer.add(memoText); profileContainer.add(memoText);
break; break;
case Page.STATS: case Page.STATS:
const statsContainer = globalScene.add.container(0, -pageBg.height); this.statsContainer = globalScene.add.container(0, -pageBg.height);
pageContainer.add(statsContainer); pageContainer.add(this.statsContainer);
this.permStatsContainer = globalScene.add.container(27, 56);
this.statsContainer.add(this.permStatsContainer);
this.ivContainer = globalScene.add.container(27, 56);
this.statsContainer.add(this.ivContainer);
this.statsContainer.setVisible(true);
PERMANENT_STATS.forEach((stat, s) => { PERMANENT_STATS.forEach((stat, s) => {
const statName = i18next.t(getStatKey(stat)); const statName = i18next.t(getStatKey(stat));
@ -888,18 +900,27 @@ export default class SummaryUiHandler extends UiHandler {
const natureStatMultiplier = getNatureStatMultiplier(this.pokemon?.getNature()!, s); // TODO: is this bang correct? const natureStatMultiplier = getNatureStatMultiplier(this.pokemon?.getNature()!, s); // TODO: is this bang correct?
const statLabel = addTextObject(27 + 115 * colIndex + (colIndex === 1 ? 5 : 0), 56 + 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE); const statLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE);
const ivLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, this.pokemon?.ivs[stat] === 31 ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY);
statLabel.setOrigin(0.5, 0); statLabel.setOrigin(0.5, 0);
statsContainer.add(statLabel); ivLabel.setOrigin(0.5, 0);
this.permStatsContainer.add(statLabel);
this.ivContainer.add(ivLabel);
const statValueText = stat !== Stat.HP const statValueText = stat !== Stat.HP
? Utils.formatStat(this.pokemon?.getStat(stat)!) // TODO: is this bang correct? ? Utils.formatStat(this.pokemon?.getStat(stat)!) // TODO: is this bang correct?
: `${Utils.formatStat(this.pokemon?.hp!, true)}/${Utils.formatStat(this.pokemon?.getMaxHp()!, true)}`; // TODO: are those bangs correct? : `${Utils.formatStat(this.pokemon?.hp!, true)}/${Utils.formatStat(this.pokemon?.getMaxHp()!, true)}`; // TODO: are those bangs correct?
const ivText = `${this.pokemon?.ivs[stat]}/31`;
const statValue = addTextObject(120 + 88 * colIndex, 56 + 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT); const statValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT);
statValue.setOrigin(1, 0); statValue.setOrigin(1, 0);
statsContainer.add(statValue); this.permStatsContainer.add(statValue);
const ivValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, ivText, TextStyle.WINDOW_ALT);
ivValue.setOrigin(1, 0);
this.ivContainer.add(ivValue);
}); });
this.ivContainer.setVisible(false);
const itemModifiers = (globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier const itemModifiers = (globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& m.pokemonId === this.pokemon?.id, this.playerParty) as PokemonHeldItemModifier[]) && m.pokemonId === this.pokemon?.id, this.playerParty) as PokemonHeldItemModifier[])
@ -909,7 +930,7 @@ export default class SummaryUiHandler extends UiHandler {
const icon = item.getIcon(true); const icon = item.getIcon(true);
icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15); icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15);
statsContainer.add(icon); this.statsContainer.add(icon);
icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains); icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains);
icon.on("pointerover", () => globalScene.ui.showTooltip(item.type.name, item.type.getDescription(), true)); icon.on("pointerover", () => globalScene.ui.showTooltip(item.type.name, item.type.getDescription(), true));
@ -925,26 +946,26 @@ export default class SummaryUiHandler extends UiHandler {
const expLabel = addTextObject(6, 112, i18next.t("pokemonSummary:expPoints"), TextStyle.SUMMARY); const expLabel = addTextObject(6, 112, i18next.t("pokemonSummary:expPoints"), TextStyle.SUMMARY);
expLabel.setOrigin(0, 0); expLabel.setOrigin(0, 0);
statsContainer.add(expLabel); this.statsContainer.add(expLabel);
const nextLvExpLabel = addTextObject(6, 128, i18next.t("pokemonSummary:nextLv"), TextStyle.SUMMARY); const nextLvExpLabel = addTextObject(6, 128, i18next.t("pokemonSummary:nextLv"), TextStyle.SUMMARY);
nextLvExpLabel.setOrigin(0, 0); nextLvExpLabel.setOrigin(0, 0);
statsContainer.add(nextLvExpLabel); this.statsContainer.add(nextLvExpLabel);
const expText = addTextObject(208, 112, pkmExp.toString(), TextStyle.WINDOW_ALT); const expText = addTextObject(208, 112, pkmExp.toString(), TextStyle.WINDOW_ALT);
expText.setOrigin(1, 0); expText.setOrigin(1, 0);
statsContainer.add(expText); this.statsContainer.add(expText);
const nextLvExp = pkmLvl < globalScene.getMaxExpLevel() const nextLvExp = pkmLvl < globalScene.getMaxExpLevel()
? getLevelTotalExp(pkmLvl + 1, pkmSpeciesGrowthRate) - pkmExp ? getLevelTotalExp(pkmLvl + 1, pkmSpeciesGrowthRate) - pkmExp
: 0; : 0;
const nextLvExpText = addTextObject(208, 128, nextLvExp.toString(), TextStyle.WINDOW_ALT); const nextLvExpText = addTextObject(208, 128, nextLvExp.toString(), TextStyle.WINDOW_ALT);
nextLvExpText.setOrigin(1, 0); nextLvExpText.setOrigin(1, 0);
statsContainer.add(nextLvExpText); this.statsContainer.add(nextLvExpText);
const expOverlay = globalScene.add.image(140, 145, "summary_stats_overlay_exp"); const expOverlay = globalScene.add.image(140, 145, "summary_stats_overlay_exp");
expOverlay.setOrigin(0, 0); expOverlay.setOrigin(0, 0);
statsContainer.add(expOverlay); this.statsContainer.add(expOverlay);
const expMaskRect = globalScene.make.graphics({}); const expMaskRect = globalScene.make.graphics({});
expMaskRect.setScale(6); expMaskRect.setScale(6);
@ -955,6 +976,11 @@ export default class SummaryUiHandler extends UiHandler {
const expMask = expMaskRect.createGeometryMask(); const expMask = expMaskRect.createGeometryMask();
expOverlay.setMask(expMask); expOverlay.setMask(expMask);
this.abilityPrompt = globalScene.add.image(0, 0, !globalScene.inputController?.gamepadSupport ? "summary_profile_prompt_z" : "summary_profile_prompt_a");
this.abilityPrompt.setPosition(8, 47);
this.abilityPrompt.setVisible(true);
this.abilityPrompt.setOrigin(0, 0);
this.statsContainer.add(this.abilityPrompt);
break; break;
case Page.MOVES: case Page.MOVES:
this.movesContainer = globalScene.add.container(5, -pageBg.height + 26); this.movesContainer = globalScene.add.container(5, -pageBg.height + 26);