mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-07 16:09:27 +02:00
Merge branch 'beta' into turn-start-phase
This commit is contained in:
commit
70d49e546d
@ -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)
|
||||
- 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
|
||||
|
||||
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*
|
||||
2. Run `pnpm start:dev` to locally run the project at `localhost:8000`
|
||||
|
||||
|
@ -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);
|
@ -21,15 +21,13 @@
|
||||
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
|
||||
"docs": "typedoc",
|
||||
"depcruise": "depcruise src test",
|
||||
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg",
|
||||
"postinstall": "lefthook install && lefthook run post-merge",
|
||||
"postinstall": "lefthook install; git submodule update --init --recursive",
|
||||
"update-version:patch": "pnpm version patch --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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
"@hpcc-js/wasm": "^2.22.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.14",
|
||||
"@vitest/coverage-istanbul": "^3.0.9",
|
||||
|
@ -45,9 +45,6 @@ importers:
|
||||
'@biomejs/biome':
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
'@hpcc-js/wasm':
|
||||
specifier: ^2.22.4
|
||||
version: 2.22.4
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.7
|
||||
version: 21.1.7
|
||||
@ -436,10 +433,6 @@ packages:
|
||||
'@gerrit0/mini-shiki@3.2.2':
|
||||
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':
|
||||
resolution: {integrity: sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -2332,10 +2325,6 @@ snapshots:
|
||||
'@shikijs/types': 3.2.1
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.9(@types/node@22.13.14)
|
||||
|
6
pnpm-workspace.yaml
Normal file
6
pnpm-workspace.yaml
Normal 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 |
BIN
public/images/items/berry_juice_bad.png
Normal file
BIN
public/images/items/berry_juice_bad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 327 B |
@ -1 +1 @@
|
||||
Subproject commit fade123e20ff951e199d7c0466686fe8c5511643
|
||||
Subproject commit aa94b0b68265c26f728d154998582bb629f2b850
|
@ -797,12 +797,14 @@ export default class BattleScene extends SceneBase {
|
||||
// TODO: Add `undefined` to return type
|
||||
/**
|
||||
* Returns an array of PlayerPokemon of length 1 or 2 depending on if in a double battle or not.
|
||||
* Does not actually check if the pokemon are on the field or not.
|
||||
* @param active - (Default `false`) Whether to consider only {@linkcode Pokemon.isActive | active} on-field pokemon
|
||||
* @returns array of {@linkcode PlayerPokemon}
|
||||
*/
|
||||
public getPlayerField(): PlayerPokemon[] {
|
||||
public getPlayerField(active = false): PlayerPokemon[] {
|
||||
const party = this.getPlayerParty();
|
||||
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
|
||||
return party
|
||||
.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1))
|
||||
.filter(p => !active || p.isActive());
|
||||
}
|
||||
|
||||
public getEnemyParty(): EnemyPokemon[] {
|
||||
|
@ -95,6 +95,12 @@ export default class Battle {
|
||||
/** If the current battle is a Mystery Encounter, this will always be defined */
|
||||
public mysteryEncounter?: MysteryEncounter;
|
||||
|
||||
/**
|
||||
* Tracker for whether the last run attempt failed.
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
public failedRunAway = false;
|
||||
|
||||
private rngCounter = 0;
|
||||
|
||||
constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double = false) {
|
||||
|
@ -7263,11 +7263,14 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.MERCILESS, 7)
|
||||
.attr(ConditionalCritAbAttr, (_user, target, _move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON),
|
||||
new Ability(AbilityId.SHIELDS_DOWN, 7, -1)
|
||||
.attr(PostBattleInitFormChangeAbAttr, () => 0)
|
||||
// Change into Meteor Form on switch-in or turn end if HP >= 50%,
|
||||
// or Core Form if HP <= 50%.
|
||||
.attr(PostBattleInitFormChangeAbAttr, p => p.formIndex % 7)
|
||||
.attr(PostSummonFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0))
|
||||
.attr(PostTurnFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0))
|
||||
.conditionalAttr(p => p.formIndex !== 7, StatusEffectImmunityAbAttr)
|
||||
.conditionalAttr(p => p.formIndex !== 7, BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||
// All variants of Meteor Form are immune to status effects & Yawn
|
||||
.conditionalAttr(p => p.formIndex < 7, StatusEffectImmunityAbAttr)
|
||||
.conditionalAttr(p => p.formIndex < 7, BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||
.attr(NoFusionAbilityAbAttr)
|
||||
.attr(NoTransformAbilityAbAttr)
|
||||
.uncopiable()
|
||||
@ -7333,12 +7336,11 @@ export function initAbilities() {
|
||||
.unsuppressable()
|
||||
.bypassFaint(),
|
||||
new Ability(AbilityId.POWER_CONSTRUCT, 7)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostBattleInitFormChangeAbAttr, () => 2)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostBattleInitFormChangeAbAttr, () => 3)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 2 || pokemon.formIndex === 4, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3)
|
||||
.conditionalAttr(pokemon => pokemon.formIndex === 3 || pokemon.formIndex === 5, PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "10-complete" ? 5 : 3)
|
||||
// Change to 10% complete or 50% complete on switchout/turn end if at <50% HP;
|
||||
// revert to 10% PC or 50% PC before a new battle starts
|
||||
.conditionalAttr(p => p.formIndex === 4 || p.formIndex === 5, PostBattleInitFormChangeAbAttr, p => p.formIndex - 2)
|
||||
.conditionalAttr(p => p.getHpRatio() <= 0.5 && (p.formIndex === 2 || p.formIndex === 3), PostSummonFormChangeAbAttr, p => p.formIndex + 2)
|
||||
.conditionalAttr(p => p.getHpRatio() <= 0.5 && (p.formIndex === 2 || p.formIndex === 3), PostTurnFormChangeAbAttr, p => p.formIndex + 2)
|
||||
.attr(NoFusionAbilityAbAttr)
|
||||
.uncopiable()
|
||||
.unreplaceable()
|
||||
|
@ -54,7 +54,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
|
||||
.withFleeAllowed(false)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "berry_juice",
|
||||
spriteKey: "berry_juice_good",
|
||||
fileRoot: "items",
|
||||
hasShadow: true,
|
||||
isItem: true,
|
||||
@ -171,11 +171,11 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
|
||||
sortedParty.forEach((pokemon, index) => {
|
||||
if (index < 2) {
|
||||
// -15 to the two highest BST mons
|
||||
modifyPlayerPokemonBST(pokemon, -HIGH_BST_REDUCTION_VALUE);
|
||||
modifyPlayerPokemonBST(pokemon, false);
|
||||
encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender());
|
||||
} else {
|
||||
// +10 for the rest
|
||||
modifyPlayerPokemonBST(pokemon, BST_INCREASE_VALUE);
|
||||
modifyPlayerPokemonBST(pokemon, true);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -33,7 +33,6 @@ import {
|
||||
TransformationScreenPosition,
|
||||
} from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
|
||||
import { getLevelTotalExp } from "#app/data/exp";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { ModifierTier } from "#enums/modifier-tier";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
@ -104,8 +103,6 @@ const EXCLUDED_TRANSFORMATION_SPECIES = [
|
||||
const SUPER_LEGENDARY_BST_THRESHOLD = 600;
|
||||
const NON_LEGENDARY_BST_THRESHOLD = 570;
|
||||
|
||||
const OLD_GATEAU_STATS_UP = 20;
|
||||
|
||||
/** 0-100 */
|
||||
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
|
||||
if (shouldGetOldGateau(newPokemon)) {
|
||||
const stats = getOldGateauBoostedStats(newPokemon);
|
||||
newPokemonHeldItemConfigs.push({
|
||||
modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU, [
|
||||
OLD_GATEAU_STATS_UP,
|
||||
stats,
|
||||
]) as PokemonHeldItemModifierType,
|
||||
modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU) as PokemonHeldItemModifierType,
|
||||
stackCount: 1,
|
||||
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
|
||||
if (shouldGetOldGateau(newPokemon)) {
|
||||
const stats = getOldGateauBoostedStats(newPokemon);
|
||||
const modType = modifierTypes
|
||||
.MYSTERY_ENCOUNTER_OLD_GATEAU()
|
||||
.generateType(globalScene.getPlayerParty(), [OLD_GATEAU_STATS_UP, stats])
|
||||
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
|
||||
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU();
|
||||
const modifier = modType?.newModifier(newPokemon);
|
||||
if (modifier) {
|
||||
globalScene.addModifier(modifier, false, false, false, true);
|
||||
@ -616,22 +605,6 @@ function shouldGetOldGateau(pokemon: Pokemon): boolean {
|
||||
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(
|
||||
originalBst: number,
|
||||
bstSearchRange: [number, number],
|
||||
|
@ -375,10 +375,10 @@ export function applyHealToPokemon(pokemon: PlayerPokemon, heal: number) {
|
||||
* @param pokemon
|
||||
* @param value
|
||||
*/
|
||||
export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) {
|
||||
export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, good: boolean) {
|
||||
const modType = modifierTypes
|
||||
.MYSTERY_ENCOUNTER_SHUCKLE_JUICE()
|
||||
.generateType(globalScene.getPlayerParty(), [value])
|
||||
.generateType(globalScene.getPlayerParty(), [good ? 10 : -15])
|
||||
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE);
|
||||
const modifier = modType?.newModifier(pokemon);
|
||||
if (modifier) {
|
||||
|
@ -967,31 +967,23 @@ export class PokemonBaseStatTotalModifierType
|
||||
extends PokemonHeldItemModifierType
|
||||
implements GeneratedPersistentModifierType
|
||||
{
|
||||
private readonly statModifier: number;
|
||||
private readonly statModifier: 10 | -15;
|
||||
|
||||
constructor(statModifier: number) {
|
||||
constructor(statModifier: 10 | -15) {
|
||||
super(
|
||||
"modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE",
|
||||
"berry_juice",
|
||||
(_type, args) => new PokemonBaseStatTotalModifier(this, (args[0] as Pokemon).id, this.statModifier),
|
||||
statModifier > 0
|
||||
? "modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD"
|
||||
: "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;
|
||||
}
|
||||
|
||||
override getDescription(): string {
|
||||
return i18next.t("modifierType:ModifierType.PokemonBaseStatTotalModifierType.description", {
|
||||
increaseDecrease: i18next.t(
|
||||
this.statModifier >= 0
|
||||
? "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,
|
||||
});
|
||||
return this.statModifier > 0
|
||||
? i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_GOOD.description")
|
||||
: i18next.t("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE_BAD.description");
|
||||
}
|
||||
|
||||
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 {
|
||||
private descriptionKey: string;
|
||||
|
||||
@ -2331,17 +2291,16 @@ const modifierTypeInitObj = Object.freeze({
|
||||
MYSTERY_ENCOUNTER_SHUCKLE_JUICE: () =>
|
||||
new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => {
|
||||
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: () =>
|
||||
new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => {
|
||||
if (pregenArgs) {
|
||||
return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]);
|
||||
}
|
||||
return new PokemonBaseStatFlatModifierType(randSeedInt(20, 1), [Stat.HP, Stat.ATK, Stat.DEF]);
|
||||
}),
|
||||
new PokemonHeldItemModifierType(
|
||||
"modifierType:ModifierType.MYSTERY_ENCOUNTER_OLD_GATEAU",
|
||||
"old_gateau",
|
||||
(type, args) => new PokemonBaseStatFlatModifier(type, (args[0] as Pokemon).id),
|
||||
),
|
||||
MYSTERY_ENCOUNTER_BLACK_SLUDGE: () =>
|
||||
new ModifierTypeGenerator((_party: Pokemon[], pregenArgs?: any[]) => {
|
||||
if (pregenArgs) {
|
||||
|
@ -952,10 +952,9 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
|
||||
export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
|
||||
public override type: PokemonBaseStatTotalModifierType;
|
||||
public isTransferable = false;
|
||||
public statModifier: 10 | -15;
|
||||
|
||||
private statModifier: number;
|
||||
|
||||
constructor(type: PokemonBaseStatTotalModifierType, pokemonId: number, statModifier: number, stackCount?: number) {
|
||||
constructor(type: PokemonBaseStatTotalModifierType, pokemonId: number, statModifier: 10 | -15, stackCount?: number) {
|
||||
super(type, pokemonId, stackCount);
|
||||
this.statModifier = statModifier;
|
||||
}
|
||||
@ -1012,31 +1011,14 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
|
||||
* Currently used by Old Gateau item
|
||||
*/
|
||||
export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier {
|
||||
private statModifier: number;
|
||||
private stats: Stat[];
|
||||
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 {
|
||||
return (
|
||||
modifier instanceof PokemonBaseStatFlatModifier &&
|
||||
modifier.statModifier === this.statModifier &&
|
||||
this.stats.every(s => modifier.stats.some(stat => s === stat))
|
||||
);
|
||||
return modifier instanceof PokemonBaseStatFlatModifier;
|
||||
}
|
||||
|
||||
override clone(): PersistentModifier {
|
||||
return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount);
|
||||
}
|
||||
|
||||
override getArgs(): any[] {
|
||||
return [...super.getArgs(), this.statModifier, this.stats];
|
||||
return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.stackCount);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1055,11 +1037,13 @@ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier {
|
||||
* @param baseStats The base stats of the {@linkcode Pokemon}
|
||||
* @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
|
||||
const stats = this.getStats(pokemon);
|
||||
const statModifier = 20;
|
||||
baseStats.forEach((v, i) => {
|
||||
if (this.stats.includes(i)) {
|
||||
const newVal = Math.floor(v + this.statModifier);
|
||||
if (stats.includes(i)) {
|
||||
const newVal = Math.floor(v + statModifier);
|
||||
baseStats[i] = Math.min(Math.max(newVal, 1), 999999);
|
||||
}
|
||||
});
|
||||
@ -1067,6 +1051,22 @@ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier {
|
||||
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 {
|
||||
return 1.1;
|
||||
}
|
||||
|
@ -119,7 +119,13 @@ class DefaultOverrides {
|
||||
* or `false` to force it to never trigger.
|
||||
*/
|
||||
readonly CONFUSION_ACTIVATION_OVERRIDE: boolean | null = null;
|
||||
|
||||
/**
|
||||
* If non-null, will override random flee attempts to always or never succeed by forcing {@linkcode calculateEscapeChance} to return 100% or 0%.
|
||||
* Set to `null` to disable.
|
||||
*
|
||||
* Is overridden if either player Pokemon has {@linkcode AbilityId.RUN_AWAY | Run Away}.
|
||||
*/
|
||||
readonly RUN_SUCCESS_OVERRIDE: boolean | null = null;
|
||||
// ----------------
|
||||
// PLAYER OVERRIDES
|
||||
// ----------------
|
||||
|
@ -1,34 +1,33 @@
|
||||
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import Overrides from "#app/overrides";
|
||||
import { FieldPhase } from "#app/phases/field-phase";
|
||||
import { NumberHolder } from "#app/utils/common";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import type { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import i18next from "i18next";
|
||||
import { NumberHolder } from "#app/utils/common";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
|
||||
export class AttemptRunPhase extends PokemonPhase {
|
||||
export class AttemptRunPhase extends FieldPhase {
|
||||
public readonly phaseName = "AttemptRunPhase";
|
||||
/** For testing purposes: this is to force the pokemon to fail and escape */
|
||||
public forceFailEscape = false;
|
||||
|
||||
start() {
|
||||
public start() {
|
||||
super.start();
|
||||
|
||||
const playerField = globalScene.getPlayerField();
|
||||
// Increment escape attempts count on entry
|
||||
const currentAttempts = globalScene.currentBattle.escapeAttempts++;
|
||||
|
||||
const activePlayerField = globalScene.getPlayerField(true);
|
||||
const enemyField = globalScene.getEnemyField();
|
||||
|
||||
const playerPokemon = this.getPokemon();
|
||||
const escapeRoll = globalScene.randBattleSeedInt(100);
|
||||
const escapeChance = new NumberHolder(this.calculateEscapeChance(currentAttempts));
|
||||
|
||||
const escapeChance = new NumberHolder(0);
|
||||
activePlayerField.forEach(pokemon => {
|
||||
applyAbAttrs("RunSuccessAbAttr", { pokemon, chance: escapeChance });
|
||||
});
|
||||
|
||||
this.attemptRunAway(playerField, enemyField, escapeChance);
|
||||
|
||||
applyAbAttrs("RunSuccessAbAttr", { pokemon: playerPokemon, chance: escapeChance });
|
||||
|
||||
if (playerPokemon.randBattleSeedInt(100) < escapeChance.value && !this.forceFailEscape) {
|
||||
enemyField.forEach(enemyPokemon => applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: enemyPokemon }));
|
||||
if (escapeRoll < escapeChance.value) {
|
||||
enemyField.forEach(pokemon => applyAbAttrs("PreLeaveFieldAbAttr", { pokemon }));
|
||||
|
||||
globalScene.playSound("se/flee");
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500);
|
||||
@ -57,24 +56,35 @@ export class AttemptRunPhase extends PokemonPhase {
|
||||
|
||||
globalScene.phaseManager.pushNew("NewBattlePhase");
|
||||
} else {
|
||||
playerPokemon.turnData.failedRunAway = true;
|
||||
activePlayerField.forEach(p => {
|
||||
p.turnData.failedRunAway = true;
|
||||
});
|
||||
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500);
|
||||
}
|
||||
|
||||
this.end();
|
||||
}
|
||||
|
||||
attemptRunAway(playerField: PlayerPokemon[], enemyField: EnemyPokemon[], escapeChance: NumberHolder) {
|
||||
/** Sum of the speed of all enemy pokemon on the field */
|
||||
const enemySpeed = enemyField.reduce(
|
||||
(total: number, enemyPokemon: Pokemon) => total + enemyPokemon.getStat(Stat.SPD),
|
||||
0,
|
||||
);
|
||||
/** Sum of the speed of all player pokemon on the field */
|
||||
const playerSpeed = playerField.reduce(
|
||||
(total: number, playerPokemon: Pokemon) => total + playerPokemon.getStat(Stat.SPD),
|
||||
0,
|
||||
);
|
||||
/**
|
||||
* Calculate the chance for the player's team to successfully run away from battle.
|
||||
*
|
||||
* @param escapeAttempts - The number of prior failed escape attempts in the current battle
|
||||
* @returns The final escape chance, as percentage out of 100.
|
||||
*/
|
||||
public calculateEscapeChance(escapeAttempts: number): number {
|
||||
// Check for override, guaranteeing or forbidding random flee attempts as applicable.
|
||||
if (Overrides.RUN_SUCCESS_OVERRIDE !== null) {
|
||||
return Overrides.RUN_SUCCESS_OVERRIDE ? 100 : 0;
|
||||
}
|
||||
|
||||
const enemyField = globalScene.getEnemyField();
|
||||
const activePlayerField = globalScene.getPlayerField(true);
|
||||
|
||||
// Cf https://bulbapedia.bulbagarden.net/wiki/Escape#Generation_V_onwards
|
||||
// From gen 5 onwards, running takes the _base_ speed totals of both party sides.
|
||||
const enemySpeed = enemyField.reduce((total, enemy) => total + enemy.getStat(Stat.SPD), 0);
|
||||
const playerSpeed = activePlayerField.reduce((total, player) => total + player.getStat(Stat.SPD), 0);
|
||||
|
||||
/* The way the escape chance works is by looking at the difference between your speed and the enemy field's average speed as a ratio. The higher this ratio, the higher your chance of success.
|
||||
* However, there is a cap for the ratio of your speed vs enemy speed which beyond that point, you won't gain any advantage. It also looks at how many times you've tried to escape.
|
||||
@ -92,10 +102,8 @@ export class AttemptRunPhase extends PokemonPhase {
|
||||
* From the above, we can calculate the below values
|
||||
*/
|
||||
|
||||
let isBoss = false;
|
||||
for (let e = 0; e < enemyField.length; e++) {
|
||||
isBoss = isBoss || enemyField[e].isBoss(); // this line checks if any of the enemy pokemon on the field are bosses; if so, the calculation for escaping is different
|
||||
}
|
||||
/** Whether at least 1 pokemon on the enemy field is a boss. */
|
||||
const isBoss = enemyField.some(e => e.isBoss());
|
||||
|
||||
/** The ratio between the speed of your active pokemon and the speed of the enemy field */
|
||||
const speedRatio = playerSpeed / enemySpeed;
|
||||
@ -111,8 +119,8 @@ export class AttemptRunPhase extends PokemonPhase {
|
||||
const escapeSlope = (maxChance - minChance) / speedCap;
|
||||
|
||||
// This will calculate the escape chance given all of the above and clamp it to the range of [`minChance`, `maxChance`]
|
||||
escapeChance.value = Phaser.Math.Clamp(
|
||||
Math.round(escapeSlope * speedRatio + minChance + escapeBonus * globalScene.currentBattle.escapeAttempts++),
|
||||
return Phaser.Math.Clamp(
|
||||
Math.round(escapeSlope * speedRatio + minChance + escapeBonus * escapeAttempts),
|
||||
minChance,
|
||||
maxChance,
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||
@ -207,22 +206,7 @@ export class TurnStartPhase extends FieldPhase {
|
||||
);
|
||||
break;
|
||||
case Command.RUN:
|
||||
{
|
||||
const playerActivePokemon = globalScene.getPokemonAllowedInBattle();
|
||||
if (!globalScene.currentBattle.double || playerActivePokemon.length === 1) {
|
||||
// If not in doubles, attempt to run with the currently active Pokemon.
|
||||
globalScene.phaseManager.unshiftNew("AttemptRunPhase", pokemon.getFieldIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the fastest first pokemon we find with Run Away, or else the faster of the 2 player pokemon.
|
||||
// This intentionally does not check for Trick Room.
|
||||
// TODO: This phase should not take a pokemon at all
|
||||
const sortedPkmn = playerActivePokemon.sort((p1, p2) => p1.getStat(Stat.SPD) - p2.getStat(Stat.SPD));
|
||||
const runningPokemon = sortedPkmn.find(p => p.hasAbility(AbilityId.RUN_AWAY)) ?? sortedPkmn[0];
|
||||
|
||||
globalScene.phaseManager.unshiftNew("AttemptRunPhase", runningPokemon.getFieldIndex());
|
||||
}
|
||||
globalScene.phaseManager.unshiftNew("AttemptRunPhase");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { SpeciesId } from "#enums/species-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
|
||||
describe("Abilities - Desolate Land", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -145,6 +146,7 @@ describe("Abilities - Desolate Land", () => {
|
||||
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN);
|
||||
|
||||
vi.spyOn(game.scene.getPlayerPokemon()!, "randBattleSeedInt").mockReturnValue(0);
|
||||
vi.spyOn(globalScene, "randBattleSeedInt").mockReturnValue(0);
|
||||
|
||||
const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase;
|
||||
commandPhase.handleCommand(Command.RUN, 0);
|
||||
|
@ -45,7 +45,7 @@ describe("Abilities - Good As Gold", () => {
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(MoveId.SPLASH, 0);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
|
||||
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 () => {
|
||||
game.override.enemyMoveset([MoveId.MEMENTO]);
|
||||
game.override.enemyAbility(AbilityId.GOOD_AS_GOLD);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
game.move.select(MoveId.MEMENTO);
|
||||
|
||||
game.move.use(MoveId.MEMENTO);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(game.scene.getPlayerPokemon()!.isFainted()).toBe(false);
|
||||
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(0);
|
||||
expect(game.field.getPlayerPokemon().isFainted()).toBe(false);
|
||||
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 () => {
|
||||
|
@ -6,6 +6,7 @@ import { SpeciesId } from "#enums/species-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import Overrides from "#app/overrides";
|
||||
|
||||
describe("Abilities - Honey Gather", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -63,6 +64,8 @@ describe("Abilities - Honey Gather", () => {
|
||||
// something weird is going on with the test framework, so this is required to prevent a crash
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "scene", "get").mockReturnValue(game.scene);
|
||||
// Expects next wave so run must succeed
|
||||
vi.spyOn(Overrides, "RUN_SUCCESS_OVERRIDE", "get").mockReturnValue(true);
|
||||
|
||||
const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase;
|
||||
commandPhase.handleCommand(Command.RUN, 0);
|
||||
|
@ -259,7 +259,7 @@ describe("Abilities - Ice Face", () => {
|
||||
|
||||
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.hasAbility(AbilityId.ICE_FACE)).toBe(true);
|
||||
});
|
||||
@ -269,13 +269,9 @@ describe("Abilities - Ice Face", () => {
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SIMPLE_BEAM);
|
||||
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
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(game.scene.getPlayerPokemon()!.hasAbility(AbilityId.TRACE)).toBe(true);
|
||||
});
|
||||
|
@ -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 () => {
|
||||
game.override.enemyMoveset([MoveId.POWER_SPLIT]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.DITTO]);
|
||||
|
||||
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 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);
|
||||
|
||||
expect(player.getStat(Stat.ATK, false)).toBe(avgAtk);
|
||||
@ -101,9 +101,6 @@ describe("Abilities - Imposter", () => {
|
||||
await game.classicMode.startBattle([SpeciesId.DITTO]);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
player.getMoveset().forEach(move => {
|
||||
// Should set correct maximum PP without touching `ppUp`
|
||||
if (move) {
|
||||
@ -122,15 +119,10 @@ describe("Abilities - Imposter", () => {
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it("should persist transformed attributes across reloads", async () => {
|
||||
game.override.moveset([MoveId.ABSORB]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.DITTO]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
@ -162,7 +154,7 @@ describe("Abilities - Imposter", () => {
|
||||
});
|
||||
|
||||
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]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
@ -3,7 +3,6 @@ import Phaser from "phaser";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { getMovePosition } from "#test/testUtils/gameManagerUtils";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
@ -114,7 +113,7 @@ describe("Abilities - Intimidate", () => {
|
||||
expect(enemyPokemon.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();
|
||||
|
||||
enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
@ -36,10 +36,8 @@ describe("Abilities - Lightningrod", () => {
|
||||
it("should redirect electric type moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemy1 = game.scene.getEnemyField()[0];
|
||||
const enemy2 = game.scene.getEnemyField()[1];
|
||||
|
||||
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD;
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||
game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
|
||||
|
||||
game.move.select(MoveId.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
@ -52,10 +50,8 @@ describe("Abilities - Lightningrod", () => {
|
||||
game.override.moveset([MoveId.SPLASH, MoveId.AERIAL_ACE]);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemy1 = game.scene.getEnemyField()[0];
|
||||
const enemy2 = game.scene.getEnemyField()[1];
|
||||
|
||||
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD;
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||
game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
|
||||
|
||||
game.move.select(MoveId.AERIAL_ACE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
@ -68,8 +64,7 @@ describe("Abilities - Lightningrod", () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemy2 = game.scene.getEnemyField()[1];
|
||||
|
||||
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD;
|
||||
game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
|
||||
|
||||
game.move.select(MoveId.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
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 () => {
|
||||
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 enemy2 = game.scene.getEnemyField()[1];
|
||||
|
||||
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD;
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||
game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
|
||||
|
||||
game.move.select(MoveId.SHOCK_WAVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy1.isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
it("should redirect moves changed to electric type via ability", async () => {
|
||||
game.override.ability(AbilityId.GALVANIZE).moveset(MoveId.TACKLE);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
|
||||
game.override.ability(AbilityId.GALVANIZE);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const enemy1 = game.scene.getEnemyField()[0];
|
||||
const enemy2 = game.scene.getEnemyField()[1];
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||
game.field.mockAbility(enemy2, AbilityId.LIGHTNING_ROD);
|
||||
|
||||
enemy2.summonData.ability = AbilityId.LIGHTNING_ROD;
|
||||
|
||||
game.move.select(MoveId.TACKLE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
game.move.use(MoveId.TACKLE, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy1.isFullHp()).toBe(true);
|
||||
|
@ -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 { SpeciesId } from "#enums/species-id";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -24,29 +22,28 @@ describe("Abilities - Mold Breaker", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([MoveId.SPLASH])
|
||||
.ability(AbilityId.MOLD_BREAKER)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyAbility(AbilityId.STURDY)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("should turn off the ignore abilities arena variable after the user's move", async () => {
|
||||
game.override
|
||||
.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()!;
|
||||
await game.classicMode.startBattle([SpeciesId.PINSIR]);
|
||||
|
||||
expect(enemy.isFainted()).toBe(false);
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase", true);
|
||||
expect(globalScene.arena.ignoreAbilities).toBe(false);
|
||||
const player = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.use(MoveId.X_SCISSOR);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -65,8 +65,7 @@ describe("Abilities - Moxie", () => {
|
||||
|
||||
secondPokemon.hp = 1;
|
||||
|
||||
game.move.select(moveToUse);
|
||||
game.selectTarget(BattlerIndex.PLAYER_2);
|
||||
game.move.select(moveToUse, BattlerIndex.PLAYER_2);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
|
@ -10,6 +10,7 @@ import { Stat } from "#enums/stat";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
|
||||
describe("Abilities - Neutralizing Gas", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -164,6 +165,7 @@ describe("Abilities - Neutralizing Gas", () => {
|
||||
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeDefined();
|
||||
|
||||
vi.spyOn(game.scene.getPlayerPokemon()!, "randBattleSeedInt").mockReturnValue(0);
|
||||
vi.spyOn(globalScene, "randBattleSeedInt").mockReturnValue(0);
|
||||
|
||||
const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase;
|
||||
commandPhase.handleCommand(Command.RUN, 0);
|
||||
|
@ -28,11 +28,11 @@ describe("Abilities - Screen Cleaner", () => {
|
||||
});
|
||||
|
||||
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]);
|
||||
|
||||
game.move.select(MoveId.HAIL);
|
||||
game.move.use(MoveId.HAIL);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
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]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
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]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.REFLECT)).toBeDefined();
|
||||
|
@ -4,10 +4,11 @@ import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CommandPhase } from "#app/phases/command-phase";
|
||||
import { Command } from "#enums/command";
|
||||
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
|
||||
import Overrides from "#app/overrides";
|
||||
|
||||
describe("Abilities - Speed Boost", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -96,12 +97,15 @@ describe("Abilities - Speed Boost", () => {
|
||||
});
|
||||
|
||||
it("should not trigger if pokemon fails to escape", async () => {
|
||||
//Account for doubles, should not trigger on either pokemon
|
||||
game.override.battleStyle("double");
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||
|
||||
vi.spyOn(Overrides, "RUN_SUCCESS_OVERRIDE", "get").mockReturnValue(false);
|
||||
|
||||
const commandPhase = game.scene.phaseManager.getCurrentPhase() as CommandPhase;
|
||||
commandPhase.handleCommand(Command.RUN, 0);
|
||||
const runPhase = game.scene.phaseManager.getCurrentPhase() as AttemptRunPhase;
|
||||
runPhase.forceFailEscape = true;
|
||||
|
||||
await game.phaseInterceptor.to(AttemptRunPhase);
|
||||
await game.toNextTurn();
|
||||
|
||||
|
@ -7,6 +7,7 @@ import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
// TODO: Condense with lightning rod tests sometime
|
||||
describe("Abilities - Storm Drain", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
@ -39,7 +40,7 @@ describe("Abilities - Storm Drain", () => {
|
||||
const enemy1 = game.scene.getEnemyField()[0];
|
||||
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.SPLASH, BattlerIndex.PLAYER_2);
|
||||
@ -55,7 +56,7 @@ describe("Abilities - Storm Drain", () => {
|
||||
const enemy1 = game.scene.getEnemyField()[0];
|
||||
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.SPLASH, BattlerIndex.PLAYER_2);
|
||||
@ -68,8 +69,7 @@ describe("Abilities - Storm Drain", () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
|
||||
|
||||
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.SPLASH, BattlerIndex.PLAYER_2);
|
||||
@ -85,8 +85,7 @@ describe("Abilities - Storm Drain", () => {
|
||||
|
||||
const enemy1 = game.scene.getEnemyField()[0];
|
||||
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.SPLASH, BattlerIndex.PLAYER_2);
|
||||
@ -96,16 +95,15 @@ describe("Abilities - Storm Drain", () => {
|
||||
});
|
||||
|
||||
it("should redirect moves changed to water type via ability", async () => {
|
||||
game.override.ability(AbilityId.LIQUID_VOICE).moveset(MoveId.PSYCHIC_NOISE);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]);
|
||||
game.override.ability(AbilityId.LIQUID_VOICE);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const enemy1 = game.scene.getEnemyField()[0];
|
||||
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.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
game.move.use(MoveId.ROUND, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy1.isFullHp()).toBe(true);
|
||||
|
@ -86,7 +86,7 @@ describe("Weather - Strong Winds", () => {
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
enemy.hp = 1;
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.weather?.weatherType).toBeUndefined();
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
|
||||
import type { CommandPhase } from "#app/phases/command-phase";
|
||||
import { Command } from "#enums/command";
|
||||
import { NumberHolder } from "#app/utils/common";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
@ -45,8 +44,6 @@ describe("Escape chance calculations", () => {
|
||||
|
||||
await game.phaseInterceptor.to(AttemptRunPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as AttemptRunPhase;
|
||||
const escapePercentage = new NumberHolder(0);
|
||||
|
||||
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
|
||||
const escapeChances: {
|
||||
pokemonSpeedRatio: number;
|
||||
@ -91,8 +88,8 @@ describe("Escape chance calculations", () => {
|
||||
20,
|
||||
escapeChances[i].pokemonSpeedRatio * enemySpeed,
|
||||
]);
|
||||
phase.attemptRunAway(playerPokemon, enemyField, escapePercentage);
|
||||
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance);
|
||||
const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts);
|
||||
expect(chance).toBe(escapeChances[i].expectedEscapeChance);
|
||||
}
|
||||
});
|
||||
|
||||
@ -118,8 +115,6 @@ describe("Escape chance calculations", () => {
|
||||
|
||||
await game.phaseInterceptor.to(AttemptRunPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as AttemptRunPhase;
|
||||
const escapePercentage = new NumberHolder(0);
|
||||
|
||||
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
|
||||
const escapeChances: {
|
||||
pokemonSpeedRatio: number;
|
||||
@ -172,9 +167,9 @@ describe("Escape chance calculations", () => {
|
||||
20,
|
||||
escapeChances[i].pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5],
|
||||
]);
|
||||
phase.attemptRunAway(playerPokemon, enemyField, escapePercentage);
|
||||
const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts);
|
||||
// checks to make sure the escape values are the same
|
||||
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance);
|
||||
expect(chance).toBe(escapeChances[i].expectedEscapeChance);
|
||||
// checks to make sure the sum of the player's speed for all pokemon is equal to the appropriate ratio of the total enemy speed
|
||||
expect(playerPokemon[0].stats[5] + playerPokemon[1].stats[5]).toBe(
|
||||
escapeChances[i].pokemonSpeedRatio * totalEnemySpeed,
|
||||
@ -197,7 +192,6 @@ describe("Escape chance calculations", () => {
|
||||
|
||||
await game.phaseInterceptor.to(AttemptRunPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as AttemptRunPhase;
|
||||
const escapePercentage = new NumberHolder(0);
|
||||
|
||||
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
|
||||
const escapeChances: {
|
||||
@ -256,8 +250,8 @@ describe("Escape chance calculations", () => {
|
||||
20,
|
||||
escapeChances[i].pokemonSpeedRatio * enemySpeed,
|
||||
]);
|
||||
phase.attemptRunAway(playerPokemon, enemyField, escapePercentage);
|
||||
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance);
|
||||
const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts);
|
||||
expect(chance).toBe(escapeChances[i].expectedEscapeChance);
|
||||
}
|
||||
});
|
||||
|
||||
@ -283,7 +277,6 @@ describe("Escape chance calculations", () => {
|
||||
|
||||
await game.phaseInterceptor.to(AttemptRunPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as AttemptRunPhase;
|
||||
const escapePercentage = new NumberHolder(0);
|
||||
|
||||
// this sets up an object for multiple attempts. The pokemonSpeedRatio is your speed divided by the enemy speed, the escapeAttempts are the number of escape attempts and the expectedEscapeChance is the chance it should be escaping
|
||||
const escapeChances: {
|
||||
@ -349,9 +342,9 @@ describe("Escape chance calculations", () => {
|
||||
20,
|
||||
escapeChances[i].pokemonSpeedRatio * totalEnemySpeed - playerPokemon[0].stats[5],
|
||||
]);
|
||||
phase.attemptRunAway(playerPokemon, enemyField, escapePercentage);
|
||||
const chance = phase.calculateEscapeChance(game.scene.currentBattle.escapeAttempts);
|
||||
// checks to make sure the escape values are the same
|
||||
expect(escapePercentage.value).toBe(escapeChances[i].expectedEscapeChance);
|
||||
expect(chance).toBe(escapeChances[i].expectedEscapeChance);
|
||||
// checks to make sure the sum of the player's speed for all pokemon is equal to the appropriate ratio of the total enemy speed
|
||||
expect(playerPokemon[0].stats[5] + playerPokemon[1].stats[5]).toBe(
|
||||
escapeChances[i].pokemonSpeedRatio * totalEnemySpeed,
|
||||
|
@ -58,8 +58,7 @@ describe("Items - Dire Hit", () => {
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.PIKACHU]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.doKillOpponents();
|
||||
|
||||
await game.phaseInterceptor.to(BattleEndPhase);
|
||||
|
@ -4,7 +4,6 @@ import type Move from "#app/data/moves/move";
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { ArenaTagType } from "#app/enums/arena-tag-type";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import { NumberHolder } from "#app/utils/common";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
@ -12,7 +11,7 @@ import { SpeciesId } from "#enums/species-id";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
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;
|
||||
|
||||
@ -52,10 +51,10 @@ describe("Moves - Aurora Veil", () => {
|
||||
|
||||
game.move.select(moveToUse);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.toEndOfTurn();
|
||||
const mockedDmg = getMockedMoveDamage(
|
||||
game.scene.getEnemyPokemon()!,
|
||||
game.scene.getPlayerPokemon()!,
|
||||
game.field.getEnemyPokemon(),
|
||||
game.field.getPlayerPokemon(),
|
||||
allMoves[moveToUse],
|
||||
);
|
||||
|
||||
@ -71,10 +70,10 @@ describe("Moves - Aurora Veil", () => {
|
||||
game.move.select(moveToUse);
|
||||
game.move.select(moveToUse, 1);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.toEndOfTurn();
|
||||
const mockedDmg = getMockedMoveDamage(
|
||||
game.scene.getEnemyPokemon()!,
|
||||
game.scene.getPlayerPokemon()!,
|
||||
game.field.getEnemyPokemon(),
|
||||
game.field.getPlayerPokemon(),
|
||||
allMoves[moveToUse],
|
||||
);
|
||||
|
||||
@ -82,72 +81,48 @@ describe("Moves - Aurora Veil", () => {
|
||||
});
|
||||
|
||||
it("reduces damage of special attacks by half in a single battle", async () => {
|
||||
const moveToUse = MoveId.ABSORB;
|
||||
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(
|
||||
game.scene.getEnemyPokemon()!,
|
||||
game.scene.getPlayerPokemon()!,
|
||||
allMoves[moveToUse],
|
||||
game.field.getEnemyPokemon(),
|
||||
game.field.getPlayerPokemon(),
|
||||
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 () => {
|
||||
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]);
|
||||
|
||||
game.move.select(moveToUse);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
game.move.use(MoveId.ABSORB);
|
||||
await game.toEndOfTurn();
|
||||
const mockedDmg = getMockedMoveDamage(
|
||||
game.scene.getEnemyPokemon()!,
|
||||
game.scene.getPlayerPokemon()!,
|
||||
allMoves[moveToUse],
|
||||
game.field.getEnemyPokemon(),
|
||||
game.field.getPlayerPokemon(),
|
||||
allMoves[MoveId.ABSORB],
|
||||
);
|
||||
expect(mockedDmg).toBe(allMoves[moveToUse].power);
|
||||
|
||||
expect(mockedDmg).toBe(allMoves[MoveId.ABSORB].power * doubleBattleMultiplier);
|
||||
});
|
||||
|
||||
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]);
|
||||
|
||||
game.move.select(moveToUse);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.move.use(MoveId.WICKED_BLOW);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const mockedDmg = getMockedMoveDamage(
|
||||
game.scene.getEnemyPokemon()!,
|
||||
game.scene.getPlayerPokemon()!,
|
||||
allMoves[moveToUse],
|
||||
game.field.getEnemyPokemon(),
|
||||
game.field.getPlayerPokemon(),
|
||||
allMoves[MoveId.WICKED_BLOW],
|
||||
);
|
||||
expect(mockedDmg).toBe(allMoves[moveToUse].power);
|
||||
expect(mockedDmg).toBe(allMoves[MoveId.WICKED_BLOW].power);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -33,7 +33,7 @@ describe("Moves - Baddy Bad", () => {
|
||||
game.override.enemyMoveset(MoveId.PROTECT);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.select(MoveId.BADDY_BAD);
|
||||
game.move.use(MoveId.BADDY_BAD);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(game.scene.arena.tags.length).toBe(0);
|
||||
|
@ -100,12 +100,12 @@ describe("Moves - Gastro Acid", () => {
|
||||
await game.toNextTurn();
|
||||
expect(enemyPokemon.summonData.abilitySuppressed).toBe(true);
|
||||
|
||||
game.move.select(MoveId.WATER_GUN);
|
||||
game.move.use(MoveId.WATER_GUN);
|
||||
await game.toNextTurn();
|
||||
// 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();
|
||||
|
||||
// Comatose should block stauts effect
|
||||
|
@ -361,7 +361,7 @@ describe("Moves - Instruct", () => {
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
});
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
expect(game.field.getEnemyPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
|
@ -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 () => {
|
||||
game.override
|
||||
.moveset([MoveId.FORESTS_CURSE, MoveId.REFLECT_TYPE])
|
||||
.startingLevel(60)
|
||||
.enemySpecies(SpeciesId.CHARMANDER)
|
||||
.enemyMoveset([MoveId.BURN_UP, MoveId.SPLASH]);
|
||||
game.override.startingLevel(60).enemySpecies(SpeciesId.CHARMANDER);
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon();
|
||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.selectEnemyMove(MoveId.BURN_UP);
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.BURN_UP);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(MoveId.FORESTS_CURSE);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
game.move.use(MoveId.FORESTS_CURSE);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
expect(enemyPokemon?.getTypes().includes(PokemonType.UNKNOWN)).toBe(true);
|
||||
expect(enemyPokemon?.getTypes().includes(PokemonType.GRASS)).toBe(true);
|
||||
expect(enemyPokemon.getTypes().includes(PokemonType.UNKNOWN)).toBe(true);
|
||||
expect(enemyPokemon.getTypes().includes(PokemonType.GRASS)).toBe(true);
|
||||
|
||||
game.move.select(MoveId.REFLECT_TYPE);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
game.move.use(MoveId.REFLECT_TYPE);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon?.getTypes()[0]).toBe(PokemonType.NORMAL);
|
||||
expect(playerPokemon?.getTypes().includes(PokemonType.GRASS)).toBe(true);
|
||||
expect(playerPokemon.getTypes()[0]).toBe(PokemonType.NORMAL);
|
||||
expect(playerPokemon.getTypes().includes(PokemonType.GRASS)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { ArenaTrapTag } from "#app/data/arena-tag";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
|
||||
describe("Moves - Spikes", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -80,14 +81,19 @@ describe("Moves - Spikes", () => {
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
});
|
||||
|
||||
it("should work when all targets fainted", async () => {
|
||||
game.override.enemySpecies(SpeciesId.DIGLETT).battleStyle("double").startingLevel(50);
|
||||
await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.ROWLET]);
|
||||
// TODO: re-enable after re-fixing hazards moves
|
||||
it.todo("should work when all targets fainted", async () => {
|
||||
game.override.enemySpecies(SpeciesId.DIGLETT).battleStyle("double").startingLevel(1000);
|
||||
await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.SHUCKLE]);
|
||||
|
||||
game.move.select(MoveId.EARTHQUAKE);
|
||||
game.move.select(MoveId.SPIKES, 1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
@ -12,7 +12,6 @@ import overrides from "#app/overrides";
|
||||
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { EncounterPhase } from "#app/phases/encounter-phase";
|
||||
import { FaintPhase } from "#app/phases/faint-phase";
|
||||
import { LoginPhase } from "#app/phases/login-phase";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
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
|
||||
* Also handles Major/Minor bosses from endless modes
|
||||
* @param game - The game manager
|
||||
* @param species
|
||||
* @param mode
|
||||
* @param species - Array of {@linkcode SpeciesId}s to start the final battle with.
|
||||
* @param mode - The {@linkcode GameModes} to spawn the final boss encounter in.
|
||||
*/
|
||||
async runToFinalBossEncounter(species: SpeciesId[], mode: GameModes) {
|
||||
console.log("===to final boss encounter===");
|
||||
@ -230,9 +228,9 @@ export default class GameManager {
|
||||
|
||||
/**
|
||||
* Runs the game to a mystery encounter phase.
|
||||
* @param encounterType if specified, will expect encounter to have been spawned
|
||||
* @param species Optional array of species for party.
|
||||
* @returns A promise that resolves when the EncounterPhase ends.
|
||||
* @param encounterType - If specified, will expect encounter to be the given type.
|
||||
* @param species - Optional array of species for party to start with.
|
||||
* @returns A Promise that resolves when the EncounterPhase ends.
|
||||
*/
|
||||
async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: SpeciesId[]) {
|
||||
if (!isNullOrUndefined(encounterType)) {
|
||||
@ -277,6 +275,7 @@ export default class GameManager {
|
||||
* Will trigger during the next {@linkcode SelectTargetPhase}
|
||||
* @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
|
||||
* @throws Immediately fails tests
|
||||
*/
|
||||
selectTarget(movePosition: number, targetIndex?: BattlerIndex) {
|
||||
this.onNextPrompt(
|
||||
@ -292,7 +291,7 @@ export default class GameManager {
|
||||
handler.setCursor(targetIndex !== undefined ? targetIndex : BattlerIndex.ENEMY);
|
||||
}
|
||||
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);
|
||||
},
|
||||
@ -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
|
||||
* @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) {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
pokemon.hp = 0;
|
||||
this.scene.phaseManager.pushPhase(new FaintPhase(pokemon.getBattlerIndex(), true));
|
||||
await this.phaseInterceptor.to(FaintPhase).catch(e => reject(e));
|
||||
resolve();
|
||||
});
|
||||
pokemon.hp = 0;
|
||||
this.scene.phaseManager.pushNew("FaintPhase", pokemon.getBattlerIndex(), true);
|
||||
await this.phaseInterceptor.to("FaintPhase");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,7 +10,7 @@ import { getGameMode } from "#app/game-mode";
|
||||
import { GameModes } from "#enums/game-modes";
|
||||
import type { StarterMoveset } from "#app/system/game-data";
|
||||
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";
|
||||
|
||||
/** 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
|
||||
*/
|
||||
|
@ -70,7 +70,7 @@ export class FieldHelper extends GameManagerHelper {
|
||||
* @see {@linkcode vi.spyOn}
|
||||
* @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]);
|
||||
}
|
||||
|
||||
|
@ -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 type Pokemon from "#app/field/pokemon";
|
||||
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 { MoveId } from "#enums/move-id";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { getMovePosition } from "#test/testUtils/gameManagerUtils";
|
||||
import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper";
|
||||
import { vi } from "vitest";
|
||||
import { coerceArray } from "#app/utils/common";
|
||||
import { expect, vi } from "vitest";
|
||||
import { coerceArray, toReadableString } from "#app/utils/common";
|
||||
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 {
|
||||
/**
|
||||
@ -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}
|
||||
* @param move - the move to use
|
||||
* @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, or `null` if a manual call to `selectTarget()` is required
|
||||
* Select a move _already in the player's moveset_ to be used during the next {@linkcode CommandPhase}.
|
||||
* @param move - The {@linkcode MoveId} 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 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) {
|
||||
const movePosition = getMovePosition(this.game.scene, pkmIndex, move);
|
||||
public select(
|
||||
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.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**.
|
||||
* Triggers during the next {@linkcode CommandPhase}
|
||||
* @param move - the move to use
|
||||
* @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, or `null` if a manual call to `selectTarget()` is required
|
||||
* Select a move _already in the player's moveset_ to be used during the next {@linkcode CommandPhase}, **which will also terastallize on this turn**.
|
||||
* @param move - The {@linkcode MoveId} 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 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)
|
||||
*/
|
||||
public selectWithTera(move: MoveId, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) {
|
||||
const movePosition = getMovePosition(this.game.scene, pkmIndex, move);
|
||||
public selectWithTera(
|
||||
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.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
|
||||
* 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
|
||||
* best to use {@linkcode changeMoveset} and {@linkcode select} instead.
|
||||
* @param moveId - the move to use
|
||||
* @param pkmIndex - the pokemon index. Relevant for double-battles only (defaults to 0)
|
||||
* @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 useTera - If `true`, the Pokemon also chooses to Terastallize. This does not require a Tera Orb. Default: `false`.
|
||||
* @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; should be omitted for multi-target moves.
|
||||
* @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) {
|
||||
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];
|
||||
|
Loading…
Reference in New Issue
Block a user