Merge branch 'beta' into deep-sea-items

This commit is contained in:
damocleas 2025-01-15 22:25:29 -05:00 committed by GitHub
commit bb6ecb0afe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1367 additions and 485 deletions

Binary file not shown.

@ -1 +1 @@
Subproject commit 4928231e22a06dce2b55d9b04cd2b283c2ee4afb Subproject commit acad8499a4ca488a9871902de140f635235f309a

View File

@ -363,28 +363,30 @@ export default class BattleScene extends SceneBase {
/** /**
* Load the variant assets for the given sprite and stores them in {@linkcode variantColorCache} * Load the variant assets for the given sprite and stores them in {@linkcode variantColorCache}
*/ */
loadPokemonVariantAssets(spriteKey: string, fileRoot: string, variant?: Variant) { public async loadPokemonVariantAssets(spriteKey: string, fileRoot: string, variant?: Variant): Promise<void> {
const useExpSprite = this.experimentalSprites && this.hasExpSprite(spriteKey); const useExpSprite = this.experimentalSprites && this.hasExpSprite(spriteKey);
if (useExpSprite) { if (useExpSprite) {
fileRoot = `exp/${fileRoot}`; fileRoot = `exp/${fileRoot}`;
} }
let variantConfig = variantData; let variantConfig = variantData;
fileRoot.split("/").map(p => variantConfig ? variantConfig = variantConfig[p] : null); fileRoot.split("/").map((p) => (variantConfig ? (variantConfig = variantConfig[p]) : null));
const variantSet = variantConfig as VariantSet; const variantSet = variantConfig as VariantSet;
if (variantSet && (variant !== undefined && variantSet[variant] === 1)) {
const populateVariantColors = (key: string): Promise<void> => { return new Promise<void>((resolve) => {
return new Promise(resolve => { if (variantSet && variant !== undefined && variantSet[variant] === 1) {
if (variantColorCache.hasOwnProperty(key)) { if (variantColorCache.hasOwnProperty(spriteKey)) {
return resolve(); return resolve();
} }
this.cachedFetch(`./images/pokemon/variant/${fileRoot}.json`).then(res => res.json()).then(c => { this.cachedFetch(`./images/pokemon/variant/${fileRoot}.json`)
variantColorCache[key] = c; .then((res) => res.json())
.then((c) => {
variantColorCache[spriteKey] = c;
resolve(); resolve();
}); });
}); } else {
}; resolve();
populateVariantColors(spriteKey); }
} });
} }
async preload() { async preload() {
@ -1191,6 +1193,9 @@ export default class BattleScene extends SceneBase {
onComplete: () => { onComplete: () => {
this.clearPhaseQueue(); this.clearPhaseQueue();
this.ui.freeUIData();
this.uiContainer.remove(this.ui, true);
this.uiContainer.destroy();
this.children.removeAll(true); this.children.removeAll(true);
this.game.domContainer.innerHTML = ""; this.game.domContainer.innerHTML = "";
this.launchBattle(); this.launchBattle();
@ -1865,7 +1870,7 @@ export default class BattleScene extends SceneBase {
generateRandomBiome(waveIndex: integer): Biome { generateRandomBiome(waveIndex: integer): Biome {
const relWave = waveIndex % 250; const relWave = waveIndex % 250;
const biomes = Utils.getEnumValues(Biome).slice(1, Utils.getEnumValues(Biome).filter(b => b >= 40).length * -1); const biomes = Utils.getEnumValues(Biome).filter(b => b !== Biome.TOWN && b !== Biome.END);
const maxDepth = biomeDepths[Biome.END][0] - 2; const maxDepth = biomeDepths[Biome.END][0] - 2;
const depthWeights = new Array(maxDepth + 1).fill(null) const depthWeights = new Array(maxDepth + 1).fill(null)
.map((_, i: integer) => ((1 - Math.min(Math.abs((i / (maxDepth - 1)) - (relWave / 250)) + 0.25, 1)) / 0.75) * 250); .map((_, i: integer) => ((1 - Math.min(Math.abs((i / (maxDepth - 1)) - (relWave / 250)) + 0.25, 1)) / 0.75) * 250);
@ -1878,9 +1883,9 @@ export default class BattleScene extends SceneBase {
const randInt = Utils.randSeedInt(totalWeight); const randInt = Utils.randSeedInt(totalWeight);
for (const biome of biomes) { for (let i = 0; i < biomes.length; i++) {
if (randInt < biomeThresholds[biome]) { if (randInt < biomeThresholds[i]) {
return biome; return biomes[i];
} }
} }

View File

@ -7,7 +7,7 @@ import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/mod
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import { trainerConfigs } from "#app/data/trainer-config"; import { trainerConfigs } from "#app/data/trainer-config";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import type { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon"; import type { EnemyPokemon, PlayerPokemon, TurnMove } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec"; import { BattleSpec } from "#enums/battle-spec";
@ -45,12 +45,12 @@ export enum BattlerIndex {
} }
export interface TurnCommand { export interface TurnCommand {
command: Command; command: Command;
cursor?: number; cursor?: number;
move?: QueuedMove; move?: TurnMove;
targets?: BattlerIndex[]; targets?: BattlerIndex[];
skip?: boolean; skip?: boolean;
args?: any[]; args?: any[];
} }
export interface FaintLogEntry { export interface FaintLogEntry {

View File

@ -612,7 +612,7 @@ export class InterruptedTag extends BattlerTag {
super.onAdd(pokemon); super.onAdd(pokemon);
pokemon.getMoveQueue().shift(); pokemon.getMoveQueue().shift();
pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.OTHER }); pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.OTHER, targets: []});
} }
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {

View File

@ -88,6 +88,11 @@ export enum ChallengeType {
* Modifies what weight AI pokemon have when generating movesets. UNIMPLEMENTED. * Modifies what weight AI pokemon have when generating movesets. UNIMPLEMENTED.
*/ */
MOVE_WEIGHT, MOVE_WEIGHT,
/**
* Modifies what the pokemon stats for Flip Stat Mode.
*/
FLIP_STAT,
} }
/** /**
@ -405,6 +410,16 @@ export abstract class Challenge {
applyMoveWeight(pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, level: Utils.IntegerHolder): boolean { applyMoveWeight(pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, level: Utils.IntegerHolder): boolean {
return false; return false;
} }
/**
* An apply function for FlipStats. Derived classes should alter this.
* @param pokemon {@link Pokemon} What pokemon would learn the move.
* @param baseStats What are the stats to flip.
* @returns {@link boolean} Whether this function did anything.
*/
applyFlipStat(pokemon: Pokemon, baseStats: number[]) {
return false;
}
} }
type ChallengeCondition = (data: GameData) => boolean; type ChallengeCondition = (data: GameData) => boolean;
@ -705,6 +720,33 @@ export class InverseBattleChallenge extends Challenge {
} }
} }
/**
* Implements a flip stat challenge.
*/
export class FlipStatChallenge extends Challenge {
constructor() {
super(Challenges.FLIP_STAT, 1);
}
override applyFlipStat(pokemon: Pokemon, baseStats: number[]) {
const origStats = Utils.deepCopy(baseStats);
baseStats[0] = origStats[5];
baseStats[1] = origStats[4];
baseStats[2] = origStats[3];
baseStats[3] = origStats[2];
baseStats[4] = origStats[1];
baseStats[5] = origStats[0];
return true;
}
static loadChallenge(source: FlipStatChallenge | any): FlipStatChallenge {
const newChallenge = new FlipStatChallenge();
newChallenge.value = source.value;
newChallenge.severity = source.severity;
return newChallenge;
}
}
/** /**
* Lowers the amount of starter points available. * Lowers the amount of starter points available.
*/ */
@ -890,6 +932,9 @@ export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType
* @returns True if any challenge was successfully applied. * @returns True if any challenge was successfully applied.
*/ */
export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.MOVE_WEIGHT, pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, weight: Utils.IntegerHolder): boolean; export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.MOVE_WEIGHT, pokemon: Pokemon, moveSource: MoveSourceType, move: Moves, weight: Utils.IntegerHolder): boolean;
export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType.FLIP_STAT, pokemon: Pokemon, baseStats: number[]): boolean;
export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType, ...args: any[]): boolean { export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType, ...args: any[]): boolean {
let ret = false; let ret = false;
gameMode.challenges.forEach(c => { gameMode.challenges.forEach(c => {
@ -934,6 +979,9 @@ export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType
case ChallengeType.MOVE_WEIGHT: case ChallengeType.MOVE_WEIGHT:
ret ||= c.applyMoveWeight(args[0], args[1], args[2], args[3]); ret ||= c.applyMoveWeight(args[0], args[1], args[2], args[3]);
break; break;
case ChallengeType.FLIP_STAT:
ret ||= c.applyFlipStat(args[0], args[1]);
break;
} }
} }
}); });
@ -959,6 +1007,8 @@ export function copyChallenge(source: Challenge | any): Challenge {
return FreshStartChallenge.loadChallenge(source); return FreshStartChallenge.loadChallenge(source);
case Challenges.INVERSE_BATTLE: case Challenges.INVERSE_BATTLE:
return InverseBattleChallenge.loadChallenge(source); return InverseBattleChallenge.loadChallenge(source);
case Challenges.FLIP_STAT:
return FlipStatChallenge.loadChallenge(source);
} }
throw new Error("Unknown challenge copied"); throw new Error("Unknown challenge copied");
} }
@ -971,5 +1021,6 @@ export function initChallenges() {
new SingleTypeChallenge(), new SingleTypeChallenge(),
new FreshStartChallenge(), new FreshStartChallenge(),
new InverseBattleChallenge(), new InverseBattleChallenge(),
new FlipStatChallenge()
); );
} }

View File

@ -8,6 +8,7 @@ import type { PokemonSpeciesForm } from "#app/data/pokemon-species";
import PokemonSpecies, { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { Biome } from "#app/enums/biome";
export interface DailyRunConfig { export interface DailyRunConfig {
seed: integer; seed: integer;
@ -71,3 +72,76 @@ function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLeve
pokemon.destroy(); pokemon.destroy();
return starter; return starter;
} }
interface BiomeWeights {
[key: integer]: integer
}
// Initially weighted by amount of exits each biome has
// Town and End are set to 0 however
// And some other biomes were balanced +1/-1 based on average size of the total daily.
const dailyBiomeWeights: BiomeWeights = {
[Biome.CAVE]: 3,
[Biome.LAKE]: 3,
[Biome.PLAINS]: 3,
[Biome.SNOWY_FOREST]: 3,
[Biome.SWAMP]: 3, // 2 -> 3
[Biome.TALL_GRASS]: 3, // 2 -> 3
[Biome.ABYSS]: 2, // 3 -> 2
[Biome.RUINS]: 2,
[Biome.BADLANDS]: 2,
[Biome.BEACH]: 2,
[Biome.CONSTRUCTION_SITE]: 2,
[Biome.DESERT]: 2,
[Biome.DOJO]: 2, // 3 -> 2
[Biome.FACTORY]: 2,
[Biome.FAIRY_CAVE]: 2,
[Biome.FOREST]: 2,
[Biome.GRASS]: 2, // 1 -> 2
[Biome.MEADOW]: 2,
[Biome.MOUNTAIN]: 2, // 3 -> 2
[Biome.SEA]: 2,
[Biome.SEABED]: 2,
[Biome.SLUM]: 2,
[Biome.TEMPLE]: 2, // 3 -> 2
[Biome.VOLCANO]: 2,
[Biome.GRAVEYARD]: 1,
[Biome.ICE_CAVE]: 1,
[Biome.ISLAND]: 1,
[Biome.JUNGLE]: 1,
[Biome.LABORATORY]: 1,
[Biome.METROPOLIS]: 1,
[Biome.POWER_PLANT]: 1,
[Biome.SPACE]: 1,
[Biome.WASTELAND]: 1,
[Biome.TOWN]: 0,
[Biome.END]: 0,
};
export function getDailyStartingBiome(): Biome {
const biomes = Utils.getEnumValues(Biome).filter(b => b !== Biome.TOWN && b !== Biome.END);
let totalWeight = 0;
const biomeThresholds: integer[] = [];
for (const biome of biomes) {
// Keep track of the total weight
totalWeight += dailyBiomeWeights[biome];
// Keep track of each biomes cumulative weight
biomeThresholds.push(totalWeight);
}
const randInt = Utils.randSeedInt(totalWeight);
for (let i = 0; i < biomes.length; i++) {
if (randInt < biomeThresholds[i]) {
return biomes[i];
}
}
// Fallback in case something went wrong
return biomes[Utils.randSeedInt(biomes.length)];
}

File diff suppressed because it is too large Load Diff

View File

@ -516,8 +516,7 @@ export abstract class PokemonSpeciesForm {
globalScene.anims.get(spriteKey).frameRate = 10; globalScene.anims.get(spriteKey).frameRate = 10;
} }
const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, ""); const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, "");
globalScene.loadPokemonVariantAssets(spriteKey, spritePath, variant); globalScene.loadPokemonVariantAssets(spriteKey, spritePath, variant).then(() => resolve());
resolve();
}); });
if (startLoad) { if (startLoad) {
if (!globalScene.load.isLoading()) { if (!globalScene.load.isLoading()) {
@ -948,19 +947,6 @@ export class PokemonForm extends PokemonSpeciesForm {
} }
} }
export const noStarterFormKeys: string[] = [
SpeciesFormKey.MEGA,
SpeciesFormKey.MEGA_X,
SpeciesFormKey.MEGA_Y,
SpeciesFormKey.PRIMAL,
SpeciesFormKey.ORIGIN,
SpeciesFormKey.THERIAN,
SpeciesFormKey.GIGANTAMAX,
SpeciesFormKey.GIGANTAMAX_RAPID,
SpeciesFormKey.GIGANTAMAX_SINGLE,
SpeciesFormKey.ETERNAMAX
].map(k => k.toString());
/** /**
* Method to get the daily list of starters with Pokerus. * Method to get the daily list of starters with Pokerus.
* @returns A list of starters with Pokerus * @returns A list of starters with Pokerus
@ -1834,7 +1820,7 @@ export function initSpecies() {
new PokemonSpecies(Species.COFAGRIGUS, 5, false, false, false, "Coffin Pokémon", Type.GHOST, null, 1.7, 76.5, Abilities.MUMMY, Abilities.NONE, Abilities.NONE, 483, 58, 50, 145, 95, 105, 30, 90, 50, 169, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.COFAGRIGUS, 5, false, false, false, "Coffin Pokémon", Type.GHOST, null, 1.7, 76.5, Abilities.MUMMY, Abilities.NONE, Abilities.NONE, 483, 58, 50, 145, 95, 105, 30, 90, 50, 169, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.TIRTOUGA, 5, false, false, false, "Prototurtle Pokémon", Type.WATER, Type.ROCK, 0.7, 16.5, Abilities.SOLID_ROCK, Abilities.STURDY, Abilities.SWIFT_SWIM, 355, 54, 78, 103, 53, 45, 22, 45, 50, 71, GrowthRate.MEDIUM_FAST, 87.5, false), new PokemonSpecies(Species.TIRTOUGA, 5, false, false, false, "Prototurtle Pokémon", Type.WATER, Type.ROCK, 0.7, 16.5, Abilities.SOLID_ROCK, Abilities.STURDY, Abilities.SWIFT_SWIM, 355, 54, 78, 103, 53, 45, 22, 45, 50, 71, GrowthRate.MEDIUM_FAST, 87.5, false),
new PokemonSpecies(Species.CARRACOSTA, 5, false, false, false, "Prototurtle Pokémon", Type.WATER, Type.ROCK, 1.2, 81, Abilities.SOLID_ROCK, Abilities.STURDY, Abilities.SWIFT_SWIM, 495, 74, 108, 133, 83, 65, 32, 45, 50, 173, GrowthRate.MEDIUM_FAST, 87.5, false), new PokemonSpecies(Species.CARRACOSTA, 5, false, false, false, "Prototurtle Pokémon", Type.WATER, Type.ROCK, 1.2, 81, Abilities.SOLID_ROCK, Abilities.STURDY, Abilities.SWIFT_SWIM, 495, 74, 108, 133, 83, 65, 32, 45, 50, 173, GrowthRate.MEDIUM_FAST, 87.5, false),
new PokemonSpecies(Species.ARCHEN, 5, false, false, false, "First Bird Pokémon", Type.ROCK, Type.FLYING, 0.5, 9.5, Abilities.DEFEATIST, Abilities.NONE, Abilities.EMERGENCY_EXIT, 401, 55, 112, 45, 74, 45, 70, 45, 50, 71, GrowthRate.MEDIUM_FAST, 87.5, false), //Custom Hidden new PokemonSpecies(Species.ARCHEN, 5, false, false, false, "First Bird Pokémon", Type.ROCK, Type.FLYING, 0.5, 9.5, Abilities.DEFEATIST, Abilities.NONE, Abilities.WIMP_OUT, 401, 55, 112, 45, 74, 45, 70, 45, 50, 71, GrowthRate.MEDIUM_FAST, 87.5, false), //Custom Hidden
new PokemonSpecies(Species.ARCHEOPS, 5, false, false, false, "First Bird Pokémon", Type.ROCK, Type.FLYING, 1.4, 32, Abilities.DEFEATIST, Abilities.NONE, Abilities.EMERGENCY_EXIT, 567, 75, 140, 65, 112, 65, 110, 45, 50, 177, GrowthRate.MEDIUM_FAST, 87.5, false), //Custom Hidden new PokemonSpecies(Species.ARCHEOPS, 5, false, false, false, "First Bird Pokémon", Type.ROCK, Type.FLYING, 1.4, 32, Abilities.DEFEATIST, Abilities.NONE, Abilities.EMERGENCY_EXIT, 567, 75, 140, 65, 112, 65, 110, 45, 50, 177, GrowthRate.MEDIUM_FAST, 87.5, false), //Custom Hidden
new PokemonSpecies(Species.TRUBBISH, 5, false, false, false, "Trash Bag Pokémon", Type.POISON, null, 0.6, 31, Abilities.STENCH, Abilities.STICKY_HOLD, Abilities.AFTERMATH, 329, 50, 50, 62, 40, 62, 65, 190, 50, 66, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.TRUBBISH, 5, false, false, false, "Trash Bag Pokémon", Type.POISON, null, 0.6, 31, Abilities.STENCH, Abilities.STICKY_HOLD, Abilities.AFTERMATH, 329, 50, 50, 62, 40, 62, 65, 190, 50, 66, GrowthRate.MEDIUM_FAST, 50, false),
new PokemonSpecies(Species.GARBODOR, 5, false, false, false, "Trash Heap Pokémon", Type.POISON, null, 1.9, 107.3, Abilities.STENCH, Abilities.WEAK_ARMOR, Abilities.AFTERMATH, 474, 80, 95, 82, 60, 82, 75, 60, 50, 166, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonSpecies(Species.GARBODOR, 5, false, false, false, "Trash Heap Pokémon", Type.POISON, null, 1.9, 107.3, Abilities.STENCH, Abilities.WEAK_ARMOR, Abilities.AFTERMATH, 474, 80, 95, 82, 60, 82, 75, 60, 50, 166, GrowthRate.MEDIUM_FAST, 50, false, true,

View File

@ -5,4 +5,5 @@ export enum Challenges {
LOWER_STARTER_POINTS, LOWER_STARTER_POINTS,
FRESH_START, FRESH_START,
INVERSE_BATTLE, INVERSE_BATTLE,
FLIP_STAT,
} }

View File

@ -215,11 +215,12 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
resolve(); resolve();
} }
const shinyPromises: Promise<void>[] = [];
this.spriteConfigs.forEach((config) => { this.spriteConfigs.forEach((config) => {
if (config.isPokemon) { if (config.isPokemon) {
globalScene.loadPokemonAtlas(config.spriteKey, config.fileRoot); globalScene.loadPokemonAtlas(config.spriteKey, config.fileRoot);
if (config.isShiny) { if (config.isShiny) {
globalScene.loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant); shinyPromises.push(globalScene.loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant));
} }
} else if (config.isItem) { } else if (config.isItem) {
globalScene.loadAtlas("items", ""); globalScene.loadAtlas("items", "");
@ -254,7 +255,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
return true; return true;
}); });
resolve(); Promise.all(shinyPromises).then(() => resolve());
}); });
if (!globalScene.load.isLoading()) { if (!globalScene.load.isLoading()) {

View File

@ -1057,6 +1057,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
calculateBaseStats(): number[] { calculateBaseStats(): number[] {
const baseStats = this.getSpeciesForm(true).baseStats.slice(0); const baseStats = this.getSpeciesForm(true).baseStats.slice(0);
applyChallenges(globalScene.gameMode, ChallengeType.FLIP_STAT, this, baseStats);
// Shuckle Juice // Shuckle Juice
globalScene.applyModifiers(PokemonBaseStatTotalModifier, this.isPlayer(), this, baseStats); globalScene.applyModifiers(PokemonBaseStatTotalModifier, this.isPlayer(), this, baseStats);
// Old Gateau // Old Gateau
@ -3298,7 +3299,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
getMoveQueue(): QueuedMove[] { getMoveQueue(): TurnMove[] {
return this.summonData.moveQueue; return this.summonData.moveQueue;
} }
@ -4810,17 +4811,19 @@ export class EnemyPokemon extends Pokemon {
* the Pokemon the move will target. * the Pokemon the move will target.
* @returns this Pokemon's next move in the format {move, moveTargets} * @returns this Pokemon's next move in the format {move, moveTargets}
*/ */
getNextMove(): QueuedMove { getNextMove(): TurnMove {
// If this Pokemon has a move already queued, return it. // If this Pokemon has a move already queued, return it.
const queuedMove = this.getMoveQueue().length const moveQueue = this.getMoveQueue();
? this.getMoveset().find(m => m?.moveId === this.getMoveQueue()[0].move) if (moveQueue.length !== 0) {
: null; const queuedMove = moveQueue[0];
if (queuedMove) { if (queuedMove) {
if (queuedMove.isUsable(this, this.getMoveQueue()[0].ignorePP)) { const moveIndex = this.getMoveset().findIndex(m => m?.moveId === queuedMove.move);
return { move: queuedMove.moveId, targets: this.getMoveQueue()[0].targets, ignorePP: this.getMoveQueue()[0].ignorePP }; if ((moveIndex > -1 && this.getMoveset()[moveIndex]!.isUsable(this, queuedMove.ignorePP)) || queuedMove.virtual) {
} else { return queuedMove;
this.getMoveQueue().shift(); } else {
return this.getNextMove(); this.getMoveQueue().shift();
return this.getNextMove();
}
} }
} }
@ -5242,15 +5245,10 @@ export class EnemyPokemon extends Pokemon {
export interface TurnMove { export interface TurnMove {
move: Moves; move: Moves;
targets?: BattlerIndex[]; targets: BattlerIndex[];
result: MoveResult; result?: MoveResult;
virtual?: boolean; virtual?: boolean;
turn?: number; turn?: number;
}
export interface QueuedMove {
move: Moves;
targets: BattlerIndex[];
ignorePP?: boolean; ignorePP?: boolean;
} }
@ -5266,7 +5264,7 @@ export interface AttackMoveResult {
export class PokemonSummonData { export class PokemonSummonData {
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */ /** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ]; public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
public moveQueue: QueuedMove[] = []; public moveQueue: TurnMove[] = [];
public tags: BattlerTag[] = []; public tags: BattlerTag[] = [];
public abilitySuppressed: boolean = false; public abilitySuppressed: boolean = false;
public abilitiesApplied: Abilities[] = []; public abilitiesApplied: Abilities[] = [];

View File

@ -428,7 +428,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
} }
// Prompts reroll of party member species if species already present in the enemy party // Prompts reroll of party member species if species already present in the enemy party
if (this.checkDuplicateSpecies(ret, baseSpecies)) { if (this.checkDuplicateSpecies(baseSpecies.speciesId)) {
console.log("Duplicate species detected, prompting reroll..."); console.log("Duplicate species detected, prompting reroll...");
retry = true; retry = true;
} }
@ -443,17 +443,23 @@ export default class Trainer extends Phaser.GameObjects.Container {
/** /**
* Checks if the enemy trainer already has the Pokemon species in their party * Checks if the enemy trainer already has the Pokemon species in their party
* @param {PokemonSpecies} species {@linkcode PokemonSpecies} * @param baseSpecies - The base {@linkcode Species} of the current Pokemon
* @param {PokemonSpecies} baseSpecies {@linkcode PokemonSpecies} - baseSpecies of the Pokemon if species is forced to evolve
* @returns `true` if the species is already present in the party * @returns `true` if the species is already present in the party
*/ */
checkDuplicateSpecies(species: PokemonSpecies, baseSpecies: PokemonSpecies): boolean { checkDuplicateSpecies(baseSpecies: Species): boolean {
const staticPartyPokemon = (signatureSpecies[TrainerType[this.config.trainerType]] ?? []).flat(1); const staticSpecies = (signatureSpecies[TrainerType[this.config.trainerType]] ?? []).flat(1).map(s => {
let root = s;
const currentPartySpecies = globalScene.getEnemyParty().map(p => { while (pokemonPrevolutions.hasOwnProperty(root)) {
return p.species.speciesId; root = pokemonPrevolutions[root];
}
return root;
}); });
return currentPartySpecies.includes(species.speciesId) || staticPartyPokemon.includes(baseSpecies.speciesId);
const currentSpecies = globalScene.getEnemyParty().map(p => {
return p.species.getRootSpeciesId();
});
return currentSpecies.includes(baseSpecies) || staticSpecies.includes(baseSpecies);
} }
getPartyMemberMatchupScores(trainerSlot: TrainerSlot = TrainerSlot.NONE, forSwitch: boolean = false): [integer, integer][] { getPartyMemberMatchupScores(trainerSlot: TrainerSlot = TrainerSlot.NONE, forSwitch: boolean = false): [integer, integer][] {

View File

@ -12,6 +12,7 @@ import { Biome } from "#enums/biome";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Challenges } from "./enums/challenges"; import { Challenges } from "./enums/challenges";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getDailyStartingBiome } from "./data/daily-run";
export enum GameModes { export enum GameModes {
CLASSIC, CLASSIC,
@ -120,7 +121,7 @@ export class GameMode implements GameModeConfig {
getStartingBiome(): Biome { getStartingBiome(): Biome {
switch (this.modeId) { switch (this.modeId) {
case GameModes.DAILY: case GameModes.DAILY:
return globalScene.generateRandomBiome(this.getWaveForDifficulty(1)); return getDailyStartingBiome();
default: default:
return Overrides.STARTING_BIOME_OVERRIDE || Biome.TOWN; return Overrides.STARTING_BIOME_OVERRIDE || Biome.TOWN;
} }

View File

@ -319,6 +319,7 @@ export class LoadingScene extends SceneBase {
this.loadSe("pb_move"); this.loadSe("pb_move");
this.loadSe("pb_catch"); this.loadSe("pb_catch");
this.loadSe("pb_lock"); this.loadSe("pb_lock");
this.loadSe("crit_throw");
this.loadSe("pb_tray_enter"); this.loadSe("pb_tray_enter");
this.loadSe("pb_tray_ball"); this.loadSe("pb_tray_ball");

View File

@ -64,7 +64,7 @@ export class AttemptCapturePhase extends PokemonPhase {
this.pokeball.setOrigin(0.5, 0.625); this.pokeball.setOrigin(0.5, 0.625);
globalScene.field.add(this.pokeball); globalScene.field.add(this.pokeball);
globalScene.playSound("se/pb_throw", isCritical ? { rate: 0.2 } : undefined); // Crit catch throws are higher pitched globalScene.playSound(isCritical ? "se/crit_throw" : "se/pb_throw");
globalScene.time.delayedCall(300, () => { globalScene.time.delayedCall(300, () => {
globalScene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon); globalScene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon);
}); });

View File

@ -11,7 +11,7 @@ import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Biome } from "#app/enums/biome"; import { Biome } from "#app/enums/biome";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon, TurnMove } from "#app/field/pokemon";
import { FieldPosition } from "#app/field/pokemon"; import { FieldPosition } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { Command } from "#app/ui/command-ui-handler"; import { Command } from "#app/ui/command-ui-handler";
@ -86,19 +86,19 @@ export class CommandPhase extends FieldPhase {
const moveQueue = playerPokemon.getMoveQueue(); const moveQueue = playerPokemon.getMoveQueue();
while (moveQueue.length && moveQueue[0] while (moveQueue.length && moveQueue[0]
&& moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move) && moveQueue[0].move && !moveQueue[0].virtual && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move)
|| !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move)]!.isUsable(playerPokemon, moveQueue[0].ignorePP))) { // TODO: is the bang correct? || !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move)]!.isUsable(playerPokemon, moveQueue[0].ignorePP))) { // TODO: is the bang correct?
moveQueue.shift(); moveQueue.shift();
} }
if (moveQueue.length) { if (moveQueue.length > 0) {
const queuedMove = moveQueue[0]; const queuedMove = moveQueue[0];
if (!queuedMove.move) { if (!queuedMove.move) {
this.handleCommand(Command.FIGHT, -1, false); this.handleCommand(Command.FIGHT, -1);
} else { } else {
const moveIndex = playerPokemon.getMoveset().findIndex(m => m?.moveId === queuedMove.move); const moveIndex = playerPokemon.getMoveset().findIndex(m => m?.moveId === queuedMove.move);
if (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex]!.isUsable(playerPokemon, queuedMove.ignorePP)) { // TODO: is the bang correct? if ((moveIndex > -1 && playerPokemon.getMoveset()[moveIndex]!.isUsable(playerPokemon, queuedMove.ignorePP)) || queuedMove.virtual) { // TODO: is the bang correct?
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, { targets: queuedMove.targets, multiple: queuedMove.targets.length > 1 }); this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, queuedMove);
} else { } else {
globalScene.ui.setMode(Mode.COMMAND, this.fieldIndex); globalScene.ui.setMode(Mode.COMMAND, this.fieldIndex);
} }
@ -120,12 +120,24 @@ export class CommandPhase extends FieldPhase {
switch (command) { switch (command) {
case Command.FIGHT: case Command.FIGHT:
let useStruggle = false; let useStruggle = false;
const turnMove: TurnMove | undefined = (args.length === 2 ? (args[1] as TurnMove) : undefined);
if (cursor === -1 || if (cursor === -1 ||
playerPokemon.trySelectMove(cursor, args[0] as boolean) || playerPokemon.trySelectMove(cursor, args[0] as boolean) ||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m?.isUsable(playerPokemon)).length)) { (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m?.isUsable(playerPokemon)).length)) {
const moveId = !useStruggle ? cursor > -1 ? playerPokemon.getMoveset()[cursor]!.moveId : Moves.NONE : Moves.STRUGGLE; // TODO: is the bang correct?
let moveId: Moves;
if (useStruggle) {
moveId = Moves.STRUGGLE;
} else if (turnMove !== undefined) {
moveId = turnMove.move;
} else if (cursor > -1) {
moveId = playerPokemon.getMoveset()[cursor]!.moveId;
} else {
moveId = Moves.NONE;
}
const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args }; const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args };
const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, moveId) : args[2]; const moveTargets: MoveTargetSet = turnMove === undefined ? getMoveTargets(playerPokemon, moveId) : { targets: turnMove.targets, multiple: turnMove.targets.length > 1 };
if (!moveId) { if (!moveId) {
turnCommand.targets = [ this.fieldIndex ]; turnCommand.targets = [ this.fieldIndex ];
} }

View File

@ -296,11 +296,6 @@ export class MovePhase extends BattlePhase {
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed)); globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed));
} }
// Update the battle's "last move" pointer, unless we're currently mimicking a move.
if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
globalScene.currentBattle.lastMove = this.move.moveId;
}
/** /**
* Determine if the move is successful (meaning that its damage/effects can be attempted) * Determine if the move is successful (meaning that its damage/effects can be attempted)
* by checking that all of the following are true: * by checking that all of the following are true:
@ -324,6 +319,14 @@ export class MovePhase extends BattlePhase {
const success = passesConditions && !failedDueToWeather && !failedDueToTerrain; const success = passesConditions && !failedDueToWeather && !failedDueToTerrain;
// Update the battle's "last move" pointer, unless we're currently mimicking a move.
if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
// The last move used is unaffected by moves that fail
if (success) {
globalScene.currentBattle.lastMove = this.move.moveId;
}
}
/** /**
* If the move has not failed, trigger ability-based user type changes and then execute it. * If the move has not failed, trigger ability-based user type changes and then execute it.
* *
@ -518,7 +521,7 @@ export class MovePhase extends BattlePhase {
frenzyMissFunc(this.pokemon, this.move.getMove()); frenzyMissFunc(this.pokemon, this.move.getMove());
} }
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL, targets: this.targets });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);

View File

@ -5,7 +5,7 @@ import i18next from "i18next";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import type { Challenge } from "#app/data/challenge"; import type { Challenge } from "#app/data/challenge";
import { FreshStartChallenge, SingleGenerationChallenge, SingleTypeChallenge, InverseBattleChallenge } from "#app/data/challenge"; import { FlipStatChallenge, FreshStartChallenge, SingleGenerationChallenge, SingleTypeChallenge, InverseBattleChallenge } from "#app/data/challenge";
import type { ConditionFn } from "#app/@types/common"; import type { ConditionFn } from "#app/@types/common";
import { Stat, getShortenedStatKey } from "#app/enums/stat"; import { Stat, getShortenedStatKey } from "#app/enums/stat";
import { Challenges } from "#app/enums/challenges"; import { Challenges } from "#app/enums/challenges";
@ -280,6 +280,10 @@ export function getAchievementDescription(localizationKey: string): string {
return i18next.t("achv:FRESH_START.description", { context: genderStr }); return i18next.t("achv:FRESH_START.description", { context: genderStr });
case "INVERSE_BATTLE": case "INVERSE_BATTLE":
return i18next.t("achv:INVERSE_BATTLE.description", { context: genderStr }); return i18next.t("achv:INVERSE_BATTLE.description", { context: genderStr });
case "FLIP_STATS":
return i18next.t("achv:FLIP_STATS.description", { context: genderStr });
case "FLIP_INVERSE":
return i18next.t("achv:FLIP_INVERSE.description", { context: genderStr });
case "BREEDERS_IN_SPACE": case "BREEDERS_IN_SPACE":
return i18next.t("achv:BREEDERS_IN_SPACE.description", { context: genderStr }); return i18next.t("achv:BREEDERS_IN_SPACE.description", { context: genderStr });
default: default:
@ -288,6 +292,7 @@ export function getAchievementDescription(localizationKey: string): string {
} }
export const achvs = { export const achvs = {
_10K_MONEY: new MoneyAchv("10K_MONEY", "", 10000, "nugget", 10), _10K_MONEY: new MoneyAchv("10K_MONEY", "", 10000, "nugget", 10),
_100K_MONEY: new MoneyAchv("100K_MONEY", "", 100000, "big_nugget", 25).setSecret(true), _100K_MONEY: new MoneyAchv("100K_MONEY", "", 100000, "big_nugget", 25).setSecret(true),
@ -330,35 +335,37 @@ export const achvs = {
PERFECT_IVS: new Achv("PERFECT_IVS", "", "PERFECT_IVS.description", "blunder_policy", 100), PERFECT_IVS: new Achv("PERFECT_IVS", "", "PERFECT_IVS.description", "blunder_policy", 100),
CLASSIC_VICTORY: new Achv("CLASSIC_VICTORY", "", "CLASSIC_VICTORY.description", "relic_crown", 150, (_) => globalScene.gameData.gameStats.sessionsWon === 0), CLASSIC_VICTORY: new Achv("CLASSIC_VICTORY", "", "CLASSIC_VICTORY.description", "relic_crown", 150, (_) => globalScene.gameData.gameStats.sessionsWon === 0),
UNEVOLVED_CLASSIC_VICTORY: new Achv("UNEVOLVED_CLASSIC_VICTORY", "", "UNEVOLVED_CLASSIC_VICTORY.description", "eviolite", 175, (_) => globalScene.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)), UNEVOLVED_CLASSIC_VICTORY: new Achv("UNEVOLVED_CLASSIC_VICTORY", "", "UNEVOLVED_CLASSIC_VICTORY.description", "eviolite", 175, (_) => globalScene.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)),
MONO_GEN_ONE_VICTORY: new ChallengeAchv("MONO_GEN_ONE", "", "MONO_GEN_ONE.description", "ribbon_gen1", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 1 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_ONE_VICTORY: new ChallengeAchv("MONO_GEN_ONE", "", "MONO_GEN_ONE.description", "ribbon_gen1", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 1 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GEN_TWO_VICTORY: new ChallengeAchv("MONO_GEN_TWO", "", "MONO_GEN_TWO.description", "ribbon_gen2", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 2 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_TWO_VICTORY: new ChallengeAchv("MONO_GEN_TWO", "", "MONO_GEN_TWO.description", "ribbon_gen2", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 2 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GEN_THREE_VICTORY: new ChallengeAchv("MONO_GEN_THREE", "", "MONO_GEN_THREE.description", "ribbon_gen3", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 3 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_THREE_VICTORY: new ChallengeAchv("MONO_GEN_THREE", "", "MONO_GEN_THREE.description", "ribbon_gen3", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 3 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GEN_FOUR_VICTORY: new ChallengeAchv("MONO_GEN_FOUR", "", "MONO_GEN_FOUR.description", "ribbon_gen4", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 4 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_FOUR_VICTORY: new ChallengeAchv("MONO_GEN_FOUR", "", "MONO_GEN_FOUR.description", "ribbon_gen4", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 4 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GEN_FIVE_VICTORY: new ChallengeAchv("MONO_GEN_FIVE", "", "MONO_GEN_FIVE.description", "ribbon_gen5", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 5 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_FIVE_VICTORY: new ChallengeAchv("MONO_GEN_FIVE", "", "MONO_GEN_FIVE.description", "ribbon_gen5", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 5 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GEN_SIX_VICTORY: new ChallengeAchv("MONO_GEN_SIX", "", "MONO_GEN_SIX.description", "ribbon_gen6", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 6 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_SIX_VICTORY: new ChallengeAchv("MONO_GEN_SIX", "", "MONO_GEN_SIX.description", "ribbon_gen6", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 6 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GEN_SEVEN_VICTORY: new ChallengeAchv("MONO_GEN_SEVEN", "", "MONO_GEN_SEVEN.description", "ribbon_gen7", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 7 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_SEVEN_VICTORY: new ChallengeAchv("MONO_GEN_SEVEN", "", "MONO_GEN_SEVEN.description", "ribbon_gen7", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 7 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GEN_EIGHT_VICTORY: new ChallengeAchv("MONO_GEN_EIGHT", "", "MONO_GEN_EIGHT.description", "ribbon_gen8", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 8 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_EIGHT_VICTORY: new ChallengeAchv("MONO_GEN_EIGHT", "", "MONO_GEN_EIGHT.description", "ribbon_gen8", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 8 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GEN_NINE_VICTORY: new ChallengeAchv("MONO_GEN_NINE", "", "MONO_GEN_NINE.description", "ribbon_gen9", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 9 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GEN_NINE_VICTORY: new ChallengeAchv("MONO_GEN_NINE", "", "MONO_GEN_NINE.description", "ribbon_gen9", 100, (c) => c instanceof SingleGenerationChallenge && c.value === 9 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_NORMAL: new ChallengeAchv("MONO_NORMAL", "", "MONO_NORMAL.description", "silk_scarf", 100, (c) => c instanceof SingleTypeChallenge && c.value === 1 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_NORMAL: new ChallengeAchv("MONO_NORMAL", "", "MONO_NORMAL.description", "silk_scarf", 100, (c) => c instanceof SingleTypeChallenge && c.value === 1 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_FIGHTING: new ChallengeAchv("MONO_FIGHTING", "", "MONO_FIGHTING.description", "black_belt", 100, (c) => c instanceof SingleTypeChallenge && c.value === 2 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_FIGHTING: new ChallengeAchv("MONO_FIGHTING", "", "MONO_FIGHTING.description", "black_belt", 100, (c) => c instanceof SingleTypeChallenge && c.value === 2 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_FLYING: new ChallengeAchv("MONO_FLYING", "", "MONO_FLYING.description", "sharp_beak", 100, (c) => c instanceof SingleTypeChallenge && c.value === 3 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_FLYING: new ChallengeAchv("MONO_FLYING", "", "MONO_FLYING.description", "sharp_beak", 100, (c) => c instanceof SingleTypeChallenge && c.value === 3 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_POISON: new ChallengeAchv("MONO_POISON", "", "MONO_POISON.description", "poison_barb", 100, (c) => c instanceof SingleTypeChallenge && c.value === 4 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_POISON: new ChallengeAchv("MONO_POISON", "", "MONO_POISON.description", "poison_barb", 100, (c) => c instanceof SingleTypeChallenge && c.value === 4 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GROUND: new ChallengeAchv("MONO_GROUND", "", "MONO_GROUND.description", "soft_sand", 100, (c) => c instanceof SingleTypeChallenge && c.value === 5 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GROUND: new ChallengeAchv("MONO_GROUND", "", "MONO_GROUND.description", "soft_sand", 100, (c) => c instanceof SingleTypeChallenge && c.value === 5 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_ROCK: new ChallengeAchv("MONO_ROCK", "", "MONO_ROCK.description", "hard_stone", 100, (c) => c instanceof SingleTypeChallenge && c.value === 6 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_ROCK: new ChallengeAchv("MONO_ROCK", "", "MONO_ROCK.description", "hard_stone", 100, (c) => c instanceof SingleTypeChallenge && c.value === 6 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_BUG: new ChallengeAchv("MONO_BUG", "", "MONO_BUG.description", "silver_powder", 100, (c) => c instanceof SingleTypeChallenge && c.value === 7 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_BUG: new ChallengeAchv("MONO_BUG", "", "MONO_BUG.description", "silver_powder", 100, (c) => c instanceof SingleTypeChallenge && c.value === 7 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GHOST: new ChallengeAchv("MONO_GHOST", "", "MONO_GHOST.description", "spell_tag", 100, (c) => c instanceof SingleTypeChallenge && c.value === 8 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GHOST: new ChallengeAchv("MONO_GHOST", "", "MONO_GHOST.description", "spell_tag", 100, (c) => c instanceof SingleTypeChallenge && c.value === 8 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_STEEL: new ChallengeAchv("MONO_STEEL", "", "MONO_STEEL.description", "metal_coat", 100, (c) => c instanceof SingleTypeChallenge && c.value === 9 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_STEEL: new ChallengeAchv("MONO_STEEL", "", "MONO_STEEL.description", "metal_coat", 100, (c) => c instanceof SingleTypeChallenge && c.value === 9 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_FIRE: new ChallengeAchv("MONO_FIRE", "", "MONO_FIRE.description", "charcoal", 100, (c) => c instanceof SingleTypeChallenge && c.value === 10 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_FIRE: new ChallengeAchv("MONO_FIRE", "", "MONO_FIRE.description", "charcoal", 100, (c) => c instanceof SingleTypeChallenge && c.value === 10 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_WATER: new ChallengeAchv("MONO_WATER", "", "MONO_WATER.description", "mystic_water", 100, (c) => c instanceof SingleTypeChallenge && c.value === 11 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_WATER: new ChallengeAchv("MONO_WATER", "", "MONO_WATER.description", "mystic_water", 100, (c) => c instanceof SingleTypeChallenge && c.value === 11 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_GRASS: new ChallengeAchv("MONO_GRASS", "", "MONO_GRASS.description", "miracle_seed", 100, (c) => c instanceof SingleTypeChallenge && c.value === 12 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_GRASS: new ChallengeAchv("MONO_GRASS", "", "MONO_GRASS.description", "miracle_seed", 100, (c) => c instanceof SingleTypeChallenge && c.value === 12 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_ELECTRIC: new ChallengeAchv("MONO_ELECTRIC", "", "MONO_ELECTRIC.description", "magnet", 100, (c) => c instanceof SingleTypeChallenge && c.value === 13 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_ELECTRIC: new ChallengeAchv("MONO_ELECTRIC", "", "MONO_ELECTRIC.description", "magnet", 100, (c) => c instanceof SingleTypeChallenge && c.value === 13 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_PSYCHIC: new ChallengeAchv("MONO_PSYCHIC", "", "MONO_PSYCHIC.description", "twisted_spoon", 100, (c) => c instanceof SingleTypeChallenge && c.value === 14 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_PSYCHIC: new ChallengeAchv("MONO_PSYCHIC", "", "MONO_PSYCHIC.description", "twisted_spoon", 100, (c) => c instanceof SingleTypeChallenge && c.value === 14 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_ICE: new ChallengeAchv("MONO_ICE", "", "MONO_ICE.description", "never_melt_ice", 100, (c) => c instanceof SingleTypeChallenge && c.value === 15 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_ICE: new ChallengeAchv("MONO_ICE", "", "MONO_ICE.description", "never_melt_ice", 100, (c) => c instanceof SingleTypeChallenge && c.value === 15 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_DRAGON: new ChallengeAchv("MONO_DRAGON", "", "MONO_DRAGON.description", "dragon_fang", 100, (c) => c instanceof SingleTypeChallenge && c.value === 16 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_DRAGON: new ChallengeAchv("MONO_DRAGON", "", "MONO_DRAGON.description", "dragon_fang", 100, (c) => c instanceof SingleTypeChallenge && c.value === 16 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_DARK: new ChallengeAchv("MONO_DARK", "", "MONO_DARK.description", "black_glasses", 100, (c) => c instanceof SingleTypeChallenge && c.value === 17 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_DARK: new ChallengeAchv("MONO_DARK", "", "MONO_DARK.description", "black_glasses", 100, (c) => c instanceof SingleTypeChallenge && c.value === 17 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c) => c instanceof SingleTypeChallenge && c.value === 18 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c) => c instanceof SingleTypeChallenge && c.value === 18 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c) => c instanceof FreshStartChallenge && c.value > 0 && !globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c) => c instanceof FreshStartChallenge && c.value > 0 && !globalScene.gameMode.challenges.some(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0), INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, (c) => c instanceof InverseBattleChallenge && c.value > 0),
FLIP_STATS: new ChallengeAchv("FLIP_STATS", "", "FLIP_STATS.description", "dubious_disc", 100, (c) => c instanceof FlipStatChallenge && c.value > 0),
FLIP_INVERSE: new ChallengeAchv("FLIP_INVERSE", "", "FLIP_INVERSE.description", "cracked_pot", 100, (c) => c instanceof FlipStatChallenge && c.value > 0 && globalScene.gameMode.challenges.every(c => [ Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT ].includes(c.id) && c.value > 0)),
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(), BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(),
}; };

View File

@ -6,7 +6,7 @@ import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { allSpecies, getPokemonSpecies, noStarterFormKeys } from "#app/data/pokemon-species"; import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
@ -1619,9 +1619,6 @@ export class GameData {
const dexEntry = this.dexData[species.speciesId]; const dexEntry = this.dexData[species.speciesId];
const caughtAttr = dexEntry.caughtAttr; const caughtAttr = dexEntry.caughtAttr;
const formIndex = pokemon.formIndex; const formIndex = pokemon.formIndex;
if (noStarterFormKeys.includes(pokemon.getFormKey())) {
pokemon.formIndex = 0;
}
const dexAttr = pokemon.getDexAttr(); const dexAttr = pokemon.getDexAttr();
pokemon.formIndex = formIndex; pokemon.formIndex = formIndex;

View File

@ -0,0 +1,105 @@
import { BattlerIndex } from "#app/battle";
import { Stat } from "#app/enums/stat";
import { MoveResult } from "#app/field/pokemon";
import { CommandPhase } from "#app/phases/command-phase";
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";
describe("Moves - Assist", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
// Manual moveset overrides are required for the player pokemon in these tests
// because the normal moveset override doesn't allow for accurate testing of moveset changes
game.override
.ability(Abilities.BALL_FETCH)
.battleType("double")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should only use an ally's moves", async () => {
game.override.enemyMoveset(Moves.SWORDS_DANCE);
await game.classicMode.startBattle([ Species.FEEBAS, Species.SHUCKLE ]);
const [ feebas, shuckle ] = game.scene.getPlayerField();
// These are all moves Assist cannot call; Sketch will be used to test that it can call other moves properly
game.move.changeMoveset(feebas, [ Moves.ASSIST, Moves.SKETCH, Moves.PROTECT, Moves.DRAGON_TAIL ]);
game.move.changeMoveset(shuckle, [ Moves.ASSIST, Moves.SKETCH, Moves.PROTECT, Moves.DRAGON_TAIL ]);
game.move.select(Moves.ASSIST, 0);
game.move.select(Moves.SKETCH, 1);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER ]);
// Player_2 uses Sketch, copies Swords Dance, Player_1 uses Assist, uses Player_2's Sketched Swords Dance
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(2); // Stat raised from Assist -> Swords Dance
});
it("should fail if there are no allies", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const feebas = game.scene.getPlayerPokemon()!;
game.move.changeMoveset(feebas, [ Moves.ASSIST, Moves.SKETCH, Moves.PROTECT, Moves.DRAGON_TAIL ]);
game.move.select(Moves.ASSIST, 0);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should fail if ally has no usable moves and user has usable moves", async () => {
game.override.enemyMoveset(Moves.SWORDS_DANCE);
await game.classicMode.startBattle([ Species.FEEBAS, Species.SHUCKLE ]);
const [ feebas, shuckle ] = game.scene.getPlayerField();
game.move.changeMoveset(feebas, [ Moves.ASSIST, Moves.SKETCH, Moves.PROTECT, Moves.DRAGON_TAIL ]);
game.move.changeMoveset(shuckle, [ Moves.ASSIST, Moves.SKETCH, Moves.PROTECT, Moves.DRAGON_TAIL ]);
game.move.select(Moves.SKETCH, 0);
game.move.select(Moves.PROTECT, 1);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ]);
// Player uses Sketch to copy Swords Dance, Player_2 stalls a turn. Player will attempt Assist and should have no usable moves
await game.toNextTurn();
game.move.select(Moves.ASSIST, 0);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.PROTECT, 1);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should apply secondary effects of a move", async () => {
game.override.moveset([ Moves.ASSIST, Moves.WOOD_HAMMER, Moves.WOOD_HAMMER, Moves.WOOD_HAMMER ]);
await game.classicMode.startBattle([ Species.FEEBAS, Species.SHUCKLE ]);
const [ feebas, shuckle ] = game.scene.getPlayerField();
game.move.changeMoveset(feebas, [ Moves.ASSIST, Moves.SKETCH, Moves.PROTECT, Moves.DRAGON_TAIL ]);
game.move.changeMoveset(shuckle, [ Moves.ASSIST, Moves.SKETCH, Moves.PROTECT, Moves.DRAGON_TAIL ]);
game.move.select(Moves.ASSIST, 0);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.ASSIST, 1);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // should receive recoil damage from Wood Hammer
});
});

View File

@ -0,0 +1,91 @@
import { BattlerIndex } from "#app/battle";
import { allMoves, RandomMoveAttr } from "#app/data/move";
import { Stat } from "#app/enums/stat";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Copycat", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const randomMoveAttr = allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0];
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.COPYCAT, Moves.SPIKY_SHIELD, Moves.SWORDS_DANCE, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.starterSpecies(Species.FEEBAS)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should copy the last move successfully executed", async () => {
game.override.enemyMoveset(Moves.SUCKER_PUNCH);
await game.classicMode.startBattle();
game.move.select(Moves.SWORDS_DANCE);
await game.toNextTurn();
game.move.select(Moves.COPYCAT); // Last successful move should be Swords Dance
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(4);
});
it("should fail when the last move used is not a valid Copycat move", async () => {
game.override.enemyMoveset(Moves.PROTECT); // Protect is not a valid move for Copycat to copy
await game.classicMode.startBattle();
game.move.select(Moves.SPIKY_SHIELD); // Spiky Shield is not a valid move for Copycat to copy
await game.toNextTurn();
game.move.select(Moves.COPYCAT);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should copy the called move when the last move successfully calls another", async () => {
game.override
.moveset([ Moves.SPLASH, Moves.METRONOME ])
.enemyMoveset(Moves.COPYCAT);
await game.classicMode.startBattle();
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.SWORDS_DANCE);
game.move.select(Moves.METRONOME);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); // Player moves first, so enemy can copy Swords Dance
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(2);
});
it("should apply secondary effects of a move", async () => {
game.override.enemyMoveset(Moves.ACID_SPRAY); // Secondary effect lowers SpDef by 2 stages
await game.classicMode.startBattle();
game.move.select(Moves.COPYCAT);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPDEF)).toBe(-2);
});
});

View File

@ -0,0 +1,113 @@
import { RechargingTag, SemiInvulnerableTag } from "#app/data/battler-tags";
import { allMoves, RandomMoveAttr } from "#app/data/move";
import { Abilities } from "#app/enums/abilities";
import { Stat } from "#app/enums/stat";
import { CommandPhase } from "#app/phases/command-phase";
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, it, expect, vi } from "vitest";
describe("Moves - Metronome", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const randomMoveAttr = allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0];
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.METRONOME, Moves.SPLASH ])
.battleType("single")
.startingLevel(100)
.starterSpecies(Species.REGIELEKI)
.enemyLevel(100)
.enemySpecies(Species.SHUCKLE)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH);
});
it("should have one semi-invulnerable turn and deal damage on the second turn when a semi-invulnerable move is called", async () => {
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.DIVE);
game.move.select(Moves.METRONOME);
await game.toNextTurn();
expect(player.getTag(SemiInvulnerableTag)).toBeTruthy();
await game.toNextTurn();
expect(player.getTag(SemiInvulnerableTag)).toBeFalsy();
expect(enemy.isFullHp()).toBeFalsy();
});
it("should apply secondary effects of a move", async () => {
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.WOOD_HAMMER);
game.move.select(Moves.METRONOME);
await game.toNextTurn();
expect(player.isFullHp()).toBeFalsy();
});
it("should recharge after using recharge move", async () => {
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.HYPER_BEAM);
vi.spyOn(allMoves[Moves.HYPER_BEAM], "accuracy", "get").mockReturnValue(100);
game.move.select(Moves.METRONOME);
await game.toNextTurn();
expect(player.getTag(RechargingTag)).toBeTruthy();
});
it("should only target ally for Aromatic Mist", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([ Species.REGIELEKI, Species.RATTATA ]);
const [ leftPlayer, rightPlayer ] = game.scene.getPlayerField();
const [ leftOpp, rightOpp ] = game.scene.getEnemyField();
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.AROMATIC_MIST);
game.move.select(Moves.METRONOME, 0);
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH, 1);
await game.toNextTurn();
expect(rightPlayer.getStatStage(Stat.SPDEF)).toBe(1);
expect(leftPlayer.getStatStage(Stat.SPDEF)).toBe(0);
expect(leftOpp.getStatStage(Stat.SPDEF)).toBe(0);
expect(rightOpp.getStatStage(Stat.SPDEF)).toBe(0);
});
it("should cause opponent to flee, and not crash for Roar", async () => {
await game.classicMode.startBattle();
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.ROAR);
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.METRONOME);
await game.phaseInterceptor.to("BerryPhase");
const isVisible = enemyPokemon.visible;
const hasFled = enemyPokemon.switchOutStatus;
expect(!isVisible && hasFled).toBe(true);
await game.phaseInterceptor.to("CommandPhase");
});
});

View File

@ -0,0 +1,84 @@
import { BattlerIndex } from "#app/battle";
import { Stat } from "#app/enums/stat";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Mirror Move", () => {
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.MIRROR_MOVE, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should use the last move that the target used on the user", async () => {
game.override
.battleType("double")
.enemyMoveset([ Moves.TACKLE, Moves.GROWL ]);
await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]);
game.move.select(Moves.MIRROR_MOVE, 0, BattlerIndex.ENEMY); // target's last move is Tackle, enemy should receive damage from Mirror Move copying Tackle
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
await game.forceEnemyMove(Moves.GROWL, BattlerIndex.PLAYER_2);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyField()[0].isFullHp()).toBeFalsy();
});
it("should apply secondary effects of a move", async () => {
game.override.enemyMoveset(Moves.ACID_SPRAY);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.MIRROR_MOVE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPDEF)).toBe(-2);
});
it("should be able to copy status moves", async () => {
game.override.enemyMoveset(Moves.GROWL);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.MIRROR_MOVE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should fail if the target has not used any moves", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.MIRROR_MOVE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
});

View File

@ -0,0 +1,75 @@
import { Stat } from "#app/enums/stat";
import { StatusEffect } from "#app/enums/status-effect";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Sleep Talk", () => {
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.SLEEP_TALK ])
.statusEffect(StatusEffect.SLEEP)
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.enemyLevel(100);
});
it("should fail when the user is not asleep", async () => {
game.override.statusEffect(StatusEffect.NONE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SLEEP_TALK);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should fail if the user has no valid moves", async () => {
game.override.moveset([ Moves.SLEEP_TALK, Moves.DIG, Moves.METRONOME, Moves.SOLAR_BEAM ]);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SLEEP_TALK);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should call a random valid move if the user is asleep", async () => {
game.override.moveset([ Moves.SLEEP_TALK, Moves.DIG, Moves.FLY, Moves.SWORDS_DANCE ]); // Dig and Fly are invalid moves, Swords Dance should always be called
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SLEEP_TALK);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK));
});
it("should apply secondary effects of a move", async () => {
game.override.moveset([ Moves.SLEEP_TALK, Moves.DIG, Moves.FLY, Moves.WOOD_HAMMER ]); // Dig and Fly are invalid moves, Wood Hammer should always be called
await game.classicMode.startBattle();
game.move.select(Moves.SLEEP_TALK);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // Wood Hammer recoil effect should be applied
});
});

View File

@ -125,7 +125,7 @@ describe("Moves - Spit Up", () => {
game.move.select(Moves.SPIT_UP); game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(TurnInitPhase); await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.FAIL }); expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.FAIL, targets: [ game.scene.getEnemyPokemon()!.getBattlerIndex() ]});
expect(spitUp.calculateBattlePower).not.toHaveBeenCalled(); expect(spitUp.calculateBattlePower).not.toHaveBeenCalled();
}); });
@ -148,7 +148,7 @@ describe("Moves - Spit Up", () => {
await game.phaseInterceptor.to(TurnInitPhase); await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS }); expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS, targets: [ game.scene.getEnemyPokemon()!.getBattlerIndex() ]});
expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce();
@ -176,7 +176,7 @@ describe("Moves - Spit Up", () => {
game.move.select(Moves.SPIT_UP); game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(TurnInitPhase); await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS }); expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS, targets: [ game.scene.getEnemyPokemon()!.getBattlerIndex() ]});
expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce();

View File

@ -72,7 +72,7 @@ describe("Moves - Stockpile", () => {
expect(user.getStatStage(Stat.SPDEF)).toBe(3); expect(user.getStatStage(Stat.SPDEF)).toBe(3);
expect(stockpilingTag).toBeDefined(); expect(stockpilingTag).toBeDefined();
expect(stockpilingTag.stockpiledCount).toBe(3); expect(stockpilingTag.stockpiledCount).toBe(3);
expect(user.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ result: MoveResult.FAIL, move: Moves.STOCKPILE }); expect(user.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ result: MoveResult.FAIL, move: Moves.STOCKPILE, targets: [ user.getBattlerIndex() ]});
} }
} }
}); });

View File

@ -135,7 +135,7 @@ describe("Moves - Swallow", () => {
game.move.select(Moves.SWALLOW); game.move.select(Moves.SWALLOW);
await game.phaseInterceptor.to(TurnInitPhase); await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.FAIL }); expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.FAIL, targets: [ pokemon.getBattlerIndex() ]});
}); });
describe("restores stat stage boosts granted by stacks", () => { describe("restores stat stage boosts granted by stacks", () => {
@ -156,7 +156,7 @@ describe("Moves - Swallow", () => {
await game.phaseInterceptor.to(TurnInitPhase); await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS }); expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS, targets: [ pokemon.getBattlerIndex() ]});
expect(pokemon.getStatStage(Stat.DEF)).toBe(0); expect(pokemon.getStatStage(Stat.DEF)).toBe(0);
expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0); expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0);
@ -183,7 +183,7 @@ describe("Moves - Swallow", () => {
await game.phaseInterceptor.to(TurnInitPhase); await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS }); expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS, targets: [ pokemon.getBattlerIndex() ]});
expect(pokemon.getStatStage(Stat.DEF)).toBe(1); expect(pokemon.getStatStage(Stat.DEF)).toBe(1);
expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2); expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2);

View File

@ -364,6 +364,8 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
success = this.setCursor(0); success = this.setCursor(0);
} else if (this.rowCursor < this.shopOptionsRows.length + 1) { } else if (this.rowCursor < this.shopOptionsRows.length + 1) {
success = this.setRowCursor(this.rowCursor + 1); success = this.setRowCursor(this.rowCursor + 1);
} else {
success = this.setRowCursor(0);
} }
break; break;
case Button.DOWN: case Button.DOWN:
@ -371,13 +373,15 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
success = this.setRowCursor(this.rowCursor - 1); success = this.setRowCursor(this.rowCursor - 1);
} else if (this.lockRarityButtonContainer.visible && this.cursor === 0) { } else if (this.lockRarityButtonContainer.visible && this.cursor === 0) {
success = this.setCursor(3); success = this.setCursor(3);
} else {
success = this.setRowCursor(this.shopOptionsRows.length + 1);
} }
break; break;
case Button.LEFT: case Button.LEFT:
if (!this.rowCursor) { if (!this.rowCursor) {
switch (this.cursor) { switch (this.cursor) {
case 0: case 0:
success = false; success = this.setCursor(2);
break; break;
case 1: case 1:
if (this.lockRarityButtonContainer.visible) { if (this.lockRarityButtonContainer.visible) {
@ -395,11 +399,21 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
success = false; success = false;
} }
break; break;
case 3:
if (this.lockRarityButtonContainer.visible) {
success = this.setCursor(2);
} else {
success = false;
}
} }
} else if (this.cursor) { } else if (this.cursor) {
success = this.setCursor(this.cursor - 1); success = this.setCursor(this.cursor - 1);
} else if (this.rowCursor === 1 && this.rerollButtonContainer.visible) { } else {
success = this.setRowCursor(0); if (this.rowCursor === 1 && this.options.length === 0) {
success = false;
} else {
success = this.setCursor(this.getRowItems(this.rowCursor) - 1);
}
} }
break; break;
case Button.RIGHT: case Button.RIGHT:
@ -416,7 +430,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
success = this.setCursor(2); success = this.setCursor(2);
break; break;
case 2: case 2:
success = false; success = this.setCursor(0);
break; break;
case 3: case 3:
if (this.transferButtonContainer.visible) { if (this.transferButtonContainer.visible) {
@ -428,8 +442,12 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
} }
} else if (this.cursor < this.getRowItems(this.rowCursor) - 1) { } else if (this.cursor < this.getRowItems(this.rowCursor) - 1) {
success = this.setCursor(this.cursor + 1); success = this.setCursor(this.cursor + 1);
} else if (this.rowCursor === 1 && this.transferButtonContainer.visible) { } else {
success = this.setRowCursor(0); if (this.rowCursor === 1 && this.options.length === 0) {
success = this.setRowCursor(0);
} else {
success = this.setCursor(0);
}
} }
break; break;
} }
@ -519,6 +537,14 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
newCursor = 2; newCursor = 2;
} }
} }
// Allows to find lock rarity button when looping from the top
if (rowCursor === 0 && lastRowCursor > 1 && newCursor === 0 && this.lockRarityButtonContainer.visible) {
newCursor = 3;
}
// Allows to loop to top when lock rarity button is shown
if (rowCursor === this.shopOptionsRows.length + 1 && lastRowCursor === 0 && this.cursor === 3) {
newCursor = 0;
}
this.cursor = -1; this.cursor = -1;
this.setCursor(newCursor); this.setCursor(newCursor);
return true; return true;

View File

@ -157,6 +157,12 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
success = (this.cursor === 0) ? this.setCursor(this.cursor) : this.setCursor(this.cursor - 1, cursorPosition); success = (this.cursor === 0) ? this.setCursor(this.cursor) : this.setCursor(this.cursor - 1, cursorPosition);
} else if (this.scrollCursor) { } else if (this.scrollCursor) {
success = this.setScrollCursor(this.scrollCursor - 1, cursorPosition); success = this.setScrollCursor(this.scrollCursor - 1, cursorPosition);
} else if ((this.cursor === 0) && (this.scrollCursor === 0)) {
this.setScrollCursor(SESSION_SLOTS_COUNT - SLOTS_ON_SCREEN);
// Revert to avoid an extra session slot sticking out
this.revertSessionSlot(SESSION_SLOTS_COUNT - SLOTS_ON_SCREEN);
this.setCursor(SLOTS_ON_SCREEN - 1);
success = true;
} }
break; break;
case Button.DOWN: case Button.DOWN:
@ -164,6 +170,11 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
success = this.setCursor(this.cursor + 1, cursorPosition); success = this.setCursor(this.cursor + 1, cursorPosition);
} else if (this.scrollCursor < SESSION_SLOTS_COUNT - SLOTS_ON_SCREEN) { } else if (this.scrollCursor < SESSION_SLOTS_COUNT - SLOTS_ON_SCREEN) {
success = this.setScrollCursor(this.scrollCursor + 1, cursorPosition); success = this.setScrollCursor(this.scrollCursor + 1, cursorPosition);
} else if ((this.cursor === SLOTS_ON_SCREEN - 1) && (this.scrollCursor === SESSION_SLOTS_COUNT - SLOTS_ON_SCREEN)) {
this.setScrollCursor(0);
this.revertSessionSlot(SLOTS_ON_SCREEN - 1);
this.setCursor(0);
success = true;
} }
break; break;
case Button.RIGHT: case Button.RIGHT:

View File

@ -89,6 +89,13 @@ export class NavigationManager {
} }
} }
/**
* Removes menus from the manager in preparation for reset
*/
public clearNavigationMenus() {
this.navigationMenus.length = 0;
}
} }
export default class NavigationMenu extends Phaser.GameObjects.Container { export default class NavigationMenu extends Phaser.GameObjects.Container {

View File

@ -2698,6 +2698,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.updateScroll(); this.updateScroll();
}; };
override destroy(): void {
// Without this the reference gets hung up and no startercontainers get GCd
this.starterContainers = [];
}
updateScroll = () => { updateScroll = () => {
const maxColumns = 9; const maxColumns = 9;
const maxRows = 9; const maxRows = 9;

View File

@ -62,4 +62,9 @@ export default abstract class UiHandler {
clear() { clear() {
this.active = false; this.active = false;
} }
/**
* To be implemented by individual handlers when necessary to free memory
* Called when {@linkcode BattleScene} is reset
*/
destroy(): void {}
} }

View File

@ -53,6 +53,7 @@ import TestDialogueUiHandler from "#app/ui/test-dialogue-ui-handler";
import AutoCompleteUiHandler from "./autocomplete-ui-handler"; import AutoCompleteUiHandler from "./autocomplete-ui-handler";
import { Device } from "#enums/devices"; import { Device } from "#enums/devices";
import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler"; import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler";
import { NavigationManager } from "./settings/navigationMenu";
export enum Mode { export enum Mode {
MESSAGE, MESSAGE,
@ -614,4 +615,14 @@ export default class UI extends Phaser.GameObjects.Container {
return globalScene.inputMethod; return globalScene.inputMethod;
} }
} }
/**
* Attempts to free memory held by UI handlers
* and clears menus from {@linkcode NavigationManager} to prepare for reset
*/
public freeUIData(): void {
this.handlers.forEach(h => h.destroy());
this.handlers = [];
NavigationManager.getInstance().clearNavigationMenus();
}
} }