Merge branch 'beta' into WorkingDiscardFunction

This commit is contained in:
Mikhail Shueb 2025-07-04 12:43:18 +01:00 committed by GitHub
commit d3dae5c4de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 4644 additions and 4729 deletions

View File

@ -26,10 +26,11 @@ If you have the motivation and experience with Typescript/Javascript (or are wil
- node: >=22.14.0 - [manage with pnpm](https://pnpm.io/cli/env) | [manage with fnm](https://github.com/Schniz/fnm) | [manage with nvm](https://github.com/nvm-sh/nvm) - node: >=22.14.0 - [manage with pnpm](https://pnpm.io/cli/env) | [manage with fnm](https://github.com/Schniz/fnm) | [manage with nvm](https://github.com/nvm-sh/nvm)
- pnpm: 10.x - [how to install](https://pnpm.io/installation) (not recommended to install via `npm` on Windows native) | [alternate method - volta.sh](https://volta.sh/) - pnpm: 10.x - [how to install](https://pnpm.io/installation) (not recommended to install via `npm` on Windows native) | [alternate method - volta.sh](https://volta.sh/)
- The repository [forked](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and [cloned](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) locally on your device
### Running Locally ### Running Locally
1. Clone the repo and in the root directory run `pnpm install` 1. Run `pnpm install` from the repository root
- *if you run into any errors, reach out in the **#dev-corner** channel on Discord* - *if you run into any errors, reach out in the **#dev-corner** channel on Discord*
2. Run `pnpm start:dev` to locally run the project at `localhost:8000` 2. Run `pnpm start:dev` to locally run the project at `localhost:8000`

View File

@ -1,13 +0,0 @@
import { Graphviz } from "@hpcc-js/wasm/graphviz";
const graphviz = await Graphviz.load();
const inputFile = [];
for await (const chunk of process.stdin) {
inputFile.push(chunk);
}
const file = Buffer.concat(inputFile).toString("utf-8");
const svg = graphviz.dot(file, "svg");
process.stdout.write(svg);

View File

@ -21,15 +21,13 @@
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched", "biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
"docs": "typedoc", "docs": "typedoc",
"depcruise": "depcruise src test", "depcruise": "depcruise src test",
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg", "postinstall": "lefthook install; git submodule update --init --recursive",
"postinstall": "lefthook install && lefthook run post-merge",
"update-version:patch": "pnpm version patch --force --no-git-tag-version", "update-version:patch": "pnpm version patch --force --no-git-tag-version",
"update-version:minor": "pnpm version minor --force --no-git-tag-version", "update-version:minor": "pnpm version minor --force --no-git-tag-version",
"update-locales:remote": "git submodule update --progress --init --recursive --force --remote" "update-locales:remote": "git submodule update --progress --init --recursive --force --remote"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.0.0", "@biomejs/biome": "2.0.0",
"@hpcc-js/wasm": "^2.22.4",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
"@vitest/coverage-istanbul": "^3.0.9", "@vitest/coverage-istanbul": "^3.0.9",

View File

@ -45,9 +45,6 @@ importers:
'@biomejs/biome': '@biomejs/biome':
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0 version: 2.0.0
'@hpcc-js/wasm':
specifier: ^2.22.4
version: 2.22.4
'@types/jsdom': '@types/jsdom':
specifier: ^21.1.7 specifier: ^21.1.7
version: 21.1.7 version: 21.1.7
@ -436,10 +433,6 @@ packages:
'@gerrit0/mini-shiki@3.2.2': '@gerrit0/mini-shiki@3.2.2':
resolution: {integrity: sha512-vaZNGhGLKMY14HbF53xxHNgFO9Wz+t5lTlGNpl2N9xFiKQ0I5oIe0vKjU9dh7Nb3Dw6lZ7wqUE0ri+zcdpnK+Q==} resolution: {integrity: sha512-vaZNGhGLKMY14HbF53xxHNgFO9Wz+t5lTlGNpl2N9xFiKQ0I5oIe0vKjU9dh7Nb3Dw6lZ7wqUE0ri+zcdpnK+Q==}
'@hpcc-js/wasm@2.22.4':
resolution: {integrity: sha512-58JkRkxZffiBAbZhc7z+9iaaAOmn1cyxLL3rRwsUvco/I0Wwb7uVAlHM9HiU6XASe2k11jrIjCFff1t9QKjlqg==}
hasBin: true
'@inquirer/checkbox@4.1.4': '@inquirer/checkbox@4.1.4':
resolution: {integrity: sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==} resolution: {integrity: sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -2332,10 +2325,6 @@ snapshots:
'@shikijs/types': 3.2.1 '@shikijs/types': 3.2.1
'@shikijs/vscode-textmate': 10.0.2 '@shikijs/vscode-textmate': 10.0.2
'@hpcc-js/wasm@2.22.4':
dependencies:
yargs: 17.7.2
'@inquirer/checkbox@4.1.4(@types/node@22.13.14)': '@inquirer/checkbox@4.1.4(@types/node@22.13.14)':
dependencies: dependencies:
'@inquirer/core': 10.1.9(@types/node@22.13.14) '@inquirer/core': 10.1.9(@types/node@22.13.14)

6
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,6 @@
onlyBuiltDependencies:
- esbuild
- msw
- lefthook
shellEmulator: true

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

@ -1 +1 @@
Subproject commit fade123e20ff951e199d7c0466686fe8c5511643 Subproject commit aa94b0b68265c26f728d154998582bb629f2b850

View File

@ -54,7 +54,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
.withFleeAllowed(false) .withFleeAllowed(false)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "berry_juice", spriteKey: "berry_juice_good",
fileRoot: "items", fileRoot: "items",
hasShadow: true, hasShadow: true,
isItem: true, isItem: true,
@ -171,11 +171,11 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
sortedParty.forEach((pokemon, index) => { sortedParty.forEach((pokemon, index) => {
if (index < 2) { if (index < 2) {
// -15 to the two highest BST mons // -15 to the two highest BST mons
modifyPlayerPokemonBST(pokemon, -HIGH_BST_REDUCTION_VALUE); modifyPlayerPokemonBST(pokemon, false);
encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender()); encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender());
} else { } else {
// +10 for the rest // +10 for the rest
modifyPlayerPokemonBST(pokemon, BST_INCREASE_VALUE); modifyPlayerPokemonBST(pokemon, true);
} }
}); });

View File

@ -33,7 +33,6 @@ import {
TransformationScreenPosition, TransformationScreenPosition,
} from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
import { getLevelTotalExp } from "#app/data/exp"; import { getLevelTotalExp } from "#app/data/exp";
import { Stat } from "#enums/stat";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
@ -104,8 +103,6 @@ const EXCLUDED_TRANSFORMATION_SPECIES = [
const SUPER_LEGENDARY_BST_THRESHOLD = 600; const SUPER_LEGENDARY_BST_THRESHOLD = 600;
const NON_LEGENDARY_BST_THRESHOLD = 570; const NON_LEGENDARY_BST_THRESHOLD = 570;
const OLD_GATEAU_STATS_UP = 20;
/** 0-100 */ /** 0-100 */
const PERCENT_LEVEL_LOSS_ON_REFUSE = 10; const PERCENT_LEVEL_LOSS_ON_REFUSE = 10;
@ -275,12 +272,8 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
} }
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) { if (shouldGetOldGateau(newPokemon)) {
const stats = getOldGateauBoostedStats(newPokemon);
newPokemonHeldItemConfigs.push({ newPokemonHeldItemConfigs.push({
modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU, [ modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU) as PokemonHeldItemModifierType,
OLD_GATEAU_STATS_UP,
stats,
]) as PokemonHeldItemModifierType,
stackCount: 1, stackCount: 1,
isTransferable: false, isTransferable: false,
}); });
@ -461,11 +454,7 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
} }
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) { if (shouldGetOldGateau(newPokemon)) {
const stats = getOldGateauBoostedStats(newPokemon); const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU();
const modType = modifierTypes
.MYSTERY_ENCOUNTER_OLD_GATEAU()
.generateType(globalScene.getPlayerParty(), [OLD_GATEAU_STATS_UP, stats])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
const modifier = modType?.newModifier(newPokemon); const modifier = modType?.newModifier(newPokemon);
if (modifier) { if (modifier) {
globalScene.addModifier(modifier, false, false, false, true); globalScene.addModifier(modifier, false, false, false, true);
@ -616,22 +605,6 @@ function shouldGetOldGateau(pokemon: Pokemon): boolean {
return pokemon.getSpeciesForm().getBaseStatTotal() < NON_LEGENDARY_BST_THRESHOLD; return pokemon.getSpeciesForm().getBaseStatTotal() < NON_LEGENDARY_BST_THRESHOLD;
} }
/**
* Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef
* @returns Array of 3 {@linkcode Stat}s to boost
*/
function getOldGateauBoostedStats(pokemon: Pokemon): Stat[] {
const stats: Stat[] = [];
const baseStats = pokemon.getSpeciesForm().baseStats.slice(0);
// HP or Speed
stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD);
// Attack or SpAtk
stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK);
// Def or SpDef
stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF);
return stats;
}
function getTransformedSpecies( function getTransformedSpecies(
originalBst: number, originalBst: number,
bstSearchRange: [number, number], bstSearchRange: [number, number],

View File

@ -375,10 +375,10 @@ export function applyHealToPokemon(pokemon: PlayerPokemon, heal: number) {
* @param pokemon * @param pokemon
* @param value * @param value
*/ */
export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) { export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, good: boolean) {
const modType = modifierTypes const modType = modifierTypes
.MYSTERY_ENCOUNTER_SHUCKLE_JUICE() .MYSTERY_ENCOUNTER_SHUCKLE_JUICE()
.generateType(globalScene.getPlayerParty(), [value]) .generateType(globalScene.getPlayerParty(), [good ? 10 : -15])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE); ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE);
const modifier = modType?.newModifier(pokemon); const modifier = modType?.newModifier(pokemon);
if (modifier) { if (modifier) {

View File

@ -967,31 +967,23 @@ export class PokemonBaseStatTotalModifierType
extends PokemonHeldItemModifierType extends PokemonHeldItemModifierType
implements GeneratedPersistentModifierType implements GeneratedPersistentModifierType
{ {
private readonly statModifier: number; private readonly statModifier: 10 | -15;
constructor(statModifier: number) { constructor(statModifier: 10 | -15) {
super( super(
"modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE", statModifier > 0
"berry_juice", ? "modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD"
(_type, args) => new PokemonBaseStatTotalModifier(this, (args[0] as Pokemon).id, this.statModifier), : "modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD",
statModifier > 0 ? "berry_juice_good" : "berry_juice_bad",
(_type, args) => new PokemonBaseStatTotalModifier(this, (args[0] as Pokemon).id, statModifier),
); );
this.statModifier = statModifier; this.statModifier = statModifier;
} }
override getDescription(): string { override getDescription(): string {
return i18next.t("modifierType:ModifierType.PokemonBaseStatTotalModifierType.description", { return this.statModifier > 0
increaseDecrease: i18next.t( ? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.description")
this.statModifier >= 0 : i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.description");
? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.increase"
: "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.decrease",
),
blessCurse: i18next.t(
this.statModifier >= 0
? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.blessed"
: "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.cursed",
),
statValue: this.statModifier,
});
} }
public getPregenArgs(): any[] { public getPregenArgs(): any[] {
@ -999,38 +991,6 @@ export class PokemonBaseStatTotalModifierType
} }
} }
/**
* Old Gateau item
*/
export class PokemonBaseStatFlatModifierType
extends PokemonHeldItemModifierType
implements GeneratedPersistentModifierType
{
private readonly statModifier: number;
private readonly stats: Stat[];
constructor(statModifier: number, stats: Stat[]) {
super(
"modifierType:ModifierType.MYSTERY_ENCOUNTER_OLD_GATEAU",
"old_gateau",
(_type, args) => new PokemonBaseStatFlatModifier(this, (args[0] as Pokemon).id, this.statModifier, this.stats),
);
this.statModifier = statModifier;
this.stats = stats;
}
override getDescription(): string {
return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description", {
stats: this.stats.map(stat => i18next.t(getStatKey(stat))).join("/"),
statValue: this.statModifier,
});
}
public getPregenArgs(): any[] {
return [this.statModifier, this.stats];
}
}
class AllPokemonFullHpRestoreModifierType extends ModifierType { class AllPokemonFullHpRestoreModifierType extends ModifierType {
private descriptionKey: string; private descriptionKey: string;
@ -2331,17 +2291,16 @@ const modifierTypeInitObj = Object.freeze({
MYSTERY_ENCOUNTER_SHUCKLE_JUICE: () => MYSTERY_ENCOUNTER_SHUCKLE_JUICE: () =>
new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) { if (pregenArgs) {
return new PokemonBaseStatTotalModifierType(pregenArgs[0] as number); return new PokemonBaseStatTotalModifierType(pregenArgs[0] as 10 | -15);
} }
return new PokemonBaseStatTotalModifierType(randSeedInt(20, 1)); return new PokemonBaseStatTotalModifierType(10);
}), }),
MYSTERY_ENCOUNTER_OLD_GATEAU: () => MYSTERY_ENCOUNTER_OLD_GATEAU: () =>
new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { new PokemonHeldItemModifierType(
if (pregenArgs) { "modifierType:ModifierType.MYSTERY_ENCOUNTER_OLD_GATEAU",
return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]); "old_gateau",
} (type, args) => new PokemonBaseStatFlatModifier(type, (args[0] as Pokemon).id),
return new PokemonBaseStatFlatModifierType(randSeedInt(20, 1), [Stat.HP, Stat.ATK, Stat.DEF]); ),
}),
MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => MYSTERY_ENCOUNTER_BLACK_SLUDGE: () =>
new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => { new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) { if (pregenArgs) {

View File

@ -952,10 +952,9 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
public override type: PokemonBaseStatTotalModifierType; public override type: PokemonBaseStatTotalModifierType;
public isTransferable = false; public isTransferable = false;
public statModifier: 10 | -15;
private statModifier: number; constructor(type: PokemonBaseStatTotalModifierType, pokemonId: number, statModifier: 10 | -15, stackCount?: number) {
constructor(type: PokemonBaseStatTotalModifierType, pokemonId: number, statModifier: number, stackCount?: number) {
super(type, pokemonId, stackCount); super(type, pokemonId, stackCount);
this.statModifier = statModifier; this.statModifier = statModifier;
} }
@ -1012,31 +1011,14 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
* Currently used by Old Gateau item * Currently used by Old Gateau item
*/ */
export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier {
private statModifier: number;
private stats: Stat[];
public isTransferable = false; public isTransferable = false;
constructor(type: ModifierType, pokemonId: number, statModifier: number, stats: Stat[], stackCount?: number) {
super(type, pokemonId, stackCount);
this.statModifier = statModifier;
this.stats = stats;
}
override matchType(modifier: Modifier): boolean { override matchType(modifier: Modifier): boolean {
return ( return modifier instanceof PokemonBaseStatFlatModifier;
modifier instanceof PokemonBaseStatFlatModifier &&
modifier.statModifier === this.statModifier &&
this.stats.every(s => modifier.stats.some(stat => s === stat))
);
} }
override clone(): PersistentModifier { override clone(): PersistentModifier {
return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount); return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.stackCount);
}
override getArgs(): any[] {
return [...super.getArgs(), this.statModifier, this.stats];
} }
/** /**
@ -1055,11 +1037,13 @@ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier {
* @param baseStats The base stats of the {@linkcode Pokemon} * @param baseStats The base stats of the {@linkcode Pokemon}
* @returns always `true` * @returns always `true`
*/ */
override apply(_pokemon: Pokemon, baseStats: number[]): boolean { override apply(pokemon: Pokemon, baseStats: number[]): boolean {
// Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats
const stats = this.getStats(pokemon);
const statModifier = 20;
baseStats.forEach((v, i) => { baseStats.forEach((v, i) => {
if (this.stats.includes(i)) { if (stats.includes(i)) {
const newVal = Math.floor(v + this.statModifier); const newVal = Math.floor(v + statModifier);
baseStats[i] = Math.min(Math.max(newVal, 1), 999999); baseStats[i] = Math.min(Math.max(newVal, 1), 999999);
} }
}); });
@ -1067,6 +1051,22 @@ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier {
return true; return true;
} }
/**
* Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef
* @returns Array of 3 {@linkcode Stat}s to boost
*/
getStats(pokemon: Pokemon): Stat[] {
const stats: Stat[] = [];
const baseStats = pokemon.getSpeciesForm().baseStats.slice(0);
// HP or Speed
stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD);
// Attack or SpAtk
stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK);
// Def or SpDef
stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF);
return stats;
}
override getScoreMultiplier(): number { override getScoreMultiplier(): number {
return 1.1; return 1.1;
} }

View File

@ -45,7 +45,7 @@ describe("Abilities - Good As Gold", () => {
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.SPLASH, 0); game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
@ -54,12 +54,13 @@ describe("Abilities - Good As Gold", () => {
}); });
it("should block memento and prevent the user from fainting", async () => { it("should block memento and prevent the user from fainting", async () => {
game.override.enemyMoveset([MoveId.MEMENTO]); game.override.enemyAbility(AbilityId.GOOD_AS_GOLD);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.MEMENTO);
game.move.use(MoveId.MEMENTO);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.isFainted()).toBe(false); expect(game.field.getPlayerPokemon().isFainted()).toBe(false);
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(0); expect(game.field.getEnemyPokemon().getStatStage(Stat.ATK)).toBe(0);
}); });
it("should not block any status moves that target the field, one side, or all pokemon", async () => { it("should not block any status moves that target the field, one side, or all pokemon", async () => {

View File

@ -259,7 +259,7 @@ describe("Abilities - Ice Face", () => {
const eiscue = game.scene.getEnemyPokemon()!; const eiscue = game.scene.getEnemyPokemon()!;
expect(eiscue.getTag(BattlerTagType.ICE_FACE)).not.toBe(undefined); expect(eiscue.getTag(BattlerTagType.ICE_FACE)).toBeDefined();
expect(eiscue.formIndex).toBe(icefaceForm); expect(eiscue.formIndex).toBe(icefaceForm);
expect(eiscue.hasAbility(AbilityId.ICE_FACE)).toBe(true); expect(eiscue.hasAbility(AbilityId.ICE_FACE)).toBe(true);
}); });
@ -269,13 +269,9 @@ describe("Abilities - Ice Face", () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.SIMPLE_BEAM);
await game.phaseInterceptor.to(TurnInitPhase);
const eiscue = game.scene.getEnemyPokemon()!; const eiscue = game.scene.getEnemyPokemon()!;
expect(eiscue.getTag(BattlerTagType.ICE_FACE)).not.toBe(undefined); expect(eiscue.getTag(BattlerTagType.ICE_FACE)).toBeDefined();
expect(eiscue.formIndex).toBe(icefaceForm); expect(eiscue.formIndex).toBe(icefaceForm);
expect(game.scene.getPlayerPokemon()!.hasAbility(AbilityId.TRACE)).toBe(true); expect(game.scene.getPlayerPokemon()!.hasAbility(AbilityId.TRACE)).toBe(true);
}); });

View File

@ -74,9 +74,8 @@ describe("Abilities - Imposter", () => {
} }
}); });
// TODO: this doesn't actually test imposter - transforming happens before poewr split
it("should copy in-battle overridden stats", async () => { it("should copy in-battle overridden stats", async () => {
game.override.enemyMoveset([MoveId.POWER_SPLIT]);
await game.classicMode.startBattle([SpeciesId.DITTO]); await game.classicMode.startBattle([SpeciesId.DITTO]);
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
@ -85,7 +84,8 @@ describe("Abilities - Imposter", () => {
const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2); const avgAtk = Math.floor((player.getStat(Stat.ATK, false) + enemy.getStat(Stat.ATK, false)) / 2);
const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2); const avgSpAtk = Math.floor((player.getStat(Stat.SPATK, false) + enemy.getStat(Stat.SPATK, false)) / 2);
game.move.select(MoveId.TACKLE); game.move.use(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.POWER_SPLIT);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(player.getStat(Stat.ATK, false)).toBe(avgAtk); expect(player.getStat(Stat.ATK, false)).toBe(avgAtk);
@ -101,9 +101,6 @@ describe("Abilities - Imposter", () => {
await game.classicMode.startBattle([SpeciesId.DITTO]); await game.classicMode.startBattle([SpeciesId.DITTO]);
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
player.getMoveset().forEach(move => { player.getMoveset().forEach(move => {
// Should set correct maximum PP without touching `ppUp` // Should set correct maximum PP without touching `ppUp`
if (move) { if (move) {
@ -122,15 +119,10 @@ describe("Abilities - Imposter", () => {
await game.classicMode.startBattle([SpeciesId.DITTO]); await game.classicMode.startBattle([SpeciesId.DITTO]);
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to("MoveEndPhase");
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1);
}); });
it("should persist transformed attributes across reloads", async () => { it("should persist transformed attributes across reloads", async () => {
game.override.moveset([MoveId.ABSORB]);
await game.classicMode.startBattle([SpeciesId.DITTO]); await game.classicMode.startBattle([SpeciesId.DITTO]);
const player = game.scene.getPlayerPokemon()!; const player = game.scene.getPlayerPokemon()!;
@ -162,7 +154,7 @@ describe("Abilities - Imposter", () => {
}); });
it("should stay transformed with the correct form after reload", async () => { it("should stay transformed with the correct form after reload", async () => {
game.override.moveset([MoveId.ABSORB]).enemySpecies(SpeciesId.UNOWN); game.override.enemySpecies(SpeciesId.UNOWN);
await game.classicMode.startBattle([SpeciesId.DITTO]); await game.classicMode.startBattle([SpeciesId.DITTO]);
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;

View File

@ -3,7 +3,6 @@ import Phaser from "phaser";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { getMovePosition } from "#test/testUtils/gameManagerUtils";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
@ -114,7 +113,7 @@ describe("Abilities - Intimidate", () => {
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-1);
game.move.select(getMovePosition(game.scene, 0, MoveId.SPLASH)); game.move.select(MoveId.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
enemyPokemon = game.scene.getEnemyPokemon()!; enemyPokemon = game.scene.getEnemyPokemon()!;

View File

@ -36,10 +36,8 @@ describe("Abilities - Lightningrod", () => {
it("should redirect electric type moves", async () => { it("should redirect electric type moves", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
const enemy1 = game.scene.getEnemyField()[0]; const [enemy1, enemy2] = game.scene.getEnemyField();
const enemy2 = game.scene.getEnemyField()[1]; game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD;
game.move.select(MoveId.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
@ -52,10 +50,8 @@ describe("Abilities - Lightningrod", () => {
game.override.moveset([MoveId.SPLASH, MoveId.AERIAL_ACE]); game.override.moveset([MoveId.SPLASH, MoveId.AERIAL_ACE]);
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
const enemy1 = game.scene.getEnemyField()[0]; const [enemy1, enemy2] = game.scene.getEnemyField();
const enemy2 = game.scene.getEnemyField()[1]; game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD;
game.move.select(MoveId.AERIAL_ACE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.AERIAL_ACE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
@ -68,8 +64,7 @@ describe("Abilities - Lightningrod", () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
const enemy2 = game.scene.getEnemyField()[1]; const enemy2 = game.scene.getEnemyField()[1];
game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD;
game.move.select(MoveId.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
@ -81,31 +76,25 @@ describe("Abilities - Lightningrod", () => {
it("should not redirect moves changed from electric type via ability", async () => { it("should not redirect moves changed from electric type via ability", async () => {
game.override.ability(AbilityId.NORMALIZE); game.override.ability(AbilityId.NORMALIZE);
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const enemy1 = game.scene.getEnemyField()[0]; const [enemy1, enemy2] = game.scene.getEnemyField();
const enemy2 = game.scene.getEnemyField()[1]; game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD;
game.move.select(MoveId.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemy1.isFullHp()).toBe(false); expect(enemy1.isFullHp()).toBe(false);
}); });
it("should redirect moves changed to electric type via ability", async () => { it("should redirect moves changed to electric type via ability", async () => {
game.override.ability(AbilityId.GALVANIZE).moveset(MoveId.TACKLE); game.override.ability(AbilityId.GALVANIZE);
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const enemy1 = game.scene.getEnemyField()[0]; const [enemy1, enemy2] = game.scene.getEnemyField();
const enemy2 = game.scene.getEnemyField()[1]; game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD; game.move.use(MoveId.TACKLE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.TACKLE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemy1.isFullHp()).toBe(true); expect(enemy1.isFullHp()).toBe(true);

View File

@ -1,8 +1,6 @@
import { BattlerIndex } from "#enums/battler-index";
import { globalScene } from "#app/global-scene";
import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { AbilityId } from "#enums/ability-id";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -24,29 +22,28 @@ describe("Abilities - Mold Breaker", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.moveset([MoveId.SPLASH])
.ability(AbilityId.MOLD_BREAKER) .ability(AbilityId.MOLD_BREAKER)
.battleStyle("single") .battleStyle("single")
.criticalHits(false) .criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP) .enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.STURDY)
.enemyMoveset(MoveId.SPLASH); .enemyMoveset(MoveId.SPLASH);
}); });
it("should turn off the ignore abilities arena variable after the user's move", async () => { it("should turn off the ignore abilities arena variable after the user's move", async () => {
game.override await game.classicMode.startBattle([SpeciesId.PINSIR]);
.enemyMoveset(MoveId.SPLASH)
.ability(AbilityId.MOLD_BREAKER)
.moveset([MoveId.ERUPTION])
.startingLevel(100)
.enemyLevel(2);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const enemy = game.scene.getEnemyPokemon()!;
expect(enemy.isFainted()).toBe(false); const player = game.field.getPlayerPokemon();
game.move.select(MoveId.SPLASH); const enemy = game.field.getEnemyPokemon();
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEndPhase", true); game.move.use(MoveId.X_SCISSOR);
expect(globalScene.arena.ignoreAbilities).toBe(false); await game.phaseInterceptor.to("MoveEffectPhase");
expect(game.scene.arena.ignoreAbilities).toBe(true);
expect(game.scene.arena.ignoringEffectSource).toBe(player.getBattlerIndex());
await game.toEndOfTurn();
expect(game.scene.arena.ignoreAbilities).toBe(false);
expect(enemy.isFainted()).toBe(true);
}); });
}); });

View File

@ -65,8 +65,7 @@ describe("Abilities - Moxie", () => {
secondPokemon.hp = 1; secondPokemon.hp = 1;
game.move.select(moveToUse); game.move.select(moveToUse, BattlerIndex.PLAYER_2);
game.selectTarget(BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);

View File

@ -28,11 +28,11 @@ describe("Abilities - Screen Cleaner", () => {
}); });
it("removes Aurora Veil", async () => { it("removes Aurora Veil", async () => {
game.override.moveset([MoveId.HAIL]).enemyMoveset(MoveId.AURORA_VEIL); game.override.enemyMoveset(MoveId.AURORA_VEIL);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
game.move.select(MoveId.HAIL); game.move.use(MoveId.HAIL);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.arena.getTag(ArenaTagType.AURORA_VEIL)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.AURORA_VEIL)).toBeDefined();
@ -49,7 +49,7 @@ describe("Abilities - Screen Cleaner", () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
game.move.select(MoveId.SPLASH); game.move.use(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.arena.getTag(ArenaTagType.LIGHT_SCREEN)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.LIGHT_SCREEN)).toBeDefined();
@ -66,7 +66,7 @@ describe("Abilities - Screen Cleaner", () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
game.move.select(MoveId.SPLASH); game.move.use(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(game.scene.arena.getTag(ArenaTagType.REFLECT)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.REFLECT)).toBeDefined();

View File

@ -7,6 +7,7 @@ import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
// TODO: Condense with lightning rod tests sometime
describe("Abilities - Storm Drain", () => { describe("Abilities - Storm Drain", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
@ -39,7 +40,7 @@ describe("Abilities - Storm Drain", () => {
const enemy1 = game.scene.getEnemyField()[0]; const enemy1 = game.scene.getEnemyField()[0];
const enemy2 = game.scene.getEnemyField()[1]; const enemy2 = game.scene.getEnemyField()[1];
enemy2.summonData.ability = AbilityId.STORM_DRAIN; game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN);
game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
@ -55,7 +56,7 @@ describe("Abilities - Storm Drain", () => {
const enemy1 = game.scene.getEnemyField()[0]; const enemy1 = game.scene.getEnemyField()[0];
const enemy2 = game.scene.getEnemyField()[1]; const enemy2 = game.scene.getEnemyField()[1];
enemy2.summonData.ability = AbilityId.STORM_DRAIN; game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN);
game.move.select(MoveId.AERIAL_ACE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.AERIAL_ACE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
@ -68,8 +69,7 @@ describe("Abilities - Storm Drain", () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
const enemy2 = game.scene.getEnemyField()[1]; const enemy2 = game.scene.getEnemyField()[1];
game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN);
enemy2.summonData.ability = AbilityId.STORM_DRAIN;
game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
@ -85,8 +85,7 @@ describe("Abilities - Storm Drain", () => {
const enemy1 = game.scene.getEnemyField()[0]; const enemy1 = game.scene.getEnemyField()[0];
const enemy2 = game.scene.getEnemyField()[1]; const enemy2 = game.scene.getEnemyField()[1];
game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN);
enemy2.summonData.ability = AbilityId.STORM_DRAIN;
game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
@ -96,16 +95,15 @@ describe("Abilities - Storm Drain", () => {
}); });
it("should redirect moves changed to water type via ability", async () => { it("should redirect moves changed to water type via ability", async () => {
game.override.ability(AbilityId.LIQUID_VOICE).moveset(MoveId.PSYCHIC_NOISE); game.override.ability(AbilityId.LIQUID_VOICE);
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const enemy1 = game.scene.getEnemyField()[0]; const enemy1 = game.scene.getEnemyField()[0];
const enemy2 = game.scene.getEnemyField()[1]; const enemy2 = game.scene.getEnemyField()[1];
enemy2.summonData.ability = AbilityId.STORM_DRAIN; game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN);
game.move.select(MoveId.PSYCHIC_NOISE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.use(MoveId.ROUND, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(enemy1.isFullHp()).toBe(true); expect(enemy1.isFullHp()).toBe(true);

View File

@ -86,7 +86,7 @@ describe("Weather - Strong Winds", () => {
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
enemy.hp = 1; enemy.hp = 1;
game.move.select(MoveId.SPLASH); game.move.use(MoveId.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.weather?.weatherType).toBeUndefined(); expect(game.scene.arena.weather?.weatherType).toBeUndefined();

View File

@ -58,8 +58,7 @@ describe("Items - Dire Hit", () => {
await game.classicMode.startBattle([SpeciesId.PIKACHU]); await game.classicMode.startBattle([SpeciesId.PIKACHU]);
game.move.select(MoveId.SPLASH); game.move.use(MoveId.SPLASH);
await game.doKillOpponents(); await game.doKillOpponents();
await game.phaseInterceptor.to(BattleEndPhase); await game.phaseInterceptor.to(BattleEndPhase);

View File

@ -4,7 +4,6 @@ import type Move from "#app/data/moves/move";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { ArenaTagType } from "#app/enums/arena-tag-type"; import { ArenaTagType } from "#app/enums/arena-tag-type";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { NumberHolder } from "#app/utils/common"; import { NumberHolder } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
@ -12,7 +11,7 @@ import { SpeciesId } from "#enums/species-id";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
let globalScene: BattleScene; let globalScene: BattleScene;
@ -52,10 +51,10 @@ describe("Moves - Aurora Veil", () => {
game.move.select(moveToUse); game.move.select(moveToUse);
await game.phaseInterceptor.to(TurnEndPhase); await game.toEndOfTurn();
const mockedDmg = getMockedMoveDamage( const mockedDmg = getMockedMoveDamage(
game.scene.getEnemyPokemon()!, game.field.getEnemyPokemon(),
game.scene.getPlayerPokemon()!, game.field.getPlayerPokemon(),
allMoves[moveToUse], allMoves[moveToUse],
); );
@ -71,10 +70,10 @@ describe("Moves - Aurora Veil", () => {
game.move.select(moveToUse); game.move.select(moveToUse);
game.move.select(moveToUse, 1); game.move.select(moveToUse, 1);
await game.phaseInterceptor.to(TurnEndPhase); await game.toEndOfTurn();
const mockedDmg = getMockedMoveDamage( const mockedDmg = getMockedMoveDamage(
game.scene.getEnemyPokemon()!, game.field.getEnemyPokemon(),
game.scene.getPlayerPokemon()!, game.field.getPlayerPokemon(),
allMoves[moveToUse], allMoves[moveToUse],
); );
@ -82,72 +81,48 @@ describe("Moves - Aurora Veil", () => {
}); });
it("reduces damage of special attacks by half in a single battle", async () => { it("reduces damage of special attacks by half in a single battle", async () => {
const moveToUse = MoveId.ABSORB;
await game.classicMode.startBattle([SpeciesId.SHUCKLE]); await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
game.move.select(moveToUse); game.move.use(MoveId.ABSORB);
await game.phaseInterceptor.to(TurnEndPhase); await game.toEndOfTurn();
const mockedDmg = getMockedMoveDamage( const mockedDmg = getMockedMoveDamage(
game.scene.getEnemyPokemon()!, game.field.getEnemyPokemon(),
game.scene.getPlayerPokemon()!, game.field.getPlayerPokemon(),
allMoves[moveToUse], allMoves[MoveId.ABSORB],
); );
expect(mockedDmg).toBe(allMoves[moveToUse].power * singleBattleMultiplier); expect(mockedDmg).toBe(allMoves[MoveId.ABSORB].power * singleBattleMultiplier);
}); });
it("reduces damage of special attacks by a third in a double battle", async () => { it("reduces damage of special attacks by a third in a double battle", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double");
const moveToUse = MoveId.DAZZLING_GLEAM;
await game.classicMode.startBattle([SpeciesId.SHUCKLE, SpeciesId.SHUCKLE]);
game.move.select(moveToUse);
game.move.select(moveToUse, 1);
await game.phaseInterceptor.to(TurnEndPhase);
const mockedDmg = getMockedMoveDamage(
game.scene.getEnemyPokemon()!,
game.scene.getPlayerPokemon()!,
allMoves[moveToUse],
);
expect(mockedDmg).toBe(allMoves[moveToUse].power * doubleBattleMultiplier);
});
it("does not affect physical critical hits", async () => {
game.override.moveset([MoveId.WICKED_BLOW]);
const moveToUse = MoveId.WICKED_BLOW;
await game.classicMode.startBattle([SpeciesId.SHUCKLE]); await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
game.move.select(moveToUse); game.move.use(MoveId.ABSORB);
await game.phaseInterceptor.to(TurnEndPhase); await game.toEndOfTurn();
const mockedDmg = getMockedMoveDamage( const mockedDmg = getMockedMoveDamage(
game.scene.getEnemyPokemon()!, game.field.getEnemyPokemon(),
game.scene.getPlayerPokemon()!, game.field.getPlayerPokemon(),
allMoves[moveToUse], allMoves[MoveId.ABSORB],
); );
expect(mockedDmg).toBe(allMoves[moveToUse].power);
expect(mockedDmg).toBe(allMoves[MoveId.ABSORB].power * doubleBattleMultiplier);
}); });
it("does not affect critical hits", async () => { it("does not affect critical hits", async () => {
game.override.moveset([MoveId.FROST_BREATH]);
const moveToUse = MoveId.FROST_BREATH;
vi.spyOn(allMoves[MoveId.FROST_BREATH], "accuracy", "get").mockReturnValue(100);
await game.classicMode.startBattle([SpeciesId.SHUCKLE]); await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
game.move.select(moveToUse); game.move.use(MoveId.WICKED_BLOW);
await game.phaseInterceptor.to(TurnEndPhase); await game.toEndOfTurn();
const mockedDmg = getMockedMoveDamage( const mockedDmg = getMockedMoveDamage(
game.scene.getEnemyPokemon()!, game.field.getEnemyPokemon(),
game.scene.getPlayerPokemon()!, game.field.getPlayerPokemon(),
allMoves[moveToUse], allMoves[MoveId.WICKED_BLOW],
); );
expect(mockedDmg).toBe(allMoves[moveToUse].power); expect(mockedDmg).toBe(allMoves[MoveId.WICKED_BLOW].power);
}); });
}); });

View File

@ -33,7 +33,7 @@ describe("Moves - Baddy Bad", () => {
game.override.enemyMoveset(MoveId.PROTECT); game.override.enemyMoveset(MoveId.PROTECT);
await game.classicMode.startBattle([SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.FEEBAS]);
game.move.select(MoveId.BADDY_BAD); game.move.use(MoveId.BADDY_BAD);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.arena.tags.length).toBe(0); expect(game.scene.arena.tags.length).toBe(0);

View File

@ -100,12 +100,12 @@ describe("Moves - Gastro Acid", () => {
await game.toNextTurn(); await game.toNextTurn();
expect(enemyPokemon.summonData.abilitySuppressed).toBe(true); expect(enemyPokemon.summonData.abilitySuppressed).toBe(true);
game.move.select(MoveId.WATER_GUN); game.move.use(MoveId.WATER_GUN);
await game.toNextTurn(); await game.toNextTurn();
// water gun should've dealt damage due to suppressed Water Absorb // water gun should've dealt damage due to suppressed Water Absorb
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
game.move.select(MoveId.SPORE); game.move.use(MoveId.SPORE);
await game.toEndOfTurn(); await game.toEndOfTurn();
// Comatose should block stauts effect // Comatose should block stauts effect

View File

@ -361,7 +361,7 @@ describe("Moves - Instruct", () => {
useMode: MoveUseMode.NORMAL, useMode: MoveUseMode.NORMAL,
}); });
game.move.select(MoveId.SPLASH); game.move.use(MoveId.SPLASH);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toEndOfTurn(); await game.toEndOfTurn();
expect(game.field.getEnemyPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(game.field.getEnemyPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);

View File

@ -30,30 +30,26 @@ describe("Moves - Reflect Type", () => {
}); });
it("will make the user Normal/Grass if targetting a typeless Pokemon affected by Forest's Curse", async () => { it("will make the user Normal/Grass if targetting a typeless Pokemon affected by Forest's Curse", async () => {
game.override game.override.startingLevel(60).enemySpecies(SpeciesId.CHARMANDER);
.moveset([MoveId.FORESTS_CURSE, MoveId.REFLECT_TYPE])
.startingLevel(60)
.enemySpecies(SpeciesId.CHARMANDER)
.enemyMoveset([MoveId.BURN_UP, MoveId.SPLASH]);
await game.classicMode.startBattle([SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const playerPokemon = game.scene.getPlayerPokemon(); const playerPokemon = game.field.getPlayerPokemon();
const enemyPokemon = game.scene.getEnemyPokemon(); const enemyPokemon = game.field.getEnemyPokemon();
game.move.select(MoveId.SPLASH); game.move.use(MoveId.SPLASH);
await game.move.selectEnemyMove(MoveId.BURN_UP); await game.move.forceEnemyMove(MoveId.BURN_UP);
await game.toNextTurn(); await game.toNextTurn();
game.move.select(MoveId.FORESTS_CURSE); game.move.use(MoveId.FORESTS_CURSE);
await game.move.selectEnemyMove(MoveId.SPLASH); await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toNextTurn(); await game.toNextTurn();
expect(enemyPokemon?.getTypes().includes(PokemonType.UNKNOWN)).toBe(true); expect(enemyPokemon.getTypes().includes(PokemonType.UNKNOWN)).toBe(true);
expect(enemyPokemon?.getTypes().includes(PokemonType.GRASS)).toBe(true); expect(enemyPokemon.getTypes().includes(PokemonType.GRASS)).toBe(true);
game.move.select(MoveId.REFLECT_TYPE); game.move.use(MoveId.REFLECT_TYPE);
await game.move.selectEnemyMove(MoveId.SPLASH); await game.move.forceEnemyMove(MoveId.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon?.getTypes()[0]).toBe(PokemonType.NORMAL); expect(playerPokemon.getTypes()[0]).toBe(PokemonType.NORMAL);
expect(playerPokemon?.getTypes().includes(PokemonType.GRASS)).toBe(true); expect(playerPokemon.getTypes().includes(PokemonType.GRASS)).toBe(true);
}); });
}); });

View File

@ -6,6 +6,7 @@ import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { ArenaTrapTag } from "#app/data/arena-tag"; import { ArenaTrapTag } from "#app/data/arena-tag";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { BattlerIndex } from "#enums/battler-index";
describe("Moves - Spikes", () => { describe("Moves - Spikes", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -80,14 +81,19 @@ describe("Moves - Spikes", () => {
expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
}); });
it("should work when all targets fainted", async () => { // TODO: re-enable after re-fixing hazards moves
game.override.enemySpecies(SpeciesId.DIGLETT).battleStyle("double").startingLevel(50); it.todo("should work when all targets fainted", async () => {
await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.ROWLET]); game.override.enemySpecies(SpeciesId.DIGLETT).battleStyle("double").startingLevel(1000);
await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.SHUCKLE]);
game.move.select(MoveId.EARTHQUAKE); const [enemy1, enemy2] = game.scene.getEnemyField();
game.move.select(MoveId.SPIKES, 1);
await game.phaseInterceptor.to("TurnEndPhase");
game.move.use(MoveId.HYPER_VOICE, BattlerIndex.PLAYER);
game.move.use(MoveId.SPIKES, BattlerIndex.PLAYER_2);
await game.toEndOfTurn();
expect(enemy1.isFainted()).toBe(true);
expect(enemy2.isFainted()).toBe(true);
expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeDefined(); expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeDefined();
}); });
}); });

View File

@ -12,7 +12,6 @@ import overrides from "#app/overrides";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { EncounterPhase } from "#app/phases/encounter-phase"; import { EncounterPhase } from "#app/phases/encounter-phase";
import { FaintPhase } from "#app/phases/faint-phase";
import { LoginPhase } from "#app/phases/login-phase"; import { LoginPhase } from "#app/phases/login-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
@ -201,9 +200,8 @@ export default class GameManager {
/** /**
* Helper function to run to the final boss encounter as it's a bit tricky due to extra dialogue * Helper function to run to the final boss encounter as it's a bit tricky due to extra dialogue
* Also handles Major/Minor bosses from endless modes * Also handles Major/Minor bosses from endless modes
* @param game - The game manager * @param species - Array of {@linkcode SpeciesId}s to start the final battle with.
* @param species * @param mode - The {@linkcode GameModes} to spawn the final boss encounter in.
* @param mode
*/ */
async runToFinalBossEncounter(species: SpeciesId[], mode: GameModes) { async runToFinalBossEncounter(species: SpeciesId[], mode: GameModes) {
console.log("===to final boss encounter==="); console.log("===to final boss encounter===");
@ -230,9 +228,9 @@ export default class GameManager {
/** /**
* Runs the game to a mystery encounter phase. * Runs the game to a mystery encounter phase.
* @param encounterType if specified, will expect encounter to have been spawned * @param encounterType - If specified, will expect encounter to be the given type.
* @param species Optional array of species for party. * @param species - Optional array of species for party to start with.
* @returns A promise that resolves when the EncounterPhase ends. * @returns A Promise that resolves when the EncounterPhase ends.
*/ */
async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: SpeciesId[]) { async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: SpeciesId[]) {
if (!isNullOrUndefined(encounterType)) { if (!isNullOrUndefined(encounterType)) {
@ -277,6 +275,7 @@ export default class GameManager {
* Will trigger during the next {@linkcode SelectTargetPhase} * Will trigger during the next {@linkcode SelectTargetPhase}
* @param targetIndex - The {@linkcode BattlerIndex} of the attack target, or `undefined` for multi-target attacks * @param targetIndex - The {@linkcode BattlerIndex} of the attack target, or `undefined` for multi-target attacks
* @param movePosition - The 0-indexed position of the move in the pokemon's moveset array * @param movePosition - The 0-indexed position of the move in the pokemon's moveset array
* @throws Immediately fails tests
*/ */
selectTarget(movePosition: number, targetIndex?: BattlerIndex) { selectTarget(movePosition: number, targetIndex?: BattlerIndex) {
this.onNextPrompt( this.onNextPrompt(
@ -292,7 +291,7 @@ export default class GameManager {
handler.setCursor(targetIndex !== undefined ? targetIndex : BattlerIndex.ENEMY); handler.setCursor(targetIndex !== undefined ? targetIndex : BattlerIndex.ENEMY);
} }
if (move.isMultiTarget() && targetIndex !== undefined) { if (move.isMultiTarget() && targetIndex !== undefined) {
throw new Error(`targetIndex was passed to selectMove() but move ("${move.name}") is not targetted`); expect.fail(`targetIndex was passed to selectMove() but move ("${move.name}") is not targetted`);
} }
handler.processInput(Button.ACTION); handler.processInput(Button.ACTION);
}, },
@ -452,17 +451,14 @@ export default class GameManager {
} }
/** /**
* Faints a player or enemy pokemon instantly by setting their HP to 0. * Faint a player or enemy pokemon instantly by setting their HP to 0.
* @param pokemon - The player/enemy pokemon being fainted * @param pokemon - The player/enemy pokemon being fainted
* @returns A promise that resolves once the fainted pokemon's FaintPhase finishes running. * @returns A Promise that resolves once the fainted pokemon's FaintPhase finishes running.
*/ */
async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) { async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) {
return new Promise<void>(async (resolve, reject) => { pokemon.hp = 0;
pokemon.hp = 0; this.scene.phaseManager.pushNew("FaintPhase", pokemon.getBattlerIndex(), true);
this.scene.phaseManager.pushPhase(new FaintPhase(pokemon.getBattlerIndex(), true)); await this.phaseInterceptor.to("FaintPhase");
await this.phaseInterceptor.to(FaintPhase).catch(e => reject(e));
resolve();
});
} }
/** /**

View File

@ -10,7 +10,7 @@ import { getGameMode } from "#app/game-mode";
import { GameModes } from "#enums/game-modes"; import { GameModes } from "#enums/game-modes";
import type { StarterMoveset } from "#app/system/game-data"; import type { StarterMoveset } from "#app/system/game-data";
import type { Starter } from "#app/ui/starter-select-ui-handler"; import type { Starter } from "#app/ui/starter-select-ui-handler";
import { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import type { SpeciesId } from "#enums/species-id"; import type { SpeciesId } from "#enums/species-id";
/** Function to convert Blob to string */ /** Function to convert Blob to string */
@ -98,15 +98,6 @@ export function waitUntil(truth): Promise<unknown> {
}); });
} }
/** Get the index of `move` from the moveset of the pokemon on the player's field at location `pokemonIndex`. */
export function getMovePosition(scene: BattleScene, pokemonIndex: 0 | 1, move: MoveId): number {
const playerPokemon = scene.getPlayerField()[pokemonIndex];
const moveSet = playerPokemon.getMoveset();
const index = moveSet.findIndex(m => m.moveId === move && m.ppUsed < m.getMovePp());
console.log(`Move position for ${MoveId[move]} (=${move}):`, index);
return index;
}
/** /**
* Useful for populating party, wave index, etc. without having to spin up and run through an entire EncounterPhase * Useful for populating party, wave index, etc. without having to spin up and run through an entire EncounterPhase
*/ */

View File

@ -70,7 +70,7 @@ export class FieldHelper extends GameManagerHelper {
* @see {@linkcode vi.spyOn} * @see {@linkcode vi.spyOn}
* @see https://vitest.dev/api/mock#mockreturnvalue * @see https://vitest.dev/api/mock#mockreturnvalue
*/ */
public mockAbility(pokemon: Pokemon, ability: AbilityId): MockInstance<(baseOnly?: boolean) => Ability> { public mockAbility(pokemon: Pokemon, ability: AbilityId): MockInstance<(ignoreOverride?: boolean) => Ability> {
return vi.spyOn(pokemon, "getAbility").mockReturnValue(allAbilities[ability]); return vi.spyOn(pokemon, "getAbility").mockReturnValue(allAbilities[ability]);
} }

View File

@ -1,4 +1,4 @@
import type { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { getMoveTargets } from "#app/data/moves/move-utils"; import { getMoveTargets } from "#app/data/moves/move-utils";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
@ -9,14 +9,13 @@ import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { Command } from "#enums/command"; import { Command } from "#enums/command";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { getMovePosition } from "#test/testUtils/gameManagerUtils";
import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper"; import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper";
import { vi } from "vitest"; import { expect, vi } from "vitest";
import { coerceArray } from "#app/utils/common"; import { coerceArray, toReadableString } from "#app/utils/common";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
/** /**
* Helper to handle a Pokemon's move * Helper to handle using a Pokemon's moves.
*/ */
export class MoveHelper extends GameManagerHelper { export class MoveHelper extends GameManagerHelper {
/** /**
@ -49,13 +48,31 @@ export class MoveHelper extends GameManagerHelper {
} }
/** /**
* Select the move to be used by the given Pokemon(-index). Triggers during the next {@linkcode CommandPhase} * Select a move _already in the player's moveset_ to be used during the next {@linkcode CommandPhase}.
* @param move - the move to use * @param move - The {@linkcode MoveId} to use.
* @param pkmIndex - the pokemon index. Relevant for double-battles only (defaults to 0) * @param pkmIndex - The {@linkcode BattlerIndex} of the player Pokemon using the move. Relevant for double battles only and defaults to {@linkcode BattlerIndex.PLAYER} if not specified.
* @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required * @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves; should be omitted for multi-target moves.
* If set to `null`, will forgo normal target selection entirely (useful for UI tests).
* @remarks
* Will fail the current test if the move being selected is not in the user's moveset.
*/ */
public select(move: MoveId, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) { public select(
const movePosition = getMovePosition(this.game.scene, pkmIndex, move); move: MoveId,
pkmIndex: BattlerIndex.PLAYER | BattlerIndex.PLAYER_2 = BattlerIndex.PLAYER,
targetIndex?: BattlerIndex | null,
) {
const movePosition = this.getMovePosition(pkmIndex, move);
if (movePosition === -1) {
expect.fail(
`MoveHelper.select called with move '${toReadableString(MoveId[move])}' not in moveset!` +
`\nBattler Index: ${toReadableString(BattlerIndex[pkmIndex])}` +
`\nMoveset: [${this.game.scene
.getPlayerParty()
[pkmIndex].getMoveset()
.map(pm => toReadableString(MoveId[pm.moveId]))
.join(", ")}]`,
);
}
this.game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => { this.game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => {
this.game.scene.ui.setMode( this.game.scene.ui.setMode(
@ -77,14 +94,30 @@ export class MoveHelper extends GameManagerHelper {
} }
/** /**
* Select the move to be used by the given Pokemon(-index), **which will also terastallize on this turn**. * Select a move _already in the player's moveset_ to be used during the next {@linkcode CommandPhase}, **which will also terastallize on this turn**.
* Triggers during the next {@linkcode CommandPhase} * @param move - The {@linkcode MoveId} to use.
* @param move - the move to use * @param pkmIndex - The {@linkcode BattlerIndex} of the player Pokemon using the move. Relevant for double battles only and defaults to {@linkcode BattlerIndex.PLAYER} if not specified.
* @param pkmIndex - the pokemon index. Relevant for double-battles only (defaults to 0) * @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves; should be omitted for multi-target moves.
* @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required * If set to `null`, will forgo normal target selection entirely (useful for UI tests)
*/ */
public selectWithTera(move: MoveId, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) { public selectWithTera(
const movePosition = getMovePosition(this.game.scene, pkmIndex, move); move: MoveId,
pkmIndex: BattlerIndex.PLAYER | BattlerIndex.PLAYER_2 = BattlerIndex.PLAYER,
targetIndex?: BattlerIndex | null,
) {
const movePosition = this.getMovePosition(pkmIndex, move);
if (movePosition === -1) {
expect.fail(
`MoveHelper.selectWithTera called with move '${toReadableString(MoveId[move])}' not in moveset!` +
`\nBattler Index: ${toReadableString(BattlerIndex[pkmIndex])}` +
`\nMoveset: [${this.game.scene
.getPlayerParty()
[pkmIndex].getMoveset()
.map(pm => toReadableString(MoveId[pm.moveId]))
.join(", ")}]`,
);
}
this.game.scene.getPlayerParty()[pkmIndex].isTerastallized = false; this.game.scene.getPlayerParty()[pkmIndex].isTerastallized = false;
this.game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => { this.game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => {
@ -107,6 +140,15 @@ export class MoveHelper extends GameManagerHelper {
} }
} }
/** Helper function to get the index of the selected move in the selected part member's moveset. */
private getMovePosition(pokemonIndex: BattlerIndex.PLAYER | BattlerIndex.PLAYER_2, move: MoveId): number {
const playerPokemon = this.game.scene.getPlayerField()[pokemonIndex];
const moveset = playerPokemon.getMoveset();
const index = moveset.findIndex(m => m.moveId === move && m.ppUsed < m.getMovePp());
console.log(`Move position for ${MoveId[move]} (=${move}):`, index);
return index;
}
/** /**
* Modifies a player pokemon's moveset to contain only the selected move and then * Modifies a player pokemon's moveset to contain only the selected move and then
* selects it to be used during the next {@linkcode CommandPhase}. * selects it to be used during the next {@linkcode CommandPhase}.
@ -116,14 +158,19 @@ export class MoveHelper extends GameManagerHelper {
* Note: If you need to check for changes in the player's moveset as part of the test, it may be * Note: If you need to check for changes in the player's moveset as part of the test, it may be
* best to use {@linkcode changeMoveset} and {@linkcode select} instead. * best to use {@linkcode changeMoveset} and {@linkcode select} instead.
* @param moveId - the move to use * @param moveId - the move to use
* @param pkmIndex - the pokemon index. Relevant for double-battles only (defaults to 0) * @param pkmIndex - The {@linkcode BattlerIndex} of the player Pokemon using the move. Relevant for double battles only and defaults to {@linkcode BattlerIndex.PLAYER} if not specified.
* @param targetIndex - (optional) The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required * @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves; should be omitted for multi-target moves.
* @param useTera - If `true`, the Pokemon also chooses to Terastallize. This does not require a Tera Orb. Default: `false`. * @param useTera - If `true`, the Pokemon will attempt to Terastallize even without a Tera Orb; default `false`.
*/ */
public use(moveId: MoveId, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null, useTera = false): void { public use(
moveId: MoveId,
pkmIndex: BattlerIndex.PLAYER | BattlerIndex.PLAYER_2 = BattlerIndex.PLAYER,
targetIndex?: BattlerIndex,
useTera = false,
): void {
if ([Overrides.MOVESET_OVERRIDE].flat().length > 0) { if ([Overrides.MOVESET_OVERRIDE].flat().length > 0) {
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([]); vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([]);
console.warn("Warning: `use` overwrites the Pokemon's moveset and disables the player moveset override!"); console.warn("Warning: `MoveHelper.use` overwriting player pokemon moveset and disabling moveset override!");
} }
const pokemon = this.game.scene.getPlayerField()[pkmIndex]; const pokemon = this.game.scene.getPlayerField()[pkmIndex];