Merge branch 'beta' into magic-bounce
@ -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 |
@ -101,6 +101,9 @@ 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;
|
||||
|
@ -2756,6 +2756,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
|
||||
*/
|
||||
@ -6075,8 +6113,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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -4571,7 +4571,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;
|
||||
}
|
||||
@ -11010,8 +11011,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),
|
||||
|
@ -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
|
||||
|
@ -887,16 +887,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) {
|
||||
|
@ -947,11 +947,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);
|
||||
@ -965,7 +968,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:
|
||||
@ -2487,9 +2490,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);
|
||||
|
||||
@ -2516,7 +2520,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;
|
||||
@ -4356,8 +4362,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);
|
||||
|
@ -249,9 +249,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", "");
|
||||
|
@ -1720,7 +1720,16 @@ const modifierPool: ModifierPool = {
|
||||
}, 4),
|
||||
new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3),
|
||||
new WeightedModifierType(modifierTypes.TERA_SHARD, 1),
|
||||
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;
|
||||
@ -1879,7 +1888,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;
|
||||
@ -2538,7 +2547,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);
|
||||
}
|
||||
|
@ -107,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()) {
|
||||
|
@ -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
|
||||
|
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
|
||||
});
|
||||
|
@ -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 () => {
|
||||
|
@ -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({
|
||||
@ -86,19 +87,86 @@ 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()!;
|
||||
playerPokemon.stats[Stat.ATK] = 100;
|
||||
playerPokemon.stats[Stat.SPATK] = 1;
|
||||
|
||||
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 () => {
|
||||
game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]);
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
@ -534,6 +537,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) {
|
||||
@ -877,8 +884,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));
|
||||
@ -887,18 +899,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[])
|
||||
@ -908,7 +929,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));
|
||||
@ -924,26 +945,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);
|
||||
@ -954,6 +975,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);
|
||||
|