Merge branch 'beta' into supreme-overlord-implementation

This commit is contained in:
geeilhan 2025-02-11 01:26:53 +01:00 committed by GitHub
commit 0d338bb1b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 621 additions and 117 deletions

View File

@ -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/
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -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;
/**
* Saves the number of times a Pokemon on the enemy's side has fainted during this battle.

View File

@ -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
*/
@ -6065,8 +6103,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

View File

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

View File

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

View File

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

View File

@ -4356,8 +4356,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);

View File

@ -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", "");

View File

@ -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);
}

View File

@ -95,6 +95,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()) {

View File

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

View File

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

View File

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

View File

@ -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
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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 () => {

View File

@ -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 () => {

View File

@ -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");

View File

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

View File

@ -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);
});

View File

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

View File

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