Merge branch 'beta' into tera-rework
@ -5,6 +5,7 @@ import importX from 'eslint-plugin-import-x';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: "eslint-config",
|
||||
files: ["src/**/*.{ts,tsx,js,jsx}"],
|
||||
ignores: ["dist/*", "build/*", "coverage/*", "public/*", ".github/*", "node_modules/*", ".vscode/*"],
|
||||
languageOptions: {
|
||||
@ -48,5 +49,22 @@ export default [
|
||||
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], // Disallows multiple empty lines
|
||||
"@typescript-eslint/consistent-type-imports": "error", // Enforces type-only imports wherever possible
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "eslint-tests",
|
||||
files: ["src/test/**/**.test.ts"],
|
||||
languageOptions: {
|
||||
parser: parser,
|
||||
parserOptions: {
|
||||
"project": ["./tsconfig.json"]
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-floating-promises": "error", // Require Promise-like statements to be handled appropriately. - https://typescript-eslint.io/rules/no-floating-promises/
|
||||
"@typescript-eslint/no-misused-promises": "error", // Disallow Promises in places not designed to handle them. - https://typescript-eslint.io/rules/no-misused-promises/
|
||||
}
|
||||
}
|
||||
]
|
||||
|
BIN
public/images/events/valentines2025event-de.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/events/valentines2025event-en.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/events/valentines2025event-es-ES.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/events/valentines2025event-fr.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/events/valentines2025event-it.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/events/valentines2025event-ja.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/events/valentines2025event-ko.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/events/valentines2025event-pt-BR.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/events/valentines2025event-zh-CN.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 10 KiB |
@ -1,19 +1,19 @@
|
||||
{ "frames": [
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"frame": { "x": 0, "y": 0, "w": 77, "h": 77 },
|
||||
"frame": { "x": 0, "y": 0, "w": 77, "h": 65 },
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 },
|
||||
"sourceSize": { "w": 77, "h": 77 },
|
||||
"spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 65 },
|
||||
"sourceSize": { "w": 77, "h": 65 },
|
||||
"duration": 100
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"app": "https://www.aseprite.org/",
|
||||
"version": "1.3.7-x64",
|
||||
"version": "1.3.9.2-x64",
|
||||
"format": "I8",
|
||||
"size": { "w": 77, "h": 77 },
|
||||
"size": { "w": 77, "h": 65 },
|
||||
"scale": "1"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 899 B After Width: | Height: | Size: 890 B |
@ -1,11 +1,11 @@
|
||||
{ "frames": [
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"frame": { "x": 0, "y": 0, "w": 77, "h": 77 },
|
||||
"frame": { "x": 0, "y": 0, "w": 77, "h": 65 },
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 },
|
||||
"sourceSize": { "w": 77, "h": 77 },
|
||||
"spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 65 },
|
||||
"sourceSize": { "w": 77, "h": 65 },
|
||||
"duration": 100
|
||||
}
|
||||
],
|
||||
@ -13,7 +13,7 @@
|
||||
"app": "https://www.aseprite.org/",
|
||||
"version": "1.3.7-x64",
|
||||
"format": "I8",
|
||||
"size": { "w": 77, "h": 77 },
|
||||
"size": { "w": 77, "h": 65 },
|
||||
"scale": "1"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 899 B After Width: | Height: | Size: 890 B |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 11 KiB |
@ -1 +1 @@
|
||||
Subproject commit 5f6fa82c17d5981eaec15f105880ac2b4c99cc8d
|
||||
Subproject commit bfcd7f91c39630f155839872c8f66fd0a89e12ac
|
@ -1405,8 +1405,8 @@ export default class BattleScene extends SceneBase {
|
||||
return this.currentBattle;
|
||||
}
|
||||
|
||||
newArena(biome: Biome): Arena {
|
||||
this.arena = new Arena(biome, Biome[biome].toLowerCase());
|
||||
newArena(biome: Biome, playerFaints?: number): Arena {
|
||||
this.arena = new Arena(biome, Biome[biome].toLowerCase(), playerFaints);
|
||||
this.eventTarget.dispatchEvent(new NewArenaEvent());
|
||||
|
||||
this.arenaBg.pipelineData = { terrainColorRatio: this.arena.getBgTerrainColorRatioForBiome() };
|
||||
@ -2357,14 +2357,14 @@ export default class BattleScene extends SceneBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Phase to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
|
||||
* @param phase {@linkcode Phase} the phase to add
|
||||
* Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
|
||||
* @param phases {@linkcode Phase} the phase(s) to add
|
||||
*/
|
||||
unshiftPhase(phase: Phase): void {
|
||||
unshiftPhase(...phases: Phase[]): void {
|
||||
if (this.phaseQueuePrependSpliceIndex === -1) {
|
||||
this.phaseQueuePrepend.push(phase);
|
||||
this.phaseQueuePrepend.push(...phases);
|
||||
} else {
|
||||
this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, phase);
|
||||
this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2502,32 +2502,38 @@ export default class BattleScene extends SceneBase {
|
||||
* @param targetPhase {@linkcode Phase} the type of phase to search for in phaseQueue
|
||||
* @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);
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
this.phaseQueue.splice(targetIndex, 0, phase);
|
||||
this.phaseQueue.splice(targetIndex, 0, ...phase);
|
||||
return true;
|
||||
} else {
|
||||
this.unshiftPhase(phase);
|
||||
this.unshiftPhase(...phase);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to add the input phase to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()}
|
||||
* @param phase {@linkcode Phase} the phase to be added
|
||||
* 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(s) to be added
|
||||
* @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue}
|
||||
* @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);
|
||||
|
||||
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
|
||||
this.phaseQueue.splice(targetIndex + 1, 0, phase);
|
||||
this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
|
||||
return true;
|
||||
} else {
|
||||
this.unshiftPhase(phase);
|
||||
this.unshiftPhase(...phase);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -102,10 +102,15 @@ export default class Battle {
|
||||
public battleSeed: string = Utils.randomString(16, true);
|
||||
private battleSeedState: string | null = null;
|
||||
public moneyScattered: number = 0;
|
||||
/** Primarily for double battles, keeps track of last enemy and player pokemon that triggered its ability or used a move */
|
||||
public lastEnemyInvolved: number;
|
||||
public lastPlayerInvolved: number;
|
||||
public lastUsedPokeball: PokeballType | null = null;
|
||||
/** The number of times a Pokemon on the player's side has fainted this battle */
|
||||
public playerFaints: number = 0;
|
||||
/** The number of times a Pokemon on the enemy's side has fainted this battle */
|
||||
/**
|
||||
* Saves the number of times a Pokemon on the enemy's side has fainted during 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 playerFaintsHistory: FaintLogEntry[] = [];
|
||||
public enemyFaintsHistory: FaintLogEntry[] = [];
|
||||
@ -116,7 +121,7 @@ export default class Battle {
|
||||
|
||||
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.waveIndex = waveIndex;
|
||||
this.battleType = battleType;
|
||||
@ -125,7 +130,7 @@ export default class Battle {
|
||||
this.enemyLevels = battleType !== BattleType.TRAINER
|
||||
? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave())
|
||||
: trainer?.getPartyLevels(this.waveIndex);
|
||||
this.double = double ?? false;
|
||||
this.double = double;
|
||||
}
|
||||
|
||||
private initBattleSpec(): void {
|
||||
|
@ -2744,6 +2744,44 @@ export class PreStatStageChangeAbAttr extends AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflect all {@linkcode BattleStat} reductions caused by other Pokémon's moves and Abilities.
|
||||
* Currently only applies to Mirror Armor.
|
||||
*/
|
||||
export class ReflectStatStageChangeAbAttr extends PreStatStageChangeAbAttr {
|
||||
/** {@linkcode BattleStat} to reflect */
|
||||
private reflectedStat? : BattleStat;
|
||||
|
||||
/**
|
||||
* Apply the {@linkcode ReflectStatStageChangeAbAttr} to an interaction
|
||||
* @param _pokemon The user pokemon
|
||||
* @param _passive N/A
|
||||
* @param simulated `true` if the ability is being simulated by the AI
|
||||
* @param stat the {@linkcode BattleStat} being affected
|
||||
* @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true due to reflection
|
||||
* @param args
|
||||
* @returns true because it reflects any stat being lowered
|
||||
*/
|
||||
applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||
const attacker: Pokemon = args[0];
|
||||
const stages = args[1];
|
||||
this.reflectedStat = stat;
|
||||
if (!simulated) {
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(attacker.getBattlerIndex(), false, [ stat ], stages, true, false, true, null, true));
|
||||
}
|
||||
cancelled.value = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string {
|
||||
return i18next.t("abilityTriggers:protectStat", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
abilityName,
|
||||
statName: this.reflectedStat ? i18next.t(getStatKey(this.reflectedStat)) : i18next.t("battle:stats")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities
|
||||
*/
|
||||
@ -4434,6 +4472,13 @@ export class InfiltratorAbAttr extends AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Magic_Bounce_(ability) | Magic Bounce}.
|
||||
* Allows the source to bounce back {@linkcode MoveFlags.REFLECTABLE | Reflectable}
|
||||
* moves as if the user had used {@linkcode Moves.MAGIC_COAT | Magic Coat}.
|
||||
*/
|
||||
export class ReflectStatusMoveAbAttr extends AbAttr { }
|
||||
|
||||
export class UncopiableAbilityAbAttr extends AbAttr {
|
||||
constructor() {
|
||||
super(false);
|
||||
@ -5755,8 +5800,11 @@ export function initAbilities() {
|
||||
}, Stat.SPD, 1)
|
||||
.attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1),
|
||||
new Ability(Abilities.MAGIC_BOUNCE, 5)
|
||||
.attr(ReflectStatusMoveAbAttr)
|
||||
.ignorable()
|
||||
.unimplemented(),
|
||||
// Interactions with stomping tantrum, instruct, encore, and probably other moves that
|
||||
// rely on move history
|
||||
.edgeCase(),
|
||||
new Ability(Abilities.SAP_SIPPER, 5)
|
||||
.attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1)
|
||||
.ignorable(),
|
||||
@ -6053,8 +6101,8 @@ export function initAbilities() {
|
||||
new Ability(Abilities.PROPELLER_TAIL, 8)
|
||||
.attr(BlockRedirectAbAttr),
|
||||
new Ability(Abilities.MIRROR_ARMOR, 8)
|
||||
.ignorable()
|
||||
.unimplemented(),
|
||||
.attr(ReflectStatStageChangeAbAttr)
|
||||
.ignorable(),
|
||||
/**
|
||||
* Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an
|
||||
* ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case
|
||||
@ -6263,8 +6311,8 @@ export function initAbilities() {
|
||||
new Ability(Abilities.SHARPNESS, 9)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5),
|
||||
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))
|
||||
.partial(), // Counter resets every wave instead of on arena reset
|
||||
.attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5))
|
||||
.partial(), // Should only boost once, on summon
|
||||
new Ability(Abilities.COSTAR, 9)
|
||||
.attr(PostSummonCopyAllyStatsAbAttr),
|
||||
new Ability(Abilities.TOXIC_DEBRIS, 9)
|
||||
|
@ -910,7 +910,7 @@ class StickyWebTag extends ArenaTrapTag {
|
||||
if (!cancelled.value) {
|
||||
globalScene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() }));
|
||||
const stages = new NumberHolder(-1);
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value));
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value, true, false, true, null, false, true));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1752,7 +1752,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
|
||||
super.onAdd(pokemon);
|
||||
|
||||
let highestStat: EffectiveStat;
|
||||
EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s)).reduce((highestValue: number, value: number, i: number) => {
|
||||
EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s, undefined, undefined, undefined, undefined, undefined, undefined, true)).reduce((highestValue: number, value: number, i: number) => {
|
||||
if (value > highestValue) {
|
||||
highestStat = EFFECTIVE_STATS[i];
|
||||
return value;
|
||||
@ -1763,15 +1763,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag {
|
||||
highestStat = highestStat!; // tell TS compiler it's defined!
|
||||
this.stat = highestStat;
|
||||
|
||||
switch (this.stat) {
|
||||
case Stat.SPD:
|
||||
this.multiplier = 1.5;
|
||||
break;
|
||||
default:
|
||||
this.multiplier = 1.3;
|
||||
break;
|
||||
}
|
||||
|
||||
this.multiplier = this.stat === Stat.SPD ? 1.5 : 1.3;
|
||||
globalScene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: i18next.t(getStatKey(highestStat)) }), null, false, null, true);
|
||||
}
|
||||
|
||||
@ -2983,6 +2975,24 @@ export class PsychoShiftTag extends BattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag associated with the move Magic Coat.
|
||||
*/
|
||||
export class MagicCoatTag extends BattlerTag {
|
||||
constructor() {
|
||||
super(BattlerTagType.MAGIC_COAT, BattlerTagLapseType.TURN_END, 1, Moves.MAGIC_COAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the "[PokemonName] shrouded itself with Magic Coat" message when the tag is added.
|
||||
* @param pokemon - The target {@linkcode Pokemon}
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon) {
|
||||
// "{pokemonNameWithAffix} shrouded itself with Magic Coat!"
|
||||
globalScene.queueMessage(i18next.t("battlerTags:magicCoatOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
|
||||
* @param sourceId - The ID of the pokemon adding the tag
|
||||
@ -3172,6 +3182,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||
return new GrudgeTag();
|
||||
case BattlerTagType.PSYCHO_SHIFT:
|
||||
return new PsychoShiftTag();
|
||||
case BattlerTagType.MAGIC_COAT:
|
||||
return new MagicCoatTag();
|
||||
case BattlerTagType.NONE:
|
||||
default:
|
||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||
|
355
src/data/move.ts
@ -125,7 +125,9 @@ export enum MoveFlags {
|
||||
/** Indicates a move is able to bypass its target's Substitute (if the target has one) */
|
||||
IGNORE_SUBSTITUTE = 1 << 17,
|
||||
/** 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;
|
||||
@ -610,6 +612,16 @@ export default class Move implements Localizable {
|
||||
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
|
||||
* @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 {
|
||||
constructor() {
|
||||
super();
|
||||
@ -4559,7 +4634,8 @@ export class TeraMoveCategoryAttr extends VariableMoveCategoryAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const category = (args[0] as Utils.NumberHolder);
|
||||
|
||||
if (user.isTerastallized && user.getEffectiveStat(Stat.ATK, target, move) > user.getEffectiveStat(Stat.SPATK, target, move)) {
|
||||
if (user.isTerastallized && user.getEffectiveStat(Stat.ATK, target, move, true, true, false, false, true) >
|
||||
user.getEffectiveStat(Stat.SPATK, target, move, true, true, false, false, true)) {
|
||||
category.value = MoveCategory.PHYSICAL;
|
||||
return true;
|
||||
}
|
||||
@ -5331,6 +5407,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
case BattlerTagType.INGRAIN:
|
||||
case BattlerTagType.IGNORE_ACCURACY:
|
||||
case BattlerTagType.AQUA_RING:
|
||||
case BattlerTagType.MAGIC_COAT:
|
||||
return 3;
|
||||
case BattlerTagType.PROTECTED:
|
||||
case BattlerTagType.FLYING:
|
||||
@ -8333,7 +8410,8 @@ export function initMoves() {
|
||||
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
|
||||
.ignoresSubstitute()
|
||||
.hidesTarget()
|
||||
.windMove(),
|
||||
.windMove()
|
||||
.reflectable(),
|
||||
new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
|
||||
.chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
||||
@ -8357,7 +8435,8 @@ export function initMoves() {
|
||||
new AttackMove(Moves.ROLLING_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1)
|
||||
.attr(FlinchAttr),
|
||||
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)
|
||||
.attr(FlinchAttr),
|
||||
new AttackMove(Moves.HORN_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1),
|
||||
@ -8386,7 +8465,8 @@ export function initMoves() {
|
||||
.recklessMove(),
|
||||
new StatusMove(Moves.TAIL_WHIP, Type.NORMAL, 100, 30, -1, 0, 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)
|
||||
.attr(StatusEffectAttr, StatusEffect.POISON)
|
||||
.makesContact(false),
|
||||
@ -8399,30 +8479,36 @@ export function initMoves() {
|
||||
.makesContact(false),
|
||||
new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 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)
|
||||
.attr(FlinchAttr)
|
||||
.bitingMove(),
|
||||
new StatusMove(Moves.GROWL, Type.NORMAL, 100, 40, -1, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -1)
|
||||
.soundBased()
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES)
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1)
|
||||
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
|
||||
.soundBased()
|
||||
.hidesTarget(),
|
||||
.hidesTarget()
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.SLEEP)
|
||||
.soundBased(),
|
||||
.soundBased()
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.SUPERSONIC, Type.NORMAL, 55, 20, -1, 0, 1)
|
||||
.attr(ConfuseAttr)
|
||||
.soundBased(),
|
||||
.soundBased()
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1)
|
||||
.attr(FixedDamageAttr, 20),
|
||||
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
|
||||
.condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined)
|
||||
.ignoresSubstitute(),
|
||||
.ignoresSubstitute()
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
@ -8475,7 +8561,8 @@ export function initMoves() {
|
||||
.triageMove(),
|
||||
new StatusMove(Moves.LEECH_SEED, Type.GRASS, 90, 10, -1, 0, 1)
|
||||
.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)
|
||||
.attr(GrowthStatStageChangeAttr),
|
||||
new AttackMove(Moves.RAZOR_LEAF, Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, 0, 1)
|
||||
@ -8489,13 +8576,16 @@ export function initMoves() {
|
||||
.attr(AntiSunlightPowerDecreaseAttr),
|
||||
new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.POISON)
|
||||
.powderMove(),
|
||||
.powderMove()
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.STUN_SPORE, Type.GRASS, 75, 30, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.powderMove(),
|
||||
.powderMove()
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.SLEEP_POWDER, Type.GRASS, 75, 15, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.SLEEP)
|
||||
.powderMove(),
|
||||
.powderMove()
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.PETAL_DANCE, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1)
|
||||
.attr(FrenzyAttr)
|
||||
.attr(MissEffectAttr, frenzyMissFunc)
|
||||
@ -8505,7 +8595,8 @@ export function initMoves() {
|
||||
.target(MoveTarget.RANDOM_NEAR_ENEMY),
|
||||
new StatusMove(Moves.STRING_SHOT, Type.BUG, 95, 40, -1, 0, 1)
|
||||
.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)
|
||||
.attr(FixedDamageAttr, 40),
|
||||
new AttackMove(Moves.FIRE_SPIN, Type.FIRE, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 1)
|
||||
@ -8516,7 +8607,8 @@ export function initMoves() {
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
|
||||
new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.attr(RespectAttackTypeImmunityAttr),
|
||||
.attr(RespectAttackTypeImmunityAttr)
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.attr(ThunderAccuracyAttr)
|
||||
@ -8538,13 +8630,15 @@ export function initMoves() {
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND),
|
||||
new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.TOXIC)
|
||||
.attr(ToxicAccuracyAttr),
|
||||
.attr(ToxicAccuracyAttr)
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.CONFUSION, Type.PSYCHIC, MoveCategory.SPECIAL, 50, 100, 25, 10, 0, 1)
|
||||
.attr(ConfuseAttr),
|
||||
new AttackMove(Moves.PSYCHIC, Type.PSYCHIC, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -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)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true),
|
||||
new SelfStatusMove(Moves.AGILITY, Type.PSYCHIC, -1, 30, -1, 0, 1)
|
||||
@ -8562,7 +8656,8 @@ export function initMoves() {
|
||||
.ignoresSubstitute(),
|
||||
new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -2)
|
||||
.soundBased(),
|
||||
.soundBased()
|
||||
.reflectable(),
|
||||
new SelfStatusMove(Moves.DOUBLE_TEAM, Type.NORMAL, -1, 15, -1, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.EVA ], 1, true),
|
||||
new SelfStatusMove(Moves.RECOVER, Type.NORMAL, -1, 5, -1, 0, 1)
|
||||
@ -8574,9 +8669,11 @@ export function initMoves() {
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.MINIMIZED, true, false)
|
||||
.attr(StatStageChangeAttr, [ Stat.EVA ], 2, true),
|
||||
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)
|
||||
.attr(ConfuseAttr),
|
||||
.attr(ConfuseAttr)
|
||||
.reflectable(),
|
||||
new SelfStatusMove(Moves.WITHDRAW, Type.WATER, -1, 40, -1, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
|
||||
new SelfStatusMove(Moves.DEFENSE_CURL, Type.NORMAL, -1, 40, -1, 0, 1)
|
||||
@ -8637,7 +8734,8 @@ export function initMoves() {
|
||||
new SelfStatusMove(Moves.AMNESIA, Type.PSYCHIC, -1, 20, -1, 0, 1)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 2, true),
|
||||
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)
|
||||
.attr(HealAttr, 0.5)
|
||||
.triageMove(),
|
||||
@ -8647,14 +8745,16 @@ export function initMoves() {
|
||||
.condition(failOnGravityCondition)
|
||||
.recklessMove(),
|
||||
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)
|
||||
.attr(HitHealAttr)
|
||||
.condition(targetSleptOrComatoseCondition)
|
||||
.triageMove(),
|
||||
new StatusMove(Moves.POISON_GAS, Type.POISON, 90, 40, -1, 0, 1)
|
||||
.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)
|
||||
.attr(MultiHitAttr)
|
||||
.makesContact(false)
|
||||
@ -8663,7 +8763,8 @@ export function initMoves() {
|
||||
.attr(HitHealAttr)
|
||||
.triageMove(),
|
||||
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)
|
||||
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
|
||||
.attr(HighCritAttr)
|
||||
@ -8682,9 +8783,11 @@ export function initMoves() {
|
||||
.punchingMove(),
|
||||
new StatusMove(Moves.SPORE, Type.GRASS, 100, 15, -1, 0, 1)
|
||||
.attr(StatusEffectAttr, StatusEffect.SLEEP)
|
||||
.powderMove(),
|
||||
.powderMove()
|
||||
.reflectable(),
|
||||
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)
|
||||
.attr(RandomLevelDamageAttr),
|
||||
new SelfStatusMove(Moves.SPLASH, Type.NORMAL, -1, 40, -1, 0, 1)
|
||||
@ -8743,7 +8846,8 @@ export function initMoves() {
|
||||
.attr(StealHeldItemChanceAttr, 0.3),
|
||||
new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2)
|
||||
.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)
|
||||
.attr(IgnoreAccuracyAttr),
|
||||
new StatusMove(Moves.NIGHTMARE, Type.GHOST, 100, 15, -1, 0, 2)
|
||||
@ -8774,12 +8878,14 @@ export function initMoves() {
|
||||
new StatusMove(Moves.COTTON_SPORE, Type.GRASS, 100, 40, -1, 0, 2)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -2)
|
||||
.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)
|
||||
.attr(LowHpPowerAttr),
|
||||
new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2)
|
||||
.ignoresSubstitute()
|
||||
.attr(ReducePpMoveAttr, 4),
|
||||
.attr(ReducePpMoveAttr, 4)
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2)
|
||||
.attr(StatusEffectAttr, StatusEffect.FREEZE)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
@ -8789,10 +8895,12 @@ export function initMoves() {
|
||||
new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2)
|
||||
.punchingMove(),
|
||||
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 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)
|
||||
.attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => {
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }));
|
||||
@ -8807,13 +8915,15 @@ export function initMoves() {
|
||||
.ballBombMove(),
|
||||
new StatusMove(Moves.SPIKES, Type.GROUND, -1, 20, -1, 0, 2)
|
||||
.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)
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
.ballBombMove(),
|
||||
new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2)
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
|
||||
.ignoresSubstitute(),
|
||||
.ignoresSubstitute()
|
||||
.reflectable(),
|
||||
new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2)
|
||||
.ignoresProtect()
|
||||
.attr(DestinyBondAttr)
|
||||
@ -8859,7 +8969,8 @@ export function initMoves() {
|
||||
.attr(ProtectAttr, BattlerTagType.ENDURING)
|
||||
.condition(failIfLastCondition),
|
||||
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)
|
||||
.partial() // Does not lock the user, also does not increase damage properly
|
||||
.attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL),
|
||||
@ -8867,7 +8978,8 @@ export function initMoves() {
|
||||
.attr(SurviveDamageAttr),
|
||||
new StatusMove(Moves.SWAGGER, Type.NORMAL, 85, 15, -1, 0, 2)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], 2)
|
||||
.attr(ConfuseAttr),
|
||||
.attr(ConfuseAttr)
|
||||
.reflectable(),
|
||||
new SelfStatusMove(Moves.MILK_DRINK, Type.NORMAL, -1, 5, -1, 0, 2)
|
||||
.attr(HealAttr, 0.5)
|
||||
.triageMove(),
|
||||
@ -8880,11 +8992,13 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
|
||||
new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2)
|
||||
.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)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INFATUATED)
|
||||
.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)
|
||||
.attr(BypassSleepAttr)
|
||||
.attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false)
|
||||
@ -8931,7 +9045,8 @@ export function initMoves() {
|
||||
new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
|
||||
.ignoresSubstitute()
|
||||
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target)),
|
||||
.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)
|
||||
.partial(), // No effect implemented
|
||||
new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2)
|
||||
@ -8952,7 +9067,8 @@ export function initMoves() {
|
||||
.attr(RemoveArenaTrapAttr),
|
||||
new StatusMove(Moves.SWEET_SCENT, Type.NORMAL, 100, 20, -1, 0, 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)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1),
|
||||
new AttackMove(Moves.METAL_CLAW, Type.STEEL, MoveCategory.PHYSICAL, 50, 95, 35, 10, 0, 2)
|
||||
@ -9040,12 +9156,15 @@ export function initMoves() {
|
||||
new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3)
|
||||
.ignoresSubstitute()
|
||||
.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)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], 1)
|
||||
.attr(ConfuseAttr),
|
||||
.attr(ConfuseAttr)
|
||||
.reflectable(),
|
||||
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)
|
||||
.attr(SacrificialAttrOnHit)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -2),
|
||||
@ -9069,7 +9188,8 @@ export function initMoves() {
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false),
|
||||
new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3)
|
||||
.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)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND)
|
||||
.ignoresSubstitute()
|
||||
@ -9092,7 +9212,12 @@ export function initMoves() {
|
||||
new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true),
|
||||
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)
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.REVENGE, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 3)
|
||||
@ -9101,7 +9226,8 @@ export function initMoves() {
|
||||
.attr(RemoveScreensAttr),
|
||||
new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3)
|
||||
.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)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
|
||||
.attr(RemoveHeldItemAttr, false),
|
||||
@ -9145,7 +9271,8 @@ export function initMoves() {
|
||||
.ballBombMove(),
|
||||
new StatusMove(Moves.FEATHER_DANCE, Type.FLYING, 100, 15, -1, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -2)
|
||||
.danceMove(),
|
||||
.danceMove()
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.TEETER_DANCE, Type.NORMAL, 100, 20, -1, 0, 3)
|
||||
.attr(ConfuseAttr)
|
||||
.danceMove()
|
||||
@ -9191,7 +9318,8 @@ export function initMoves() {
|
||||
.attr(PartyStatusCureAttr, i18next.t("moveTriggers:soothingAromaWaftedThroughArea"), Abilities.SAP_SIPPER)
|
||||
.target(MoveTarget.PARTY),
|
||||
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)
|
||||
.attr(HighCritAttr)
|
||||
.slicingMove()
|
||||
@ -9202,7 +9330,8 @@ export function initMoves() {
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE),
|
||||
new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3)
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST)
|
||||
.ignoresSubstitute(),
|
||||
.ignoresSubstitute()
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
||||
.makesContact(false),
|
||||
@ -9211,12 +9340,15 @@ export function initMoves() {
|
||||
.windMove(),
|
||||
new StatusMove(Moves.METAL_SOUND, Type.STEEL, 85, 40, -1, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2)
|
||||
.soundBased(),
|
||||
.soundBased()
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.GRASS_WHISTLE, Type.GRASS, 55, 15, -1, 0, 3)
|
||||
.attr(StatusEffectAttr, StatusEffect.SLEEP)
|
||||
.soundBased(),
|
||||
.soundBased()
|
||||
.reflectable(),
|
||||
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)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true),
|
||||
new AttackMove(Moves.WATER_SPOUT, Type.WATER, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 3)
|
||||
@ -9254,7 +9386,8 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
|
||||
new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3)
|
||||
.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)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], 1)
|
||||
.soundBased()
|
||||
@ -9317,7 +9450,8 @@ export function initMoves() {
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4)
|
||||
.attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK)
|
||||
.ignoresSubstitute(),
|
||||
.ignoresSubstitute()
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1)
|
||||
.attr(HealStatusEffectAttr, false, StatusEffect.SLEEP),
|
||||
@ -9363,6 +9497,7 @@ export function initMoves() {
|
||||
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),
|
||||
new StatusMove(Moves.EMBARGO, Type.DARK, 100, 15, -1, 0, 4)
|
||||
.reflectable()
|
||||
.unimplemented(),
|
||||
new AttackMove(Moves.FLING, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4)
|
||||
.makesContact(false)
|
||||
@ -9382,14 +9517,16 @@ export function initMoves() {
|
||||
.attr(LessPPMorePowerAttr),
|
||||
new StatusMove(Moves.HEAL_BLOCK, Type.PSYCHIC, 100, 15, -1, 0, 4)
|
||||
.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)
|
||||
.attr(OpponentHighHpPowerAttr, 120)
|
||||
.makesContact(),
|
||||
new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true),
|
||||
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)
|
||||
.attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true)
|
||||
.target(MoveTarget.USER_SIDE),
|
||||
@ -9411,12 +9548,14 @@ export function initMoves() {
|
||||
new AttackMove(Moves.LAST_RESORT, Type.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4)
|
||||
.attr(LastResortAttr),
|
||||
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)
|
||||
.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)
|
||||
.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)
|
||||
.attr(SwapStatStagesAttr, BATTLE_STATS)
|
||||
.ignoresSubstitute(),
|
||||
@ -9528,7 +9667,8 @@ export function initMoves() {
|
||||
.attr(ClearTerrainAttr)
|
||||
.attr(RemoveScreensAttr, false)
|
||||
.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)
|
||||
.attr(AddArenaTagAttr, ArenaTagType.TRICK_ROOM, 5)
|
||||
.ignoresProtect()
|
||||
@ -9566,10 +9706,12 @@ export function initMoves() {
|
||||
new StatusMove(Moves.CAPTIVATE, Type.NORMAL, 100, 20, -1, 0, 4)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2)
|
||||
.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)
|
||||
.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)
|
||||
.attr(WeightPowerAttr)
|
||||
.makesContact(),
|
||||
@ -9613,7 +9755,8 @@ export function initMoves() {
|
||||
.attr(TrapAttr, BattlerTagType.MAGMA_STORM),
|
||||
new StatusMove(Moves.DARK_VOID, Type.DARK, 80, 10, -1, 0, 4) //Accuracy from Generations 4-6
|
||||
.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)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
|
||||
new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4)
|
||||
@ -9653,7 +9796,8 @@ export function initMoves() {
|
||||
.condition((_user, target, _move) => !(target.species.speciesId === Species.GENGAR && target.getFormKey() === "mega"))
|
||||
.condition((_user, target, _move) => Utils.isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && Utils.isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING)))
|
||||
.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)
|
||||
.ignoresProtect()
|
||||
.target(MoveTarget.BOTH_SIDES)
|
||||
@ -9686,7 +9830,8 @@ export function initMoves() {
|
||||
.attr(ElectroBallPowerAttr)
|
||||
.ballBombMove(),
|
||||
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)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true),
|
||||
new SelfStatusMove(Moves.COIL, Type.POISON, -1, 20, -1, 0, 5)
|
||||
@ -9699,9 +9844,11 @@ export function initMoves() {
|
||||
new AttackMove(Moves.FOUL_PLAY, Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, -1, 0, 5)
|
||||
.attr(TargetAtkUserAtkAttr),
|
||||
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)
|
||||
.attr(AbilityGiveAttr),
|
||||
.attr(AbilityGiveAttr)
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5)
|
||||
.ignoresProtect()
|
||||
.ignoresSubstitute()
|
||||
@ -9739,7 +9886,8 @@ export function initMoves() {
|
||||
new StatusMove(Moves.HEAL_PULSE, Type.PSYCHIC, -1, 10, -1, 0, 5)
|
||||
.attr(HealAttr, 0.5, false, false)
|
||||
.pulseMove()
|
||||
.triageMove(),
|
||||
.triageMove()
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.HEX, Type.GHOST, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 5)
|
||||
.attr(
|
||||
MovePowerMultiplierAttr,
|
||||
@ -9942,7 +10090,8 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }),
|
||||
new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6)
|
||||
.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)
|
||||
.attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ),
|
||||
new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
||||
@ -9950,10 +10099,12 @@ export function initMoves() {
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN)
|
||||
.ignoresProtect(),
|
||||
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)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1)
|
||||
.soundBased(),
|
||||
.soundBased()
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6)
|
||||
.attr(AddArenaTagAttr, ArenaTagType.ION_DELUGE)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
@ -9962,7 +10113,8 @@ export function initMoves() {
|
||||
.target(MoveTarget.ALL_NEAR_OTHERS)
|
||||
.triageMove(),
|
||||
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)
|
||||
.windMove()
|
||||
.makesContact(false)
|
||||
@ -9976,9 +10128,11 @@ export function initMoves() {
|
||||
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(ForceSwitchOutAttr, true)
|
||||
.soundBased(),
|
||||
.soundBased()
|
||||
.reflectable(),
|
||||
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)
|
||||
.attr(HitHealAttr, 0.75)
|
||||
.makesContact()
|
||||
@ -10017,10 +10171,12 @@ export function initMoves() {
|
||||
.condition(failIfLastCondition),
|
||||
new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -1)
|
||||
.ignoresSubstitute(),
|
||||
.ignoresSubstitute()
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
|
||||
.soundBased(),
|
||||
.soundBased()
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true })
|
||||
.makesContact(false)
|
||||
@ -10047,14 +10203,17 @@ export function initMoves() {
|
||||
.condition(failIfSingleBattle)
|
||||
.target(MoveTarget.NEAR_ALLY),
|
||||
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)
|
||||
.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)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.POWDER, false, true)
|
||||
.ignoresSubstitute()
|
||||
.powderMove(),
|
||||
.powderMove()
|
||||
.reflectable(),
|
||||
new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6)
|
||||
.chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" }))
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true),
|
||||
@ -10076,7 +10235,8 @@ export function initMoves() {
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.NEAR_ALLY),
|
||||
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)
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
|
||||
new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6)
|
||||
@ -10220,13 +10380,15 @@ export function initMoves() {
|
||||
.punchingMove(),
|
||||
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)
|
||||
.triageMove(),
|
||||
.triageMove()
|
||||
.reflectable(),
|
||||
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)
|
||||
.attr(HitHealAttr, null, Stat.ATK)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], -1)
|
||||
.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)
|
||||
.chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" }))
|
||||
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ])
|
||||
@ -10236,10 +10398,12 @@ export function initMoves() {
|
||||
.makesContact(false),
|
||||
new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false)
|
||||
.condition(failIfSingleBattle),
|
||||
.condition(failIfSingleBattle)
|
||||
.reflectable(),
|
||||
new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7)
|
||||
.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)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
|
||||
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
|
||||
@ -10283,7 +10447,8 @@ export function initMoves() {
|
||||
(user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct?
|
||||
.attr(HealAttr, 0.5)
|
||||
.attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
|
||||
.triageMove(),
|
||||
.triageMove()
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7)
|
||||
.danceMove()
|
||||
.attr(MatchUserTypeAttr),
|
||||
@ -10365,14 +10530,15 @@ export function initMoves() {
|
||||
new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7)
|
||||
.attr(RechargeAttr),
|
||||
new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7)
|
||||
.ignoresSubstitute()
|
||||
.partial(), // Does not steal stats
|
||||
.attr(SpectralThiefAttr)
|
||||
.ignoresSubstitute(),
|
||||
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
|
||||
.ignoresAbilities(),
|
||||
new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
|
||||
.ignoresAbilities(),
|
||||
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)
|
||||
.attr(FlinchAttr),
|
||||
new AttackMove(Moves.NATURES_MADNESS, Type.FAIRY, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 7)
|
||||
@ -10491,10 +10657,12 @@ export function initMoves() {
|
||||
.condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat
|
||||
new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8)
|
||||
.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)
|
||||
.attr(ChangeTypeAttr, Type.PSYCHIC)
|
||||
.powderMove(),
|
||||
.powderMove()
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.DRAGON_DARTS, Type.DRAGON, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 8)
|
||||
.attr(MultiHitAttr, MultiHitType._2)
|
||||
.makesContact(false)
|
||||
@ -10671,6 +10839,7 @@ export function initMoves() {
|
||||
.makesContact(false),
|
||||
new StatusMove(Moves.CORROSIVE_GAS, Type.POISON, 100, 40, -1, 0, 8)
|
||||
.target(MoveTarget.ALL_NEAR_OTHERS)
|
||||
.reflectable()
|
||||
.unimplemented(),
|
||||
new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1)
|
||||
@ -10905,8 +11074,7 @@ export function initMoves() {
|
||||
.attr(TeraMoveCategoryAttr)
|
||||
.attr(TeraBlastTypeAttr)
|
||||
.attr(TeraBlastPowerAttr)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized && user.isOfType(Type.STELLAR) })
|
||||
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized && user.isOfType(Type.STELLAR) }),
|
||||
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
|
||||
.attr(ProtectAttr, BattlerTagType.SILK_TRAP)
|
||||
.condition(failIfLastCondition),
|
||||
@ -10916,8 +11084,7 @@ export function initMoves() {
|
||||
.attr(ConfuseAttr)
|
||||
.recklessMove(),
|
||||
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.currentBattle.playerFaints : globalScene.currentBattle.enemyFaints, 100))
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 100))
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
|
||||
|
@ -41,8 +41,6 @@ export const FunAndGamesEncounter: MysteryEncounter =
|
||||
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
||||
.withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion to play
|
||||
.withAutoHideIntroVisuals(false)
|
||||
// Allows using move without a visible enemy pokemon
|
||||
.withBattleAnimationsWithoutTargets(true)
|
||||
// The Wobbuffet won't use moves
|
||||
.withSkipEnemyBattleTurns(true)
|
||||
// Will skip COMMAND selection menu and go straight to FIGHT (move select) menu
|
||||
|
@ -897,16 +897,21 @@ export function getRandomEncounterSpecies(level: number, isBoss: boolean = false
|
||||
let bossSpecies: PokemonSpecies;
|
||||
let isEventEncounter = false;
|
||||
const eventEncounters = globalScene.eventManager.getEventEncounters();
|
||||
let formIndex;
|
||||
|
||||
if (eventEncounters.length > 0 && randSeedInt(2) === 1) {
|
||||
const eventEncounter = randSeedItem(eventEncounters);
|
||||
const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, !eventEncounter.blockEvolution, isBoss, globalScene.gameMode);
|
||||
isEventEncounter = true;
|
||||
bossSpecies = getPokemonSpecies(levelSpecies);
|
||||
formIndex = eventEncounter.formIndex;
|
||||
} else {
|
||||
bossSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, level, 0, getPartyLuckValue(globalScene.getPlayerParty()), isBoss);
|
||||
}
|
||||
const ret = new EnemyPokemon(bossSpecies, level, TrainerSlot.NONE, isBoss);
|
||||
if (formIndex) {
|
||||
ret.formIndex = formIndex;
|
||||
}
|
||||
|
||||
//Reroll shiny for event encounters
|
||||
if (isEventEncounter && !ret.shiny) {
|
||||
|
@ -94,4 +94,5 @@ export enum BattlerTagType {
|
||||
PSYCHO_SHIFT = "PSYCHO_SHIFT",
|
||||
ENDURE_TOKEN = "ENDURE_TOKEN",
|
||||
POWDER = "POWDER",
|
||||
MAGIC_COAT = "MAGIC_COAT",
|
||||
}
|
||||
|
@ -45,6 +45,11 @@ export class Arena {
|
||||
public ignoreAbilities: boolean;
|
||||
public ignoringEffectSource: BattlerIndex | null;
|
||||
public playerTerasUsed: number;
|
||||
/**
|
||||
* Saves the number of times a party pokemon faints during a arena encounter.
|
||||
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
|
||||
*/
|
||||
public playerFaints: number;
|
||||
|
||||
private lastTimeOfDay: TimeOfDay;
|
||||
|
||||
@ -53,13 +58,14 @@ export class Arena {
|
||||
|
||||
public readonly eventTarget: EventTarget = new EventTarget();
|
||||
|
||||
constructor(biome: Biome, bgm: string) {
|
||||
constructor(biome: Biome, bgm: string, playerFaints: number = 0) {
|
||||
this.biomeType = biome;
|
||||
this.tags = [];
|
||||
this.bgm = bgm;
|
||||
this.trainerPool = biomeTrainerPools[biome];
|
||||
this.updatePoolsForTimeOfDay();
|
||||
this.playerTerasUsed = 0;
|
||||
this.playerFaints = playerFaints;
|
||||
}
|
||||
|
||||
init() {
|
||||
@ -690,6 +696,7 @@ export class Arena {
|
||||
this.trySetWeather(WeatherType.NONE, false);
|
||||
}
|
||||
this.trySetTerrain(TerrainType.NONE, false, true);
|
||||
this.resetPlayerFaintCount();
|
||||
this.removeAllTags();
|
||||
}
|
||||
|
||||
@ -775,6 +782,10 @@ export class Arena {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
resetPlayerFaintCount(): void {
|
||||
this.playerFaints = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function getBiomeKey(biome: Biome): string {
|
||||
|
@ -7,7 +7,40 @@ import { variantColorCache } from "#app/data/variant";
|
||||
import { variantData } from "#app/data/variant";
|
||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
|
||||
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 { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
|
||||
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
|
||||
@ -962,11 +995,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation.
|
||||
* @param isCritical determines whether a critical hit has occurred or not (`false` by default)
|
||||
* @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering
|
||||
* @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false`
|
||||
* @returns the final in-battle value of a stat
|
||||
*/
|
||||
getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number {
|
||||
getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true, ignoreHeldItems: boolean = false): number {
|
||||
const statValue = new Utils.NumberHolder(this.getStat(stat, false));
|
||||
globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue);
|
||||
if (!ignoreHeldItems) {
|
||||
globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue);
|
||||
}
|
||||
|
||||
// The Ruin abilities here are never ignored, but they reveal themselves on summon anyway
|
||||
const fieldApplied = new Utils.BooleanHolder(false);
|
||||
@ -980,7 +1016,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue, simulated);
|
||||
}
|
||||
|
||||
let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated);
|
||||
let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated, ignoreHeldItems);
|
||||
|
||||
switch (stat) {
|
||||
case Stat.ATK:
|
||||
@ -1078,6 +1114,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats);
|
||||
if (this.isFusion()) {
|
||||
const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats;
|
||||
applyChallenges(globalScene.gameMode, ChallengeType.FLIP_STAT, this, fusionBaseStats);
|
||||
|
||||
for (const s of PERMANENT_STATS) {
|
||||
baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2);
|
||||
}
|
||||
@ -2519,9 +2557,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default)
|
||||
* @param isCritical determines whether a critical hit has occurred or not (`false` by default)
|
||||
* @param simulated determines whether effects are applied without altering game state (`true` by default)
|
||||
* @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false`
|
||||
* @return the stat stage multiplier to be used for effective stat calculation
|
||||
*/
|
||||
getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number {
|
||||
getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true, ignoreHeldItems: boolean = false): number {
|
||||
const statStage = new Utils.IntegerHolder(this.getStatStage(stat));
|
||||
const ignoreStatStage = new Utils.BooleanHolder(false);
|
||||
|
||||
@ -2548,7 +2587,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
if (!ignoreStatStage.value) {
|
||||
const statStageMultiplier = new Utils.NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value));
|
||||
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier);
|
||||
if (!ignoreHeldItems) {
|
||||
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier);
|
||||
}
|
||||
return Math.min(statStageMultiplier.value, 4);
|
||||
}
|
||||
return 1;
|
||||
@ -2937,6 +2978,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
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 typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag;
|
||||
@ -4408,8 +4455,12 @@ export class PlayerPokemon extends Pokemon {
|
||||
].filter(d => !!d);
|
||||
const amount = new Utils.NumberHolder(friendship);
|
||||
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
|
||||
const candyFriendshipMultiplier = globalScene.eventManager.getClassicFriendshipMultiplier();
|
||||
const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * (globalScene.gameMode.isClassic ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1)));
|
||||
const candyFriendshipMultiplier = globalScene.gameMode.isClassic ? globalScene.eventManager.getClassicFriendshipMultiplier() : 1;
|
||||
const fusionReduction = fusionStarterSpeciesId
|
||||
? globalScene.eventManager.areFusionsBoosted() ? 1.5 // Divide candy gain for fusions by 1.5 during events
|
||||
: 2 // 2 for fusions outside events
|
||||
: 1; // 1 for non-fused mons
|
||||
const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * candyFriendshipMultiplier / fusionReduction));
|
||||
|
||||
// Add friendship to this PlayerPokemon
|
||||
this.friendship = Math.min(this.friendship + amount.value, 255);
|
||||
|
@ -250,9 +250,9 @@ export class LoadingScene extends SceneBase {
|
||||
}
|
||||
const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ];
|
||||
if (lang && availableLangs.includes(lang)) {
|
||||
this.loadImage("yearofthesnakeevent-" + lang, "events");
|
||||
this.loadImage("valentines2025event-" + lang, "events");
|
||||
} else {
|
||||
this.loadImage("yearofthesnakeevent-en", "events");
|
||||
this.loadImage("valentines2025event-en", "events");
|
||||
}
|
||||
|
||||
this.loadAtlas("statuses", "");
|
||||
|
@ -1735,7 +1735,16 @@ const modifierPool: ModifierPool = {
|
||||
}, 4),
|
||||
new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3),
|
||||
new WeightedModifierType(modifierTypes.TERA_SHARD, (party: Pokemon[]) => party.filter(p => !(p.hasSpecies(Species.TERAPAGOS) || p.hasSpecies(Species.OGERPON) || p.hasSpecies(Species.SHEDINJA))).length > 0 ? 1 : 0),
|
||||
new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 4 : 0),
|
||||
new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => {
|
||||
if (party.filter(p => !p.fusionSpecies).length > 1) {
|
||||
if (globalScene.gameMode.isSplicedOnly) {
|
||||
return 4;
|
||||
} else if (globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}, 4),
|
||||
new WeightedModifierType(modifierTypes.VOUCHER, (_party: Pokemon[], rerollCount: number) => !globalScene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1),
|
||||
].map(m => {
|
||||
m.setTier(ModifierTier.GREAT); return m;
|
||||
@ -1894,7 +1903,7 @@ const modifierPool: ModifierPool = {
|
||||
new WeightedModifierType(modifierTypes.MULTI_LENS, 18),
|
||||
new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (_party: Pokemon[], rerollCount: number) =>
|
||||
!globalScene.gameMode.isDaily && !globalScene.gameMode.isEndless && !globalScene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5),
|
||||
new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24),
|
||||
new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !(globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) && !globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24),
|
||||
new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, () => (globalScene.gameMode.isDaily || (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE))) ? 1 : 0, 1),
|
||||
].map(m => {
|
||||
m.setTier(ModifierTier.MASTER); return m;
|
||||
@ -2553,7 +2562,7 @@ export function getPartyLuckValue(party: Pokemon[]): number {
|
||||
return DailyLuck.value;
|
||||
}
|
||||
const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies();
|
||||
const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0)
|
||||
const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 3 : 0) : 0)
|
||||
.reduce((total: number, value: number) => total += value, 0), 0, 14);
|
||||
return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14);
|
||||
}
|
||||
|
@ -96,10 +96,9 @@ export class FaintPhase extends PokemonPhase {
|
||||
doFaint(): void {
|
||||
const pokemon = this.getPokemon();
|
||||
|
||||
|
||||
// Track total times pokemon have been KO'd for supreme overlord/last respects
|
||||
// Track total times pokemon have been KO'd for Last Respects/Supreme Overlord
|
||||
if (pokemon.isPlayer()) {
|
||||
globalScene.currentBattle.playerFaints += 1;
|
||||
globalScene.arena.playerFaints += 1;
|
||||
globalScene.currentBattle.playerFaintsHistory.push({ pokemon: pokemon, turn: globalScene.currentBattle.turn });
|
||||
} else {
|
||||
globalScene.currentBattle.enemyFaints += 1;
|
||||
|
@ -249,7 +249,8 @@ export class GameOverPhase extends BattlePhase {
|
||||
timestamp: new Date().getTime(),
|
||||
challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)),
|
||||
mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1,
|
||||
mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData
|
||||
mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData,
|
||||
playerFaints: globalScene.arena.playerFaints
|
||||
} as SessionSaveData;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
PostAttackAbAttr,
|
||||
PostDamageAbAttr,
|
||||
PostDefendAbAttr,
|
||||
ReflectStatusMoveAbAttr,
|
||||
TypeImmunityAbAttr,
|
||||
} from "#app/data/ability";
|
||||
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
||||
@ -31,6 +32,7 @@ import {
|
||||
AttackMove,
|
||||
DelayedAttackAttr,
|
||||
FlinchAttr,
|
||||
getMoveTargets,
|
||||
HitsTagAttr,
|
||||
MissEffectAttr,
|
||||
MoveCategory,
|
||||
@ -47,7 +49,7 @@ import {
|
||||
} from "#app/data/move";
|
||||
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
||||
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 { HitResult, MoveResult } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
@ -60,17 +62,27 @@ import {
|
||||
} from "#app/modifier/modifier";
|
||||
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||
import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils";
|
||||
import { type nil } from "#app/utils";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import type { Moves } from "#enums/moves";
|
||||
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 {
|
||||
public move: PokemonMove;
|
||||
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);
|
||||
this.move = move;
|
||||
this.reflected = reflected;
|
||||
/**
|
||||
* 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
|
||||
@ -95,6 +107,13 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return super.end();
|
||||
}
|
||||
|
||||
/** If an enemy used this move, set this as last enemy that used move or ability */
|
||||
if (!user.isPlayer()) {
|
||||
globalScene.currentBattle.lastEnemyInvolved = this.fieldIndex;
|
||||
} else {
|
||||
globalScene.currentBattle.lastPlayerInvolved = this.fieldIndex;
|
||||
}
|
||||
|
||||
const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr);
|
||||
/** If the user was somehow removed from the field and it's not a delayed attack, end this phase */
|
||||
if (!user.isOnField()) {
|
||||
@ -177,12 +196,14 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||
&& !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
|
||||
* 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();
|
||||
if (hasActiveTargets) {
|
||||
globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
|
||||
@ -204,12 +225,21 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => {
|
||||
/** Has the move successfully hit a target (for damage) yet? */
|
||||
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 */
|
||||
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
|
||||
@ -222,7 +252,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
|
||||
const isProtected = (
|
||||
const isProtected = !([ MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.move.getMove().moveTarget)) && (
|
||||
bypassIgnoreProtect.value
|
||||
|| !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
|
||||
&& (hasConditionalProtectApplied.value
|
||||
@ -231,13 +261,39 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|| (this.move.getMove().category !== MoveCategory.STATUS
|
||||
&& 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? */
|
||||
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
|
||||
&& (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
|
||||
@ -364,6 +420,10 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
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
|
||||
const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
|
||||
@ -585,12 +645,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
}
|
||||
|
||||
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) {
|
||||
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) {
|
||||
if (this.checkBypassAccAndInvuln(target)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -598,15 +653,12 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
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;
|
||||
}
|
||||
|
||||
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
|
||||
if (semiInvulnerableTag
|
||||
&& !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)
|
||||
&& !(this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON))
|
||||
) {
|
||||
if (semiInvulnerableTag && !this.checkBypassSemiInvuln(semiInvulnerableTag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -622,6 +674,52 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
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 */
|
||||
public getUserPokemon(): Pokemon | null {
|
||||
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
|
||||
|
@ -58,6 +58,7 @@ export class MovePhase extends BattlePhase {
|
||||
protected ignorePp: boolean;
|
||||
protected failed: boolean = false;
|
||||
protected cancelled: boolean = false;
|
||||
protected reflected: boolean = false;
|
||||
|
||||
public get pokemon(): 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.
|
||||
* @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();
|
||||
|
||||
this.pokemon = pokemon;
|
||||
@ -95,6 +98,7 @@ export class MovePhase extends BattlePhase {
|
||||
this.move = move;
|
||||
this.followUp = followUp;
|
||||
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.
|
||||
if (!this.followUp) {
|
||||
if (!this.followUp || this.reflected) {
|
||||
if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) {
|
||||
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
||||
}
|
||||
@ -335,7 +339,7 @@ export class MovePhase extends BattlePhase {
|
||||
*/
|
||||
if (success) {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
globalScene.queueMessage(i18next.t("battle:useMove", {
|
||||
globalScene.queueMessage(i18next.t(this.reflected ? "battle:magicCoatActivated" : "battle:useMove", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
|
||||
moveName: this.move.getName()
|
||||
}), 500);
|
||||
|
@ -17,6 +17,14 @@ export class ShowAbilityPhase extends PokemonPhase {
|
||||
const pokemon = this.getPokemon();
|
||||
|
||||
if (pokemon) {
|
||||
|
||||
if (!pokemon.isPlayer()) {
|
||||
/** If its an enemy pokemon, list it as last enemy to use ability or move */
|
||||
globalScene.currentBattle.lastEnemyInvolved = pokemon.getBattlerIndex() % 2;
|
||||
} else {
|
||||
globalScene.currentBattle.lastPlayerInvolved = pokemon.getBattlerIndex() % 2;
|
||||
}
|
||||
|
||||
globalScene.abilityBar.showAbility(pokemon, this.passive);
|
||||
|
||||
if (pokemon?.battleData) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { BattlerIndex } from "#app/battle";
|
||||
import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability";
|
||||
import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, ReflectStatStageChangeAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability";
|
||||
import { ArenaTagSide, MistTag } from "#app/data/arena-tag";
|
||||
import type { ArenaTag } from "#app/data/arena-tag";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { ResetNegativeStatStageModifier } from "#app/modifier/modifier";
|
||||
@ -10,6 +11,8 @@ import { NumberHolder, BooleanHolder } from "#app/utils";
|
||||
import i18next from "i18next";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
|
||||
import { OctolockTag } from "#app/data/battler-tags";
|
||||
import { ArenaTagType } from "#app/enums/arena-tag-type";
|
||||
|
||||
export type StatStageChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void;
|
||||
|
||||
@ -21,9 +24,11 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
private ignoreAbilities: boolean;
|
||||
private canBeCopied: boolean;
|
||||
private onChange: StatStageChangeCallback | null;
|
||||
private comingFromMirrorArmorUser: boolean;
|
||||
private comingFromStickyWeb: boolean;
|
||||
|
||||
|
||||
constructor(battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: number, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null) {
|
||||
constructor(battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: number, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null, comingFromMirrorArmorUser: boolean = false, comingFromStickyWeb: boolean = false) {
|
||||
super(battlerIndex);
|
||||
|
||||
this.selfTarget = selfTarget;
|
||||
@ -33,6 +38,8 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
this.ignoreAbilities = ignoreAbilities;
|
||||
this.canBeCopied = canBeCopied;
|
||||
this.onChange = onChange;
|
||||
this.comingFromMirrorArmorUser = comingFromMirrorArmorUser;
|
||||
this.comingFromStickyWeb = comingFromStickyWeb;
|
||||
}
|
||||
|
||||
start() {
|
||||
@ -41,12 +48,44 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
if (this.stats.length > 1) {
|
||||
for (let i = 0; i < this.stats.length; i++) {
|
||||
const stat = [ this.stats[i] ];
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(this.battlerIndex, this.selfTarget, stat, this.stages, this.showMessage, this.ignoreAbilities, this.canBeCopied, this.onChange));
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(this.battlerIndex, this.selfTarget, stat, this.stages, this.showMessage, this.ignoreAbilities, this.canBeCopied, this.onChange, this.comingFromMirrorArmorUser));
|
||||
}
|
||||
return this.end();
|
||||
}
|
||||
|
||||
const pokemon = this.getPokemon();
|
||||
let opponentPokemon: Pokemon | undefined;
|
||||
|
||||
/** Gets the position of last enemy or player pokemon that used ability or move, primarily for double battles involving Mirror Armor */
|
||||
if (pokemon.isPlayer()) {
|
||||
/** If this SSCP is not from sticky web, then we find the opponent pokemon that last did something */
|
||||
if (!this.comingFromStickyWeb) {
|
||||
opponentPokemon = globalScene.getEnemyField()[globalScene.currentBattle.lastEnemyInvolved];
|
||||
} else {
|
||||
/** If this SSCP is from sticky web, then check if pokemon that last sucessfully used sticky web is on field */
|
||||
const stickyTagID = globalScene.arena.findTagsOnSide(
|
||||
(t: ArenaTag) => t.tagType === ArenaTagType.STICKY_WEB,
|
||||
ArenaTagSide.PLAYER)[0].sourceId;
|
||||
globalScene.getEnemyField().forEach((e) => {
|
||||
if (e.id === stickyTagID) {
|
||||
opponentPokemon = e;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!this.comingFromStickyWeb) {
|
||||
opponentPokemon = globalScene.getPlayerField()[globalScene.currentBattle.lastPlayerInvolved];
|
||||
} else {
|
||||
const stickyTagID = globalScene.arena.findTagsOnSide(
|
||||
(t: ArenaTag) => t.tagType === ArenaTagType.STICKY_WEB,
|
||||
ArenaTagSide.ENEMY)[0].sourceId;
|
||||
globalScene.getPlayerField().forEach((e) => {
|
||||
if (e.id === stickyTagID) {
|
||||
opponentPokemon = e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!pokemon.isActive(true)) {
|
||||
return this.end();
|
||||
@ -70,6 +109,11 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
|
||||
if (!cancelled.value && !this.selfTarget && stages.value < 0) {
|
||||
applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate);
|
||||
|
||||
/** Potential stat reflection due to Mirror Armor, does not apply to Octolock end of turn effect */
|
||||
if (opponentPokemon !== undefined && !pokemon.findTag(t => t instanceof OctolockTag) && !this.comingFromMirrorArmorUser) {
|
||||
applyPreStatStageChangeAbAttrs(ReflectStatStageChangeAbAttr, pokemon, stat, cancelled, simulate, opponentPokemon, this.stages);
|
||||
}
|
||||
}
|
||||
|
||||
// If one stat stage decrease is cancelled, simulate the rest of the applications
|
||||
|
@ -141,6 +141,10 @@ export interface SessionSaveData {
|
||||
challenges: ChallengeData[];
|
||||
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
||||
mysteryEncounterSaveData: MysteryEncounterSaveData;
|
||||
/**
|
||||
* Counts the amount of pokemon fainted in your party during the current arena encounter.
|
||||
*/
|
||||
playerFaints: number;
|
||||
}
|
||||
|
||||
interface Unlocks {
|
||||
@ -964,7 +968,8 @@ export class GameData {
|
||||
timestamp: new Date().getTime(),
|
||||
challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)),
|
||||
mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1,
|
||||
mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData
|
||||
mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData,
|
||||
playerFaints: globalScene.arena.playerFaints
|
||||
} as SessionSaveData;
|
||||
}
|
||||
|
||||
@ -1056,7 +1061,7 @@ export class GameData {
|
||||
|
||||
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData);
|
||||
|
||||
globalScene.newArena(sessionData.arena.biome);
|
||||
globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints);
|
||||
|
||||
const battleType = sessionData.battleType || 0;
|
||||
const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null;
|
||||
|
351
src/test/abilities/magic_bounce.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
315
src/test/abilities/mirror_armor.test.ts
Normal file
@ -0,0 +1,315 @@
|
||||
import { Stat } from "#enums/stat";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
|
||||
// TODO: When Magic Bounce is implemented, make a test for its interaction with mirror guard, use screech
|
||||
|
||||
describe("Ability - Mirror Armor", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override.battleType("single")
|
||||
.enemySpecies(Species.RATTATA)
|
||||
.enemyMoveset([ Moves.SPLASH, Moves.STICKY_WEB, Moves.TICKLE, Moves.OCTOLOCK ])
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.startingLevel(2000)
|
||||
.moveset([ Moves.SPLASH, Moves.STICKY_WEB, Moves.TICKLE, Moves.OCTOLOCK ])
|
||||
.ability(Abilities.BALL_FETCH);
|
||||
});
|
||||
|
||||
it("Player side + single battle Intimidate - opponent loses stats", async () => {
|
||||
game.override.ability(Abilities.MIRROR_ARMOR);
|
||||
game.override.enemyAbility(Abilities.INTIMIDATE);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// Enemy has intimidate, enemy should lose -1 atk
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
expect(userPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
it("Enemy side + single battle Intimidate - player loses stats", async () => {
|
||||
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
|
||||
game.override.ability(Abilities.INTIMIDATE);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// Enemy has intimidate, enemy should lose -1 atk
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(userPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
it("Player side + double battle Intimidate - opponents each lose -2 atk", async () => {
|
||||
game.override.battleType("double");
|
||||
game.override.ability(Abilities.MIRROR_ARMOR);
|
||||
game.override.enemyAbility(Abilities.INTIMIDATE);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
|
||||
|
||||
const [ enemy1, enemy2 ] = game.scene.getEnemyField();
|
||||
const [ player1, player2 ] = game.scene.getPlayerField();
|
||||
|
||||
// Enemy has intimidate, enemy should lose -2 atk each
|
||||
game.move.select(Moves.SPLASH);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy1.getStatStage(Stat.ATK)).toBe(-2);
|
||||
expect(enemy2.getStatStage(Stat.ATK)).toBe(-2);
|
||||
expect(player1.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(player2.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
it("Enemy side + double battle Intimidate - players each lose -2 atk", async () => {
|
||||
game.override.battleType("double");
|
||||
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
|
||||
game.override.ability(Abilities.INTIMIDATE);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
|
||||
|
||||
const [ enemy1, enemy2 ] = game.scene.getEnemyField();
|
||||
const [ player1, player2 ] = game.scene.getPlayerField();
|
||||
|
||||
// Enemy has intimidate, enemy should lose -1 atk
|
||||
game.move.select(Moves.SPLASH);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy1.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(enemy2.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(player1.getStatStage(Stat.ATK)).toBe(-2);
|
||||
expect(player2.getStatStage(Stat.ATK)).toBe(-2);
|
||||
});
|
||||
|
||||
it("Player side + single battle Intimidate + Tickle - opponent loses stats", async () => {
|
||||
game.override.ability(Abilities.MIRROR_ARMOR);
|
||||
game.override.enemyAbility(Abilities.INTIMIDATE);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// Enemy has intimidate and uses tickle, enemy receives -2 atk and -1 defense
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2);
|
||||
expect(userPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(userPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
});
|
||||
|
||||
it("Player side + double battle Intimidate + Tickle - opponents each lose -3 atk, -1 def", async () => {
|
||||
game.override.battleType("double");
|
||||
game.override.ability(Abilities.MIRROR_ARMOR);
|
||||
game.override.enemyAbility(Abilities.INTIMIDATE);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
|
||||
|
||||
const [ enemy1, enemy2 ] = game.scene.getEnemyField();
|
||||
const [ player1, player2 ] = game.scene.getPlayerField();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER);
|
||||
await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player1.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(player1.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(player2.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(player2.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(enemy1.getStatStage(Stat.ATK)).toBe(-3);
|
||||
expect(enemy1.getStatStage(Stat.DEF)).toBe(-1);
|
||||
expect(enemy2.getStatStage(Stat.ATK)).toBe(-3);
|
||||
expect(enemy2.getStatStage(Stat.DEF)).toBe(-1);
|
||||
|
||||
});
|
||||
|
||||
it("Enemy side + single battle Intimidate + Tickle - player loses stats", async () => {
|
||||
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
|
||||
game.override.ability(Abilities.INTIMIDATE);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// Enemy has intimidate and uses tickle, enemy receives -2 atk and -1 defense
|
||||
game.move.select(Moves.TICKLE);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(userPokemon.getStatStage(Stat.DEF)).toBe(-1);
|
||||
expect(userPokemon.getStatStage(Stat.ATK)).toBe(-2);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
});
|
||||
|
||||
it("Player side + single battle Intimidate + oppoenent has white smoke - no one loses stats", async () => {
|
||||
game.override.enemyAbility(Abilities.WHITE_SMOKE);
|
||||
game.override.ability(Abilities.MIRROR_ARMOR);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// Enemy has intimidate and uses tickle, enemy has white smoke, no one loses stats
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(userPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(userPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
});
|
||||
|
||||
it("Enemy side + single battle Intimidate + player has white smoke - no one loses stats", async () => {
|
||||
game.override.ability(Abilities.WHITE_SMOKE);
|
||||
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// Enemy has intimidate and uses tickle, enemy has white smoke, no one loses stats
|
||||
game.move.select(Moves.TICKLE);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(userPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(userPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
});
|
||||
|
||||
it("Player side + single battle + opponent uses octolock - does not interact with mirror armor, player loses stats", async () => {
|
||||
game.override.ability(Abilities.MIRROR_ARMOR);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// Enemy uses octolock, player loses stats at end of turn
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.OCTOLOCK, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
|
||||
expect(userPokemon.getStatStage(Stat.DEF)).toBe(-1);
|
||||
expect(userPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
|
||||
});
|
||||
|
||||
it("Enemy side + single battle + player uses octolock - does not interact with mirror armor, opponent loses stats", async () => {
|
||||
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// Player uses octolock, enemy loses stats at end of turn
|
||||
game.move.select(Moves.OCTOLOCK);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(userPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(userPokemon.getStatStage(Stat.SPDEF)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
|
||||
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
|
||||
});
|
||||
|
||||
it("Both sides have mirror armor - does not loop, player loses attack", async () => {
|
||||
game.override.enemyAbility(Abilities.MIRROR_ARMOR);
|
||||
game.override.ability(Abilities.MIRROR_ARMOR);
|
||||
game.override.ability(Abilities.INTIMIDATE);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(userPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
it("Single battle + sticky web applied player side - player switches out and enemy should lose -1 speed", async () => {
|
||||
game.override.ability(Abilities.MIRROR_ARMOR);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const userPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.STICKY_WEB, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(userPokemon.getStatStage(Stat.SPD)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(-1);
|
||||
});
|
||||
|
||||
it("Double battle + sticky web applied player side - player switches out and enemy 1 should lose -1 speed", async () => {
|
||||
game.override.battleType("double");
|
||||
game.override.ability(Abilities.MIRROR_ARMOR);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
|
||||
|
||||
const [ enemy1, enemy2 ] = game.scene.getEnemyField();
|
||||
const [ player1, player2 ] = game.scene.getPlayerField();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
await game.forceEnemyMove(Moves.STICKY_WEB, BattlerIndex.PLAYER);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.doSwitchPokemon(2);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy1.getStatStage(Stat.SPD)).toBe(-1);
|
||||
expect(enemy2.getStatStage(Stat.SPD)).toBe(0);
|
||||
expect(player1.getStatStage(Stat.SPD)).toBe(0);
|
||||
expect(player2.getStatStage(Stat.SPD)).toBe(0);
|
||||
});
|
||||
});
|
66
src/test/abilities/protosynthesis.test.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Nature } from "#enums/nature";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#enums/stat";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Abilities - Protosynthesis", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([ Moves.SPLASH, Moves.TACKLE ])
|
||||
.ability(Abilities.PROTOSYNTHESIS)
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
it("should not consider temporary items when determining which stat to boost", async() => {
|
||||
// Mew has uniform base stats
|
||||
game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.DEF }])
|
||||
.enemyMoveset(Moves.SUNNY_DAY)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
await game.classicMode.startBattle([ Species.MEW ]);
|
||||
const mew = game.scene.getPlayerPokemon()!;
|
||||
// Nature of starting mon is randomized. We need to fix it to a neutral nature for the automated test.
|
||||
mew.setNature(Nature.HARDY);
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
const def_before_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true);
|
||||
const atk_before_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true);
|
||||
const initialHp = enemy.hp;
|
||||
game.move.select(Moves.TACKLE);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.toNextTurn();
|
||||
const unboosted_dmg = initialHp - enemy.hp;
|
||||
enemy.hp = initialHp;
|
||||
const def_after_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true);
|
||||
const atk_after_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true);
|
||||
game.move.select(Moves.TACKLE);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.toNextTurn();
|
||||
const boosted_dmg = initialHp - enemy.hp;
|
||||
expect(boosted_dmg).toBeGreaterThan(unboosted_dmg);
|
||||
expect(def_after_boost).toEqual(def_before_boost);
|
||||
expect(atk_after_boost).toBeGreaterThan(atk_before_boost);
|
||||
});
|
||||
});
|
@ -53,11 +53,11 @@ describe("Abilities - Shield Dust", () => {
|
||||
expect(move.id).toBe(Moves.AIR_SLASH);
|
||||
|
||||
const chance = new NumberHolder(move.chance);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
|
||||
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance);
|
||||
await applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
|
||||
await applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance);
|
||||
expect(chance.value).toBe(0);
|
||||
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
//TODO King's Rock Interaction Unit Test
|
||||
});
|
||||
|
178
src/test/abilities/supreme_overlord.test.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -45,9 +45,9 @@ describe("Abilities - Unseen Fist", () => {
|
||||
|
||||
it(
|
||||
"should not apply if the source has Long Reach",
|
||||
() => {
|
||||
async () => {
|
||||
game.override.passiveAbility(Abilities.LONG_REACH);
|
||||
testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false);
|
||||
await testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false);
|
||||
}
|
||||
);
|
||||
|
||||
@ -67,7 +67,7 @@ describe("Abilities - Unseen Fist", () => {
|
||||
game.override.enemyLevel(1);
|
||||
game.override.moveset([ Moves.TACKLE ]);
|
||||
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id);
|
||||
@ -86,7 +86,7 @@ async function testUnseenFistHitResult(game: GameManager, attackMove: Moves, pro
|
||||
game.override.moveset([ attackMove ]);
|
||||
game.override.enemyMoveset([ protectMove, protectMove, protectMove, protectMove ]);
|
||||
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const leadPokemon = game.scene.getPlayerPokemon()!;
|
||||
expect(leadPokemon).not.toBe(undefined);
|
||||
|
@ -20,8 +20,8 @@ const pokemonName = "PKM";
|
||||
const sourceText = "SOURCE";
|
||||
|
||||
describe("Status Effect Messages", () => {
|
||||
beforeAll(() => {
|
||||
i18next.init();
|
||||
beforeAll(async () => {
|
||||
await i18next.init();
|
||||
});
|
||||
|
||||
describe("NONE", () => {
|
||||
|
@ -40,10 +40,10 @@ describe("Evolution", () => {
|
||||
eevee.abilityIndex = 2;
|
||||
trapinch.abilityIndex = 2;
|
||||
|
||||
eevee.evolve(pokemonEvolutions[Species.EEVEE][6], eevee.getSpeciesForm());
|
||||
await eevee.evolve(pokemonEvolutions[Species.EEVEE][6], eevee.getSpeciesForm());
|
||||
expect(eevee.abilityIndex).toBe(2);
|
||||
|
||||
trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm());
|
||||
await trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm());
|
||||
expect(trapinch.abilityIndex).toBe(1);
|
||||
});
|
||||
|
||||
@ -55,10 +55,10 @@ describe("Evolution", () => {
|
||||
bulbasaur.abilityIndex = 0;
|
||||
charmander.abilityIndex = 1;
|
||||
|
||||
bulbasaur.evolve(pokemonEvolutions[Species.BULBASAUR][0], bulbasaur.getSpeciesForm());
|
||||
await bulbasaur.evolve(pokemonEvolutions[Species.BULBASAUR][0], bulbasaur.getSpeciesForm());
|
||||
expect(bulbasaur.abilityIndex).toBe(0);
|
||||
|
||||
charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm());
|
||||
await charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm());
|
||||
expect(charmander.abilityIndex).toBe(1);
|
||||
});
|
||||
|
||||
@ -68,7 +68,7 @@ describe("Evolution", () => {
|
||||
const squirtle = game.scene.getPlayerPokemon()!;
|
||||
squirtle.abilityIndex = 5;
|
||||
|
||||
squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm());
|
||||
await squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm());
|
||||
expect(squirtle.abilityIndex).toBe(0);
|
||||
});
|
||||
|
||||
@ -80,7 +80,7 @@ describe("Evolution", () => {
|
||||
nincada.metBiome = -1;
|
||||
nincada.gender = 1;
|
||||
|
||||
nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm());
|
||||
await nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm());
|
||||
const ninjask = game.scene.getPlayerParty()[0];
|
||||
const shedinja = game.scene.getPlayerParty()[1];
|
||||
expect(ninjask.abilityIndex).toBe(2);
|
||||
|
@ -31,7 +31,7 @@ describe("Items - Light Ball", () => {
|
||||
it("LIGHT_BALL activates in battle correctly", async() => {
|
||||
game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "LIGHT_BALL" }]);
|
||||
const consoleSpy = vi.spyOn(console, "log");
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
@ -64,7 +64,7 @@ describe("Items - Light Ball", () => {
|
||||
});
|
||||
|
||||
it("LIGHT_BALL held by PIKACHU", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
@ -83,7 +83,7 @@ describe("Items - Light Ball", () => {
|
||||
expect(spAtkValue.value / spAtkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
|
||||
|
||||
@ -92,7 +92,7 @@ describe("Items - Light Ball", () => {
|
||||
}, 20000);
|
||||
|
||||
it("LIGHT_BALL held by fused PIKACHU (base)", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU,
|
||||
Species.MAROWAK
|
||||
]);
|
||||
@ -122,7 +122,7 @@ describe("Items - Light Ball", () => {
|
||||
expect(spAtkValue.value / spAtkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
|
||||
|
||||
@ -161,7 +161,7 @@ describe("Items - Light Ball", () => {
|
||||
expect(spAtkValue.value / spAtkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
|
||||
|
||||
@ -189,7 +189,7 @@ describe("Items - Light Ball", () => {
|
||||
expect(spAtkValue.value / spAtkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue);
|
||||
|
||||
|
@ -31,7 +31,7 @@ describe("Items - Metal Powder", () => {
|
||||
it("METAL_POWDER activates in battle correctly", async() => {
|
||||
game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "METAL_POWDER" }]);
|
||||
const consoleSpy = vi.spyOn(console, "log");
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
|
||||
@ -79,7 +79,7 @@ describe("Items - Metal Powder", () => {
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(2);
|
||||
@ -112,7 +112,7 @@ describe("Items - Metal Powder", () => {
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(2);
|
||||
@ -145,7 +145,7 @@ describe("Items - Metal Powder", () => {
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(2);
|
||||
@ -167,7 +167,7 @@ describe("Items - Metal Powder", () => {
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue);
|
||||
|
||||
expect(defValue.value / defStat).toBe(1);
|
||||
|
@ -31,7 +31,7 @@ describe("Items - Quick Powder", () => {
|
||||
it("QUICK_POWDER activates in battle correctly", async() => {
|
||||
game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]);
|
||||
const consoleSpy = vi.spyOn(console, "log");
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
|
||||
@ -64,7 +64,7 @@ describe("Items - Quick Powder", () => {
|
||||
});
|
||||
|
||||
it("QUICK_POWDER held by DITTO", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.DITTO
|
||||
]);
|
||||
|
||||
@ -79,14 +79,14 @@ describe("Items - Quick Powder", () => {
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("QUICK_POWDER held by fused DITTO (base)", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.DITTO,
|
||||
Species.MAROWAK
|
||||
]);
|
||||
@ -112,14 +112,14 @@ describe("Items - Quick Powder", () => {
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("QUICK_POWDER held by fused DITTO (part)", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.MAROWAK,
|
||||
Species.DITTO
|
||||
]);
|
||||
@ -145,14 +145,14 @@ describe("Items - Quick Powder", () => {
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("QUICK_POWDER not held by DITTO", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.MAROWAK
|
||||
]);
|
||||
|
||||
@ -167,9 +167,9 @@ describe("Items - Quick Powder", () => {
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue);
|
||||
|
||||
expect(spdValue.value / spdStat).toBe(1);
|
||||
}, 20000);
|
||||
});
|
||||
});
|
||||
|
@ -31,7 +31,7 @@ describe("Items - Thick Club", () => {
|
||||
it("THICK_CLUB activates in battle correctly", async() => {
|
||||
game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]);
|
||||
const consoleSpy = vi.spyOn(console, "log");
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.CUBONE
|
||||
]);
|
||||
|
||||
@ -64,7 +64,7 @@ describe("Items - Thick Club", () => {
|
||||
});
|
||||
|
||||
it("THICK_CLUB held by CUBONE", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.CUBONE
|
||||
]);
|
||||
|
||||
@ -79,14 +79,14 @@ describe("Items - Thick Club", () => {
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("THICK_CLUB held by MAROWAK", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.MAROWAK
|
||||
]);
|
||||
|
||||
@ -101,14 +101,14 @@ describe("Items - Thick Club", () => {
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("THICK_CLUB held by ALOLA_MAROWAK", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.ALOLA_MAROWAK
|
||||
]);
|
||||
|
||||
@ -123,18 +123,18 @@ describe("Items - Thick Club", () => {
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("THICK_CLUB held by fused CUBONE line (base)", async() => {
|
||||
// Randomly choose from the Cubone line
|
||||
const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ];
|
||||
const randSpecies = Utils.randInt(species.length);
|
||||
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
species[randSpecies],
|
||||
Species.PIKACHU
|
||||
]);
|
||||
@ -160,18 +160,18 @@ describe("Items - Thick Club", () => {
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("THICK_CLUB held by fused CUBONE line (part)", async() => {
|
||||
// Randomly choose from the Cubone line
|
||||
const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ];
|
||||
const randSpecies = Utils.randInt(species.length);
|
||||
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU,
|
||||
species[randSpecies]
|
||||
]);
|
||||
@ -197,14 +197,14 @@ describe("Items - Thick Club", () => {
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(2);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("THICK_CLUB not held by CUBONE", async() => {
|
||||
await game.startBattle([
|
||||
await game.classicMode.startBattle([
|
||||
Species.PIKACHU
|
||||
]);
|
||||
|
||||
@ -219,9 +219,9 @@ describe("Items - Thick Club", () => {
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
|
||||
// Giving Eviolite to party member and testing if it applies
|
||||
game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true);
|
||||
game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue);
|
||||
|
||||
expect(atkValue.value / atkStat).toBe(1);
|
||||
}, 20000);
|
||||
});
|
||||
});
|
||||
|
@ -45,14 +45,10 @@ describe("Moves - Dragon Rage", () => {
|
||||
game.override.enemyPassiveAbility(Abilities.BALL_FETCH);
|
||||
game.override.enemyLevel(100);
|
||||
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
partyPokemon = game.scene.getPlayerParty()[0];
|
||||
enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// remove berries
|
||||
game.scene.removePartyMemberModifiers(0);
|
||||
game.scene.clearEnemyHeldItemModifiers();
|
||||
});
|
||||
|
||||
it("ignores weaknesses", async () => {
|
||||
|
@ -41,14 +41,10 @@ describe("Moves - Fissure", () => {
|
||||
game.override.enemyPassiveAbility(Abilities.BALL_FETCH);
|
||||
game.override.enemyLevel(100);
|
||||
|
||||
await game.startBattle();
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
partyPokemon = game.scene.getPlayerParty()[0];
|
||||
enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// remove berries
|
||||
game.scene.removePartyMemberModifiers(0);
|
||||
game.scene.clearEnemyHeldItemModifiers();
|
||||
});
|
||||
|
||||
it("ignores damage modification from abilities, for example FUR_COAT", async () => {
|
||||
|
219
src/test/moves/last_respects.test.ts
Normal 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);
|
||||
});
|
||||
});
|
286
src/test/moves/magic_coat.test.ts
Normal 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();
|
||||
});
|
||||
});
|
224
src/test/moves/spectral_thief.test.ts
Normal 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());
|
||||
});
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { allMoves, TeraMoveCategoryAttr } from "#app/data/move";
|
||||
import { Type } from "#enums/type";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { HitResult } from "#app/field/pokemon";
|
||||
@ -14,6 +14,7 @@ describe("Moves - Tera Blast", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
const moveToCheck = allMoves[Moves.TERA_BLAST];
|
||||
const teraBlastAttr = moveToCheck.getAttrs(TeraMoveCategoryAttr)[0];
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
@ -36,8 +37,8 @@ describe("Moves - Tera Blast", () => {
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyLevel(20);
|
||||
.enemyAbility(Abilities.STURDY)
|
||||
.enemyLevel(50);
|
||||
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
});
|
||||
@ -91,9 +92,7 @@ describe("Moves - Tera Blast", () => {
|
||||
expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE);
|
||||
});
|
||||
|
||||
// Currently abilities are bugged and can't see when a move's category is changed
|
||||
it.todo("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => {
|
||||
game.override.enemyAbility(Abilities.TOXIC_DEBRIS);
|
||||
it("uses the higher ATK for damage calculation", async () => {
|
||||
await game.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
@ -101,10 +100,79 @@ describe("Moves - Tera Blast", () => {
|
||||
playerPokemon.stats[Stat.SPATK] = 1;
|
||||
playerPokemon.isTerastallized = true;
|
||||
|
||||
vi.spyOn(teraBlastAttr, "apply");
|
||||
|
||||
game.move.select(Moves.TERA_BLAST);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true);
|
||||
}, 20000);
|
||||
await game.toNextTurn();
|
||||
expect(teraBlastAttr.apply).toHaveLastReturnedWith(true);
|
||||
});
|
||||
|
||||
it("uses the higher SPATK for damage calculation", async () => {
|
||||
await game.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
playerPokemon.stats[Stat.ATK] = 1;
|
||||
playerPokemon.stats[Stat.SPATK] = 100;
|
||||
|
||||
vi.spyOn(teraBlastAttr, "apply");
|
||||
|
||||
game.move.select(Moves.TERA_BLAST);
|
||||
await game.toNextTurn();
|
||||
expect(teraBlastAttr.apply).toHaveLastReturnedWith(false);
|
||||
});
|
||||
|
||||
it("should stay as a special move if ATK turns lower than SPATK mid-turn", async () => {
|
||||
game.override.enemyMoveset([ Moves.CHARM ]);
|
||||
await game.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
playerPokemon.stats[Stat.ATK] = 51;
|
||||
playerPokemon.stats[Stat.SPATK] = 50;
|
||||
|
||||
vi.spyOn(teraBlastAttr, "apply");
|
||||
|
||||
game.move.select(Moves.TERA_BLAST);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.toNextTurn();
|
||||
expect(teraBlastAttr.apply).toHaveLastReturnedWith(false);
|
||||
});
|
||||
|
||||
it("does not change its move category from stat changes due to held items", async () => {
|
||||
game.override
|
||||
.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }])
|
||||
.starterSpecies(Species.CUBONE);
|
||||
await game.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
playerPokemon.stats[Stat.ATK] = 50;
|
||||
playerPokemon.stats[Stat.SPATK] = 51;
|
||||
|
||||
vi.spyOn(teraBlastAttr, "apply");
|
||||
|
||||
game.move.select(Moves.TERA_BLAST);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(teraBlastAttr.apply).toHaveLastReturnedWith(false);
|
||||
});
|
||||
|
||||
it("does not change its move category from stat changes due to abilities", async () => {
|
||||
game.override.ability(Abilities.HUGE_POWER);
|
||||
await game.startBattle();
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
playerPokemon.stats[Stat.ATK] = 50;
|
||||
playerPokemon.stats[Stat.SPATK] = 51;
|
||||
|
||||
vi.spyOn(teraBlastAttr, "apply");
|
||||
|
||||
game.move.select(Moves.TERA_BLAST);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.toNextTurn();
|
||||
expect(teraBlastAttr.apply).toHaveLastReturnedWith(false);
|
||||
});
|
||||
|
||||
|
||||
it("causes stat drops if user is Stellar tera type", async () => {
|
||||
await game.startBattle();
|
||||
|
@ -132,7 +132,7 @@ describe("Moves - Toxic Spikes", () => {
|
||||
const sessionData : SessionSaveData = gameData["getSessionSaveData"]();
|
||||
localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true));
|
||||
const recoveredData : SessionSaveData = gameData.parseSessionData(decrypt(localStorage.getItem("sessionTestData")!, true));
|
||||
gameData.loadSession(0, recoveredData);
|
||||
await gameData.loadSession(0, recoveredData);
|
||||
|
||||
expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags);
|
||||
localStorage.removeItem("sessionTestData");
|
||||
|
@ -48,12 +48,12 @@ describe("Mystery Encounter Utils", () => {
|
||||
expect(result.species.speciesId).toBe(Species.ARCEUS);
|
||||
});
|
||||
|
||||
it("gets a fainted pokemon from player party if isAllowedInBattle is false", () => {
|
||||
it("gets a fainted pokemon from player party if isAllowedInBattle is false", async () => {
|
||||
// Both pokemon fainted
|
||||
scene.getPlayerParty().forEach(p => {
|
||||
p.hp = 0;
|
||||
p.trySetStatus(StatusEffect.FAINT);
|
||||
p.updateInfo();
|
||||
void p.updateInfo();
|
||||
});
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
@ -68,12 +68,12 @@ describe("Mystery Encounter Utils", () => {
|
||||
expect(result.species.speciesId).toBe(Species.ARCEUS);
|
||||
});
|
||||
|
||||
it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", () => {
|
||||
it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", async () => {
|
||||
// Only faint 1st pokemon
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].updateInfo();
|
||||
await party[0].updateInfo();
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
game.override.seed("random");
|
||||
@ -87,12 +87,12 @@ describe("Mystery Encounter Utils", () => {
|
||||
expect(result.species.speciesId).toBe(Species.MANAPHY);
|
||||
});
|
||||
|
||||
it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", () => {
|
||||
it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", async () => {
|
||||
// Only faint 1st pokemon
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].updateInfo();
|
||||
await party[0].updateInfo();
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
game.override.seed("random");
|
||||
@ -106,12 +106,12 @@ describe("Mystery Encounter Utils", () => {
|
||||
expect(result.species.speciesId).toBe(Species.MANAPHY);
|
||||
});
|
||||
|
||||
it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", () => {
|
||||
it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", async () => {
|
||||
// Only faint 1st pokemon
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].updateInfo();
|
||||
await party[0].updateInfo();
|
||||
|
||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||
game.override.seed("random");
|
||||
@ -152,12 +152,12 @@ describe("Mystery Encounter Utils", () => {
|
||||
expect(result.species.speciesId).toBe(Species.ARCEUS);
|
||||
});
|
||||
|
||||
it("returns highest level unfainted if unfainted is true", () => {
|
||||
it("returns highest level unfainted if unfainted is true", async () => {
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].level = 100;
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].updateInfo();
|
||||
await party[0].updateInfo();
|
||||
party[1].level = 10;
|
||||
|
||||
const result = getHighestLevelPlayerPokemon(true);
|
||||
@ -191,12 +191,12 @@ describe("Mystery Encounter Utils", () => {
|
||||
expect(result.species.speciesId).toBe(Species.ARCEUS);
|
||||
});
|
||||
|
||||
it("returns lowest level unfainted if unfainted is true", () => {
|
||||
it("returns lowest level unfainted if unfainted is true", async () => {
|
||||
const party = scene.getPlayerParty();
|
||||
party[0].level = 10;
|
||||
party[0].hp = 0;
|
||||
party[0].trySetStatus(StatusEffect.FAINT);
|
||||
party[0].updateInfo();
|
||||
await party[0].updateInfo();
|
||||
party[1].level = 100;
|
||||
|
||||
const result = getLowestLevelPlayerPokemon(true);
|
||||
|
@ -2,8 +2,6 @@ import { BerryType } from "#app/enums/berry-type";
|
||||
import { Button } from "#app/enums/buttons";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Species } from "#app/enums/species";
|
||||
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
||||
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
|
||||
import PartyUiHandler, { PartyUiMode } from "#app/ui/party-ui-handler";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
@ -12,7 +10,6 @@ import Phaser from "phaser";
|
||||
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
|
||||
describe("UI - Transfer Items", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
@ -41,7 +38,7 @@ describe("UI - Transfer Items", () => {
|
||||
game.override.enemySpecies(Species.MAGIKARP);
|
||||
game.override.enemyMoveset([ Moves.SPLASH ]);
|
||||
|
||||
await game.startBattle([ Species.RAYQUAZA, Species.RAYQUAZA, Species.RAYQUAZA ]);
|
||||
await game.classicMode.startBattle([ Species.RAYQUAZA, Species.RAYQUAZA, Species.RAYQUAZA ]);
|
||||
|
||||
game.move.select(Moves.DRAGON_CLAW);
|
||||
|
||||
@ -52,10 +49,10 @@ describe("UI - Transfer Items", () => {
|
||||
handler.setCursor(1);
|
||||
handler.processInput(Button.ACTION);
|
||||
|
||||
game.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER);
|
||||
void game.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER);
|
||||
});
|
||||
|
||||
await game.phaseInterceptor.to(BattleEndPhase);
|
||||
await game.phaseInterceptor.to("BattleEndPhase");
|
||||
});
|
||||
|
||||
it("check red tint for held item limit in transfer menu", async () => {
|
||||
@ -72,7 +69,7 @@ describe("UI - Transfer Items", () => {
|
||||
game.phaseInterceptor.unlock();
|
||||
});
|
||||
|
||||
await game.phaseInterceptor.to(SelectModifierPhase);
|
||||
await game.phaseInterceptor.to("SelectModifierPhase");
|
||||
}, 20000);
|
||||
|
||||
it("check transfer option for pokemon to transfer to", async () => {
|
||||
@ -91,6 +88,6 @@ describe("UI - Transfer Items", () => {
|
||||
game.phaseInterceptor.unlock();
|
||||
});
|
||||
|
||||
await game.phaseInterceptor.to(SelectModifierPhase);
|
||||
await game.phaseInterceptor.to("SelectModifierPhase");
|
||||
}, 20000);
|
||||
});
|
||||
|
@ -27,6 +27,7 @@ interface EventBanner {
|
||||
interface EventEncounter {
|
||||
species: Species;
|
||||
blockEvolution?: boolean;
|
||||
formIndex?: number;
|
||||
}
|
||||
|
||||
interface EventMysteryEncounterTier {
|
||||
@ -49,6 +50,7 @@ interface TimedEvent extends EventBanner {
|
||||
weather?: WeatherPoolEntry[];
|
||||
mysteryEncounterTierChanges?: EventMysteryEncounterTier[];
|
||||
luckBoostedSpecies?: Species[];
|
||||
boostFusions?: boolean; //MODIFIER REWORK PLEASE
|
||||
}
|
||||
|
||||
const timedEvents: TimedEvent[] = [
|
||||
@ -144,6 +146,40 @@ const timedEvents: TimedEvent[] = [
|
||||
Species.ROARING_MOON,
|
||||
Species.BLOODMOON_URSALUNA
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Valentine",
|
||||
eventType: EventType.SHINY,
|
||||
startDate: new Date(Date.UTC(2025, 1, 10)),
|
||||
endDate: new Date(Date.UTC(2025, 1, 21)),
|
||||
boostFusions: true,
|
||||
shinyMultiplier: 2,
|
||||
bannerKey: "valentines2025event-",
|
||||
scale: 0.21,
|
||||
availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ],
|
||||
eventEncounters: [
|
||||
{ species: Species.NIDORAN_F },
|
||||
{ species: Species.NIDORAN_M },
|
||||
{ species: Species.IGGLYBUFF },
|
||||
{ species: Species.SMOOCHUM },
|
||||
{ species: Species.VOLBEAT },
|
||||
{ species: Species.ILLUMISE },
|
||||
{ species: Species.ROSELIA },
|
||||
{ species: Species.LUVDISC },
|
||||
{ species: Species.WOOBAT },
|
||||
{ species: Species.FRILLISH },
|
||||
{ species: Species.ALOMOMOLA },
|
||||
{ species: Species.FURFROU, formIndex: 1 }, // Heart trim
|
||||
{ species: Species.ESPURR },
|
||||
{ species: Species.SPRITZEE },
|
||||
{ species: Species.SWIRLIX },
|
||||
{ species: Species.APPLIN },
|
||||
{ species: Species.MILCERY },
|
||||
{ species: Species.INDEEDEE },
|
||||
{ species: Species.TANDEMAUS },
|
||||
{ species: Species.ENAMORUS }
|
||||
],
|
||||
luckBoostedSpecies: [ Species.LUVDISC ]
|
||||
}
|
||||
];
|
||||
|
||||
@ -297,6 +333,10 @@ export class TimedEventManager {
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
areFusionsBoosted(): boolean {
|
||||
return timedEvents.some((te) => this.isActive(te) && te.boostFusions);
|
||||
}
|
||||
}
|
||||
|
||||
export class TimedEventDisplay extends Phaser.GameObjects.Container {
|
||||
|
@ -6,7 +6,7 @@ import { addWindow } from "./ui-theme";
|
||||
import * as Utils from "../utils";
|
||||
import { argbFromRgba } from "@material/material-color-utilities";
|
||||
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 {
|
||||
xOffset?: number;
|
||||
|
@ -1,7 +1,17 @@
|
||||
import type { Variant } from "#app/data/variant";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { isNullOrUndefined } from "#app/utils";
|
||||
import type PokemonSpecies from "../data/pokemon-species";
|
||||
import { addTextObject, TextStyle } from "./text";
|
||||
|
||||
|
||||
interface SpeciesDetails {
|
||||
shiny?: boolean,
|
||||
formIndex?: number
|
||||
female?: boolean,
|
||||
variant?: Variant
|
||||
}
|
||||
|
||||
export class PokedexMonContainer extends Phaser.GameObjects.Container {
|
||||
public species: PokemonSpecies;
|
||||
public icon: Phaser.GameObjects.Sprite;
|
||||
@ -19,16 +29,34 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
|
||||
public tmMove2Icon: Phaser.GameObjects.Image;
|
||||
public passive1Icon: Phaser.GameObjects.Image;
|
||||
public passive2Icon: Phaser.GameObjects.Image;
|
||||
public passive1OverlayIcon: Phaser.GameObjects.Image;
|
||||
public passive2OverlayIcon: Phaser.GameObjects.Image;
|
||||
public cost: number = 0;
|
||||
|
||||
constructor(species: PokemonSpecies) {
|
||||
constructor(species: PokemonSpecies, options: SpeciesDetails = {}) {
|
||||
super(globalScene, 0, 0);
|
||||
|
||||
this.species = species;
|
||||
|
||||
const { shiny, formIndex, female, variant } = options;
|
||||
|
||||
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true);
|
||||
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
|
||||
const starterPassiveBg = globalScene.add.image(2, 5, "passive_bg");
|
||||
starterPassiveBg.setOrigin(0, 0);
|
||||
@ -137,7 +165,7 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
|
||||
this.tmMove2Icon = tmMove2Icon;
|
||||
|
||||
|
||||
// move icons
|
||||
// passive icons
|
||||
const passive1Icon = globalScene.add.image(3, 3, "candy");
|
||||
passive1Icon.setOrigin(0, 0);
|
||||
passive1Icon.setScale(0.25);
|
||||
@ -145,13 +173,27 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container {
|
||||
this.add(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");
|
||||
passive2Icon.setOrigin(0, 0);
|
||||
passive2Icon.setScale(0.25);
|
||||
passive2Icon.setVisible(false);
|
||||
this.add(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) {
|
||||
|
@ -43,7 +43,6 @@ import type { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { EggSourceType } from "#enums/egg-source-types";
|
||||
import { StarterContainer } from "#app/ui/starter-container";
|
||||
import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters";
|
||||
import { BooleanHolder, capitalizeString, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, rgbHexToRgba, toReadableString } from "#app/utils";
|
||||
import type { Nature } from "#enums/nature";
|
||||
@ -128,7 +127,6 @@ interface SpeciesDetails {
|
||||
formIndex?: number
|
||||
female?: boolean,
|
||||
variant?: number,
|
||||
forSeen?: boolean, // default = false
|
||||
}
|
||||
|
||||
enum MenuOptions {
|
||||
@ -147,8 +145,6 @@ enum MenuOptions {
|
||||
export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
private starterSelectContainer: Phaser.GameObjects.Container;
|
||||
private shinyOverlay: Phaser.GameObjects.Image;
|
||||
private starterContainers: StarterContainer[] = [];
|
||||
private filteredStarterContainers: StarterContainer[] = [];
|
||||
private pokemonNumberText: Phaser.GameObjects.Text;
|
||||
private pokemonSprite: Phaser.GameObjects.Sprite;
|
||||
private pokemonNameText: Phaser.GameObjects.Text;
|
||||
@ -199,6 +195,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
|
||||
private allSpecies: PokemonSpecies[] = [];
|
||||
private species: PokemonSpecies;
|
||||
private starterId: number;
|
||||
private formIndex: number;
|
||||
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
|
||||
private levelMoves: LevelMoves;
|
||||
@ -312,10 +309,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
|
||||
this.speciesLoaded.set(species.speciesId, false);
|
||||
this.allSpecies.push(species);
|
||||
|
||||
const starterContainer = new StarterContainer(species).setVisible(false);
|
||||
this.starterContainers.push(starterContainer);
|
||||
starterBoxContainer.add(starterContainer);
|
||||
}
|
||||
|
||||
this.starterSelectContainer.add(starterBoxContainer);
|
||||
@ -513,7 +506,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
|
||||
this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale;
|
||||
this.menuBg = addWindow(
|
||||
(globalScene.game.canvas.width / 6) - (this.optionSelectText.displayWidth + 25),
|
||||
(globalScene.game.canvas.width / 6 - 83),
|
||||
0,
|
||||
this.optionSelectText.displayWidth + 19 + 24 * this.scale,
|
||||
(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
|
||||
this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer);
|
||||
|
||||
this.updateInstructions();
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
@ -603,6 +594,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
const species = this.species;
|
||||
const formIndex = this.formIndex ?? 0;
|
||||
|
||||
this.starterId = this.getStarterSpeciesId(this.species.speciesId);
|
||||
|
||||
const allEvolutions = pokemonEvolutions.hasOwnProperty(species.speciesId) ? pokemonEvolutions[species.speciesId] : [];
|
||||
|
||||
if (species.forms.length > 0) {
|
||||
@ -629,17 +622,19 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
this.baseTotal = species.baseTotal;
|
||||
}
|
||||
|
||||
this.eggMoves = speciesEggMoves[this.getStarterSpeciesId(species.speciesId)] ?? [];
|
||||
this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)].eggMoves & (1 << em)) !== 0);
|
||||
this.eggMoves = speciesEggMoves[this.starterId] ?? [];
|
||||
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 : "";
|
||||
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) ?? [];
|
||||
|
||||
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];
|
||||
|
||||
const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)];
|
||||
const starterData = globalScene.gameData.starterData[this.starterId];
|
||||
const abilityAttr = starterData.abilityAttr;
|
||||
this.hasPassive = starterData.passiveAttr > 0;
|
||||
|
||||
@ -655,9 +650,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
|
||||
const allBiomes = catchableSpecies[species.speciesId] ?? [];
|
||||
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)),
|
||||
this.getStarterSpeciesId(species.speciesId));
|
||||
this.starterId);
|
||||
this.biomes = this.sanitizeBiomes(allBiomes, 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 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
|
||||
starterAttributes.shiny = false;
|
||||
starterAttributes.variant = 0;
|
||||
} else if (starterAttributes.shiny === false && !hasNonShiny) {
|
||||
// non shiny form wasn't unlocked, purging shiny setting
|
||||
starterAttributes.shiny = false;
|
||||
} else if (!hasNonShiny || (starterAttributes.shiny === undefined && hasShiny)) {
|
||||
starterAttributes.shiny = true;
|
||||
starterAttributes.variant = 0;
|
||||
}
|
||||
|
||||
if (starterAttributes.variant !== undefined) {
|
||||
const unlockedVariants = [
|
||||
hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT,
|
||||
hasShiny && caughtAttr & DexAttr.VARIANT_2,
|
||||
hasShiny && caughtAttr & DexAttr.VARIANT_3
|
||||
];
|
||||
if (isNaN(starterAttributes.variant) || starterAttributes.variant < 0) {
|
||||
starterAttributes.variant = 0;
|
||||
} else if (!unlockedVariants[starterAttributes.variant]) {
|
||||
let highestValidIndex = -1;
|
||||
for (let i = 0; i <= starterAttributes.variant && i < unlockedVariants.length; i++) {
|
||||
if (unlockedVariants[i] !== 0n) {
|
||||
highestValidIndex = i;
|
||||
}
|
||||
const unlockedVariants = [
|
||||
hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT,
|
||||
hasShiny && caughtAttr & DexAttr.VARIANT_2,
|
||||
hasShiny && caughtAttr & DexAttr.VARIANT_3
|
||||
];
|
||||
if (starterAttributes.variant === undefined || isNaN(starterAttributes.variant) || starterAttributes.variant < 0) {
|
||||
starterAttributes.variant = 0;
|
||||
} else if (!unlockedVariants[starterAttributes.variant]) {
|
||||
let highestValidIndex = -1;
|
||||
for (let i = 0; i <= starterAttributes.variant && i < unlockedVariants.length; i++) {
|
||||
if (unlockedVariants[i] !== 0n) {
|
||||
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 && !(caughtAttr & DexAttr.FEMALE)) || (!starterAttributes.female && !(caughtAttr & DexAttr.MALE))) {
|
||||
starterAttributes.female = !starterAttributes.female;
|
||||
}
|
||||
} else {
|
||||
if (caughtAttr & DexAttr.FEMALE) {
|
||||
starterAttributes.female = true;
|
||||
} else if (caughtAttr & DexAttr.MALE) {
|
||||
starterAttributes.female = false;
|
||||
}
|
||||
}
|
||||
|
||||
return starterAttributes;
|
||||
@ -878,7 +877,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
* @returns the id of the corresponding starter
|
||||
*/
|
||||
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;
|
||||
} else {
|
||||
return pokemonStarters[speciesId];
|
||||
@ -886,7 +892,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
}
|
||||
|
||||
getStarterSpecies(species): PokemonSpecies {
|
||||
if (globalScene.gameData.starterData.hasOwnProperty(species.speciesId)) {
|
||||
if (speciesStarterCosts.hasOwnProperty(species.speciesId)) {
|
||||
return species;
|
||||
} else {
|
||||
return allSpecies.find(sp => sp.speciesId === pokemonStarters[species.speciesId]) ?? species;
|
||||
@ -970,7 +976,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
}
|
||||
} 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
|
||||
const starterAttributes = this.starterAttributes;
|
||||
|
||||
@ -1126,6 +1132,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
|
||||
if (!isCaught || !isFormCaught) {
|
||||
error = true;
|
||||
} else if (this.tmMoves.length < 1) {
|
||||
ui.showText(i18next.t("pokedexUiHandler:noTmMoves"));
|
||||
error = true;
|
||||
} else {
|
||||
this.blockInput = true;
|
||||
|
||||
@ -1633,90 +1642,55 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
error = true;
|
||||
} else {
|
||||
const ui = this.getUi();
|
||||
ui.showText("");
|
||||
const options: any[] = []; // TODO: add proper type
|
||||
|
||||
const passiveAttr = starterData.passiveAttr;
|
||||
const candyCount = starterData.candyCount;
|
||||
|
||||
if (!pokemonPrevolutions.hasOwnProperty(this.species.speciesId)) {
|
||||
if (!(passiveAttr & PassiveAttr.UNLOCKED)) {
|
||||
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)]);
|
||||
if (!(passiveAttr & PassiveAttr.UNLOCKED)) {
|
||||
const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.starterId]);
|
||||
options.push({
|
||||
label: `x${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`,
|
||||
label: `x${passiveCost} ${i18next.t("pokedexUiHandler:unlockPassive")} (${allAbilities[this.passive].name})`,
|
||||
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 || candyCount >= passiveCost) {
|
||||
starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED;
|
||||
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
|
||||
starterData.candyCount -= sameSpeciesEggCost;
|
||||
starterData.candyCount -= passiveCost;
|
||||
}
|
||||
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 });
|
||||
egg.addEggToGameData();
|
||||
return true;
|
||||
}
|
||||
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 => {
|
||||
if (!success) {
|
||||
return globalScene.reset(true);
|
||||
@ -1729,24 +1703,59 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
style: this.isValueReductionAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
|
||||
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");
|
||||
globalScene.playSound("se/buy");
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
|
||||
options: options,
|
||||
yOffset: 47
|
||||
});
|
||||
success = true;
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
style: this.isSameSpeciesEggAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
|
||||
item: "candy",
|
||||
itemArgs: this.isSameSpeciesEggAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ]
|
||||
});
|
||||
options.push({
|
||||
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;
|
||||
case Button.CYCLE_ABILITY:
|
||||
@ -1877,9 +1886,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
|
||||
if (this.isCaught()) {
|
||||
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) {
|
||||
this.updateButtonIcon(SettingKeyboard.Button_Cycle_Shiny, gamepadType, this.shinyIconElement, this.shinyLabel);
|
||||
}
|
||||
@ -1936,16 +1943,51 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
}
|
||||
|
||||
getFriendship(speciesId: number) {
|
||||
let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship;
|
||||
let currentFriendship = globalScene.gameData.starterData[this.starterId].friendship;
|
||||
if (!currentFriendship || currentFriendship === undefined) {
|
||||
currentFriendship = 0;
|
||||
}
|
||||
|
||||
const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]);
|
||||
const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.starterId]);
|
||||
|
||||
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() {
|
||||
const species = this.species;
|
||||
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())) {
|
||||
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()) {
|
||||
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 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
|
||||
const props: StarterAttributes = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
|
||||
if (starterAttributes?.variant && !isNaN(starterAttributes.variant)) {
|
||||
@ -2065,12 +2029,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
female: props.female,
|
||||
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 {
|
||||
this.pokemonGrowthRateText.setText("");
|
||||
this.pokemonGrowthRateLabelText.setVisible(false);
|
||||
@ -2092,7 +2050,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
formIndex: props.formIndex,
|
||||
female: props.female,
|
||||
variant: props.variant,
|
||||
forSeen: true
|
||||
});
|
||||
this.pokemonSprite.setTint(0x808080);
|
||||
}
|
||||
@ -2123,7 +2080,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
|
||||
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, forceUpdate?: boolean): void {
|
||||
let { shiny, formIndex, female, variant } = options;
|
||||
const forSeen: boolean = options.forSeen ?? false;
|
||||
const oldProps = species ? this.starterAttributes : null;
|
||||
|
||||
// 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 isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false;
|
||||
|
||||
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.setShadowColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, true));
|
||||
|
||||
|
||||
const assetLoadCancelled = new BooleanHolder(false);
|
||||
this.assetLoadCancelled = assetLoadCancelled;
|
||||
|
||||
@ -2221,13 +2177,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
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 isShinyCaught = !!(caughtAttr & DexAttr.SHINY);
|
||||
|
||||
@ -2250,27 +2199,129 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
|
||||
this.pokemonGenderText.setText("");
|
||||
}
|
||||
|
||||
if (caughtAttr) {
|
||||
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 {
|
||||
this.pokemonSprite.setTint(0x000000);
|
||||
}
|
||||
// Setting the name
|
||||
if (isFormCaught || isFormSeen) {
|
||||
this.pokemonNameText.setText(species.name);
|
||||
} else {
|
||||
this.pokemonNameText.setText(species ? "???" : "");
|
||||
}
|
||||
|
||||
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?
|
||||
this.setTypeIcons(speciesForm.type1, speciesForm.type2);
|
||||
this.pokemonFormText.setText(this.getFormString((speciesForm as PokemonForm).formKey, species));
|
||||
|
||||
this.pokemonFormText.setVisible(true);
|
||||
if (!isFormCaught) {
|
||||
this.pokemonFormText.setY(18);
|
||||
}
|
||||
} else {
|
||||
this.setTypeIcons(null, null);
|
||||
this.pokemonFormText.setText("");
|
||||
this.pokemonFormText.setVisible(false);
|
||||
}
|
||||
} else {
|
||||
this.shinyOverlay.setVisible(false);
|
||||
|
@ -11,7 +11,7 @@ import { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data
|
||||
import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters";
|
||||
import { catchableSpecies } from "#app/data/balance/biomes";
|
||||
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 MessageUiHandler from "#app/ui/message-ui-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 { SettingKeyboard } from "#app/system/settings/settings-keyboard";
|
||||
import { Passive as PassiveAttr } from "#enums/passive";
|
||||
import type { Moves } from "#enums/moves";
|
||||
import type { Species } from "#enums/species";
|
||||
import { Button } from "#enums/buttons";
|
||||
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 { globalScene } from "#app/global-scene";
|
||||
|
||||
|
||||
interface LanguageSetting {
|
||||
starterInfoTextSize: string,
|
||||
instructionTextSize: string,
|
||||
@ -139,7 +137,6 @@ interface SpeciesDetails {
|
||||
variant?: Variant,
|
||||
abilityIndex?: number,
|
||||
natureIndex?: number,
|
||||
forSeen?: boolean, // default = false
|
||||
}
|
||||
|
||||
export default class PokedexUiHandler extends MessageUiHandler {
|
||||
@ -161,7 +158,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
|
||||
private filterMode: boolean;
|
||||
private filterBarCursor: number = 0;
|
||||
private starterMoveset: StarterMoveset | null;
|
||||
private scrollCursor: number;
|
||||
|
||||
private allSpecies: PokemonSpecies[] = [];
|
||||
@ -169,7 +165,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
|
||||
private pokerusSpecies: PokemonSpecies[] = [];
|
||||
private speciesStarterDexEntry: DexEntry | null;
|
||||
private speciesStarterMoves: Moves[];
|
||||
|
||||
private assetLoadCancelled: BooleanHolder | null;
|
||||
public cursorObj: Phaser.GameObjects.Image;
|
||||
@ -206,6 +201,20 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
private toggleDecorationsIconElement: Phaser.GameObjects.Sprite;
|
||||
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() {
|
||||
super(Mode.POKEDEX);
|
||||
}
|
||||
@ -425,7 +434,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
|
||||
this.cursorObj = globalScene.add.image(0, 0, "select_cursor");
|
||||
this.cursorObj.setOrigin(0, 0);
|
||||
|
||||
starterBoxContainer.add(this.cursorObj);
|
||||
|
||||
for (const species of allSpecies) {
|
||||
@ -438,6 +446,20 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
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.pokemonSprite = globalScene.add.sprite(96, 143, "pkmn__sub");
|
||||
@ -449,7 +471,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
this.type1Icon.setOrigin(0, 0);
|
||||
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.setOrigin(0, 0);
|
||||
this.starterSelectContainer.add(this.type2Icon);
|
||||
@ -488,6 +510,17 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
this.starterSelectContainer.add(this.toggleDecorationsIconElement);
|
||||
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.setOrigin(0, 0);
|
||||
this.starterSelectMessageBoxContainer.add(this.message);
|
||||
@ -527,7 +560,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
|
||||
this.starterPreferences[species.speciesId] = this.initStarterPrefs(species);
|
||||
|
||||
if (dexEntry.caughtAttr) {
|
||||
if (dexEntry.caughtAttr || globalScene.dexForDevs) {
|
||||
icon.clearTint();
|
||||
} else if (dexEntry.seenAttr) {
|
||||
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)) {
|
||||
this.filterText.resetSelection(this.filterTextCursor);
|
||||
success = true;
|
||||
} else if (this.showingTray) {
|
||||
success = this.closeFormTray();
|
||||
} else {
|
||||
this.tryExit();
|
||||
success = true;
|
||||
}
|
||||
} else if (button === Button.STATS) {
|
||||
if (!this.filterMode) {
|
||||
if (!this.filterMode && !this.showingTray) {
|
||||
this.cursorObj.setVisible(false);
|
||||
this.setSpecies(null);
|
||||
this.filterText.cursorObj.setVisible(false);
|
||||
this.filterTextMode = false;
|
||||
this.filterBarCursor = 0;
|
||||
this.setFilterMode(true);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
} else if (button === Button.V) {
|
||||
if (!this.filterTextMode) {
|
||||
if (!this.filterTextMode && !this.showingTray) {
|
||||
this.cursorObj.setVisible(false);
|
||||
this.setSpecies(null);
|
||||
this.filterBar.cursorObj.setVisible(false);
|
||||
this.filterMode = false;
|
||||
this.filterTextCursor = 0;
|
||||
this.setFilterTextMode(true);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
} else if (button === Button.CYCLE_SHINY) {
|
||||
this.showDecorations = !this.showDecorations;
|
||||
this.updateScroll();
|
||||
success = true;
|
||||
if (!this.showingTray) {
|
||||
this.showDecorations = !this.showDecorations;
|
||||
this.updateScroll();
|
||||
success = true;
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
} else if (this.filterMode) {
|
||||
switch (button) {
|
||||
case Button.LEFT:
|
||||
@ -982,8 +1025,55 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
success = true;
|
||||
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 {
|
||||
|
||||
if (button === Button.ACTION) {
|
||||
ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, 0);
|
||||
success = true;
|
||||
@ -1042,6 +1132,12 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
success = true;
|
||||
}
|
||||
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:
|
||||
iconPath = "V.png";
|
||||
break;
|
||||
case SettingKeyboard.Button_Cycle_Form:
|
||||
iconPath = "F.png";
|
||||
break;
|
||||
case SettingKeyboard.Button_Stats:
|
||||
iconPath = "C.png";
|
||||
break;
|
||||
@ -1145,13 +1244,15 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
this.validPokemonContainers.forEach(container => {
|
||||
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
|
||||
// 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 starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(container.species.speciesId)];
|
||||
const isStarterProgressable = speciesEggMoves.hasOwnProperty(this.getStarterSpeciesId(container.species.speciesId));
|
||||
const starterData = globalScene.gameData.starterData[starterId];
|
||||
const isStarterProgressable = speciesEggMoves.hasOwnProperty(starterId);
|
||||
|
||||
// Name filter
|
||||
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)
|
||||
const levelMoves = pokemonSpeciesLevelMoves[container.species.speciesId].map(m => allMoves[m[1]].name);
|
||||
// This always gets egg moves from the starter
|
||||
const eggMoves = speciesEggMoves[this.getStarterSpeciesId(container.species.speciesId)]?.map(m => allMoves[m].name) ?? [];
|
||||
const tmMoves = speciesTmMoves[this.getStarterSpeciesId(container.species.speciesId)]?.map(m => allMoves[Array.isArray(m) ? m[1] : m].name) ?? [];
|
||||
const eggMoves = speciesEggMoves[starterId]?.map(m => allMoves[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 selectedMove2 = this.filterText.getValue(FilterTextRow.MOVE_2);
|
||||
|
||||
@ -1185,27 +1286,40 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
container.tmMove2Icon.setVisible(false);
|
||||
if (fitsEggMove1 && !fitsLevelMove1) {
|
||||
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) {
|
||||
container.tmMove1Icon.setVisible(true);
|
||||
}
|
||||
if (fitsEggMove2 && !fitsLevelMove2) {
|
||||
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) {
|
||||
container.tmMove2Icon.setVisible(true);
|
||||
}
|
||||
|
||||
// Ability filter
|
||||
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 fitsFormAbility = container.species.forms.some(form => allAbilities[form.ability1].name === selectedAbility1);
|
||||
const fitsAbility1 = abilities.includes(selectedAbility1) || fitsFormAbility || selectedAbility1 === this.filterText.defaultText;
|
||||
const fitsPassive1 = Object.values(passives).some(p => p.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) || fitsFormAbility1 || selectedAbility1 === this.filterText.defaultText;
|
||||
const fitsPassive1 = Object.values(passives).some(p => allAbilities[p].name === selectedAbility1);
|
||||
|
||||
const selectedAbility2 = this.filterText.getValue(FilterTextRow.ABILITY_2);
|
||||
const fitsAbility2 = abilities.includes(selectedAbility2) || fitsFormAbility || selectedAbility2 === this.filterText.defaultText;
|
||||
const fitsPassive2 = Object.values(passives).some(p => p.name === selectedAbility2);
|
||||
const fitsFormAbility2 = container.species.forms.some(form => [ form.ability1, form.ability2, form.abilityHidden ].map(a => allAbilities[a].name).includes(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
|
||||
const fitsAbilities = (fitsAbility1 && (fitsPassive2 || selectedAbility2 === this.filterText.defaultText)) ||
|
||||
@ -1213,11 +1327,26 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
|
||||
container.passive1Icon.setVisible(false);
|
||||
container.passive2Icon.setVisible(false);
|
||||
if (fitsPassive1) {
|
||||
container.passive1Icon.setVisible(true);
|
||||
}
|
||||
if (fitsPassive2) {
|
||||
container.passive2Icon.setVisible(true);
|
||||
if (fitsPassive1 || fitsPassive2) {
|
||||
if (fitsPassive1) {
|
||||
if (starterData.passiveAttr > 0) {
|
||||
container.passive1Icon.clearTint();
|
||||
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
|
||||
@ -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.
|
||||
// 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) {
|
||||
biomes.push("Uncatchable");
|
||||
}
|
||||
@ -1530,6 +1659,8 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
this.cursorObj.setVisible(!filterMode);
|
||||
this.filterBar.cursorObj.setVisible(filterMode);
|
||||
this.pokemonSprite.setVisible(false);
|
||||
this.showFormTrayIconElement.setVisible(false);
|
||||
this.showFormTrayLabel.setVisible(false);
|
||||
|
||||
if (filterMode !== this.filterMode) {
|
||||
this.filterMode = filterMode;
|
||||
@ -1546,6 +1677,8 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
this.cursorObj.setVisible(!filterTextMode);
|
||||
this.filterText.cursorObj.setVisible(filterTextMode);
|
||||
this.pokemonSprite.setVisible(false);
|
||||
this.showFormTrayIconElement.setVisible(false);
|
||||
this.showFormTrayLabel.setVisible(false);
|
||||
|
||||
if (filterTextMode !== this.filterTextMode) {
|
||||
this.filterTextMode = filterTextMode;
|
||||
@ -1558,6 +1691,101 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
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) {
|
||||
let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship;
|
||||
if (!currentFriendship || currentFriendship === undefined) {
|
||||
@ -1592,13 +1820,13 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
|
||||
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.pokemonNameText.setText(species.name);
|
||||
|
||||
if (this.speciesStarterDexEntry?.caughtAttr) {
|
||||
if (this.speciesStarterDexEntry?.caughtAttr || globalScene.dexForDevs) {
|
||||
|
||||
// Pause the animation when the species is selected
|
||||
const speciesIndex = this.allSpecies.indexOf(species);
|
||||
@ -1627,9 +1855,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
this.type1Icon.setVisible(true);
|
||||
this.type2Icon.setVisible(true);
|
||||
|
||||
this.setSpeciesDetails(species, {
|
||||
forSeen: true
|
||||
});
|
||||
this.setSpeciesDetails(species);
|
||||
this.pokemonSprite.setTint(0x808080);
|
||||
}
|
||||
} else {
|
||||
@ -1646,7 +1872,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
|
||||
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void {
|
||||
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
|
||||
// or gender for species with gender sprite differences
|
||||
@ -1667,34 +1892,33 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
this.assetLoadCancelled = null;
|
||||
}
|
||||
|
||||
this.starterMoveset = null;
|
||||
this.speciesStarterMoves = [];
|
||||
|
||||
if (species) {
|
||||
const dexEntry = globalScene.gameData.dexData[species.speciesId];
|
||||
|
||||
if (!dexEntry.caughtAttr) {
|
||||
const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)));
|
||||
|
||||
if (shiny === undefined || shiny !== props.shiny) {
|
||||
if (shiny === undefined) {
|
||||
shiny = props.shiny;
|
||||
}
|
||||
if (formIndex === undefined || formIndex !== props.formIndex) {
|
||||
if (formIndex === undefined) {
|
||||
formIndex = props.formIndex;
|
||||
}
|
||||
if (female === undefined || female !== props.female) {
|
||||
if (female === undefined) {
|
||||
female = props.female;
|
||||
}
|
||||
if (variant === undefined || variant !== props.variant) {
|
||||
if (variant === undefined) {
|
||||
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);
|
||||
this.assetLoadCancelled = assetLoadCancelled;
|
||||
|
||||
if (shouldUpdateSprite) {
|
||||
|
||||
species.loadAssets(female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct?
|
||||
if (assetLoadCancelled.value) {
|
||||
return;
|
||||
@ -1711,21 +1935,37 @@ export default class PokedexUiHandler extends MessageUiHandler {
|
||||
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
|
||||
|
||||
this.setTypeIcons(speciesForm.type1, speciesForm.type2);
|
||||
} else {
|
||||
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 {
|
||||
this.setTypeIcons(null, null);
|
||||
}
|
||||
|
||||
if (!this.starterMoveset) {
|
||||
this.starterMoveset = this.speciesStarterMoves.slice(0, 4) as StarterMoveset;
|
||||
}
|
||||
}
|
||||
|
||||
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.setModeWithoutClear(Mode.CONFIRM, () => {
|
||||
ui.setMode(Mode.POKEDEX, "refresh");
|
||||
globalScene.clearPhaseQueue();
|
||||
this.clearText();
|
||||
this.clear();
|
||||
ui.revertMode();
|
||||
|
@ -1981,8 +1981,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
female: starterAttributes.female
|
||||
};
|
||||
ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, starterAttributes.form, attributes);
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
options.push({
|
||||
|
@ -96,6 +96,9 @@ export default class SummaryUiHandler extends UiHandler {
|
||||
private friendshipText: Phaser.GameObjects.Text;
|
||||
private friendshipIcon: Phaser.GameObjects.Sprite;
|
||||
private friendshipOverlay: Phaser.GameObjects.Sprite;
|
||||
private permStatsContainer: Phaser.GameObjects.Container;
|
||||
private ivContainer: Phaser.GameObjects.Container;
|
||||
private statsContainer: Phaser.GameObjects.Container;
|
||||
|
||||
private descriptionScrollTween: Phaser.Tweens.Tween | null;
|
||||
private moveCursorBlinkTimer: Phaser.Time.TimerEvent | null;
|
||||
@ -535,6 +538,10 @@ export default class SummaryUiHandler extends UiHandler {
|
||||
this.passiveContainer.nameText?.setVisible(!this.passiveContainer.descriptionText?.visible);
|
||||
this.passiveContainer.descriptionText?.setVisible(!this.passiveContainer.descriptionText.visible);
|
||||
this.passiveContainer.labelImage.setVisible(!this.passiveContainer.labelImage.visible);
|
||||
} else if (this.cursor === Page.STATS) {
|
||||
//Show IVs
|
||||
this.permStatsContainer.setVisible(!this.permStatsContainer.visible);
|
||||
this.ivContainer.setVisible(!this.ivContainer.visible);
|
||||
}
|
||||
} else if (button === Button.CANCEL) {
|
||||
if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) {
|
||||
@ -878,8 +885,13 @@ export default class SummaryUiHandler extends UiHandler {
|
||||
profileContainer.add(memoText);
|
||||
break;
|
||||
case Page.STATS:
|
||||
const statsContainer = globalScene.add.container(0, -pageBg.height);
|
||||
pageContainer.add(statsContainer);
|
||||
this.statsContainer = globalScene.add.container(0, -pageBg.height);
|
||||
pageContainer.add(this.statsContainer);
|
||||
this.permStatsContainer = globalScene.add.container(27, 56);
|
||||
this.statsContainer.add(this.permStatsContainer);
|
||||
this.ivContainer = globalScene.add.container(27, 56);
|
||||
this.statsContainer.add(this.ivContainer);
|
||||
this.statsContainer.setVisible(true);
|
||||
|
||||
PERMANENT_STATS.forEach((stat, s) => {
|
||||
const statName = i18next.t(getStatKey(stat));
|
||||
@ -888,18 +900,27 @@ export default class SummaryUiHandler extends UiHandler {
|
||||
|
||||
const natureStatMultiplier = getNatureStatMultiplier(this.pokemon?.getNature()!, s); // TODO: is this bang correct?
|
||||
|
||||
const statLabel = addTextObject(27 + 115 * colIndex + (colIndex === 1 ? 5 : 0), 56 + 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE);
|
||||
const statLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE);
|
||||
const ivLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, this.pokemon?.ivs[stat] === 31 ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY);
|
||||
|
||||
statLabel.setOrigin(0.5, 0);
|
||||
statsContainer.add(statLabel);
|
||||
ivLabel.setOrigin(0.5, 0);
|
||||
this.permStatsContainer.add(statLabel);
|
||||
this.ivContainer.add(ivLabel);
|
||||
|
||||
const statValueText = stat !== Stat.HP
|
||||
? Utils.formatStat(this.pokemon?.getStat(stat)!) // TODO: is this bang correct?
|
||||
: `${Utils.formatStat(this.pokemon?.hp!, true)}/${Utils.formatStat(this.pokemon?.getMaxHp()!, true)}`; // TODO: are those bangs correct?
|
||||
const ivText = `${this.pokemon?.ivs[stat]}/31`;
|
||||
|
||||
const statValue = addTextObject(120 + 88 * colIndex, 56 + 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT);
|
||||
const statValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT);
|
||||
statValue.setOrigin(1, 0);
|
||||
statsContainer.add(statValue);
|
||||
this.permStatsContainer.add(statValue);
|
||||
const ivValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, ivText, TextStyle.WINDOW_ALT);
|
||||
ivValue.setOrigin(1, 0);
|
||||
this.ivContainer.add(ivValue);
|
||||
});
|
||||
this.ivContainer.setVisible(false);
|
||||
|
||||
const itemModifiers = (globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier
|
||||
&& m.pokemonId === this.pokemon?.id, this.playerParty) as PokemonHeldItemModifier[])
|
||||
@ -909,7 +930,7 @@ export default class SummaryUiHandler extends UiHandler {
|
||||
const icon = item.getIcon(true);
|
||||
|
||||
icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15);
|
||||
statsContainer.add(icon);
|
||||
this.statsContainer.add(icon);
|
||||
|
||||
icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains);
|
||||
icon.on("pointerover", () => globalScene.ui.showTooltip(item.type.name, item.type.getDescription(), true));
|
||||
@ -925,26 +946,26 @@ export default class SummaryUiHandler extends UiHandler {
|
||||
|
||||
const expLabel = addTextObject(6, 112, i18next.t("pokemonSummary:expPoints"), TextStyle.SUMMARY);
|
||||
expLabel.setOrigin(0, 0);
|
||||
statsContainer.add(expLabel);
|
||||
this.statsContainer.add(expLabel);
|
||||
|
||||
const nextLvExpLabel = addTextObject(6, 128, i18next.t("pokemonSummary:nextLv"), TextStyle.SUMMARY);
|
||||
nextLvExpLabel.setOrigin(0, 0);
|
||||
statsContainer.add(nextLvExpLabel);
|
||||
this.statsContainer.add(nextLvExpLabel);
|
||||
|
||||
const expText = addTextObject(208, 112, pkmExp.toString(), TextStyle.WINDOW_ALT);
|
||||
expText.setOrigin(1, 0);
|
||||
statsContainer.add(expText);
|
||||
this.statsContainer.add(expText);
|
||||
|
||||
const nextLvExp = pkmLvl < globalScene.getMaxExpLevel()
|
||||
? getLevelTotalExp(pkmLvl + 1, pkmSpeciesGrowthRate) - pkmExp
|
||||
: 0;
|
||||
const nextLvExpText = addTextObject(208, 128, nextLvExp.toString(), TextStyle.WINDOW_ALT);
|
||||
nextLvExpText.setOrigin(1, 0);
|
||||
statsContainer.add(nextLvExpText);
|
||||
this.statsContainer.add(nextLvExpText);
|
||||
|
||||
const expOverlay = globalScene.add.image(140, 145, "summary_stats_overlay_exp");
|
||||
expOverlay.setOrigin(0, 0);
|
||||
statsContainer.add(expOverlay);
|
||||
this.statsContainer.add(expOverlay);
|
||||
|
||||
const expMaskRect = globalScene.make.graphics({});
|
||||
expMaskRect.setScale(6);
|
||||
@ -955,6 +976,11 @@ export default class SummaryUiHandler extends UiHandler {
|
||||
const expMask = expMaskRect.createGeometryMask();
|
||||
|
||||
expOverlay.setMask(expMask);
|
||||
this.abilityPrompt = globalScene.add.image(0, 0, !globalScene.inputController?.gamepadSupport ? "summary_profile_prompt_z" : "summary_profile_prompt_a");
|
||||
this.abilityPrompt.setPosition(8, 47);
|
||||
this.abilityPrompt.setVisible(true);
|
||||
this.abilityPrompt.setOrigin(0, 0);
|
||||
this.statsContainer.add(this.abilityPrompt);
|
||||
break;
|
||||
case Page.MOVES:
|
||||
this.movesContainer = globalScene.add.container(5, -pageBg.height + 26);
|
||||
|