Merge branch 'beta' into damo-balance-1

This commit is contained in:
damocleas 2025-02-11 13:42:44 -05:00 committed by GitHub
commit df3539b20c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2395 additions and 449 deletions

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

View File

@ -1401,8 +1401,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() };
@ -2353,14 +2353,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);
} }
} }
@ -2498,32 +2498,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

@ -105,9 +105,11 @@ export default class Battle {
public lastEnemyInvolved: number; public lastEnemyInvolved: number;
public lastPlayerInvolved: 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[] = [];
@ -118,7 +120,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;
@ -127,7 +129,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

@ -4484,6 +4484,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);
@ -5805,8 +5812,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(),
@ -6313,8 +6323,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

@ -2975,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
@ -3164,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();
@ -5332,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:
@ -8334,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)
@ -8358,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),
@ -8387,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),
@ -8400,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),
@ -8476,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)
@ -8490,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)
@ -8506,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)
@ -8517,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)
@ -8539,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)
@ -8563,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)
@ -8575,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)
@ -8638,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(),
@ -8648,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)
@ -8664,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)
@ -8683,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)
@ -8744,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)
@ -8775,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),
@ -8790,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)) }));
@ -8808,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)
@ -8860,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),
@ -8868,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(),
@ -8881,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)
@ -8932,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)
@ -8953,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)
@ -9041,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),
@ -9070,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()
@ -9093,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)
@ -9102,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),
@ -9146,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()
@ -9192,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()
@ -9203,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),
@ -9212,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)
@ -9255,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()
@ -9318,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),
@ -9364,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)
@ -9383,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),
@ -9412,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(),
@ -9529,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()
@ -9567,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(),
@ -9614,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)
@ -9654,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)
@ -9687,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)
@ -9700,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()
@ -9740,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,
@ -9943,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)
@ -9951,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),
@ -9963,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)
@ -9977,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()
@ -10018,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)
@ -10048,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),
@ -10077,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)
@ -10221,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 ])
@ -10237,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)
@ -10284,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),
@ -10366,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)
@ -10492,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)
@ -10672,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)
@ -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

@ -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

@ -44,6 +44,11 @@ export class Arena {
public bgm: string; public bgm: string;
public ignoreAbilities: boolean; public ignoreAbilities: boolean;
public ignoringEffectSource: BattlerIndex | null; public ignoringEffectSource: BattlerIndex | null;
/**
* 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;
@ -52,12 +57,13 @@ 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.playerFaints = playerFaints;
} }
init() { init() {
@ -688,6 +694,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();
} }
@ -773,6 +780,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";
@ -2903,6 +2936,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;

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
@ -184,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()!) : "" }));
@ -211,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)? */
@ -229,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
@ -238,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
@ -371,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) :
@ -586,12 +639,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;
} }
@ -599,15 +647,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;
} }
@ -623,6 +668,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

@ -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,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

@ -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

@ -38,8 +38,8 @@ describe("Moves - Tera Blast", () => {
.startingHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]) .startingHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }])
.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");
}); });

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({