Merge branch 'beta' into turn-start-phase

This commit is contained in:
Bertie690 2025-06-16 08:16:33 -04:00 committed by GitHub
commit 4ab2a33578
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
250 changed files with 2877 additions and 2376 deletions

View File

@ -4,7 +4,7 @@ module.exports = {
{ {
name: "only-type-imports", name: "only-type-imports",
severity: "error", severity: "error",
comment: "Files in enums and @types may only use type imports.", comment: "Files in 'enums/' and '@types/' must only use type imports.",
from: { from: {
path: ["(^|/)src/@types", "(^|/)src/enums"], path: ["(^|/)src/@types", "(^|/)src/enums"],
}, },
@ -14,7 +14,7 @@ module.exports = {
}, },
{ {
name: "no-circular-at-runtime", name: "no-circular-at-runtime",
severity: "warn", severity: "error",
comment: comment:
"This dependency is part of a circular relationship. You might want to revise " + "This dependency is part of a circular relationship. You might want to revise " +
"your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ",
@ -34,7 +34,7 @@ module.exports = {
"add an exception for it in your dependency-cruiser configuration. By default " + "add an exception for it in your dependency-cruiser configuration. By default " +
"this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " +
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.", "files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
severity: "warn", severity: "error",
from: { from: {
orphan: true, orphan: true,
pathNot: [ pathNot: [
@ -42,8 +42,7 @@ module.exports = {
"[.]d[.]ts$", // TypeScript declaration files "[.]d[.]ts$", // TypeScript declaration files
"(^|/)tsconfig[.]json$", // TypeScript config "(^|/)tsconfig[.]json$", // TypeScript config
"(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs "(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs
// anything in src/@types "(^|/)test/.+[.]setup[.]ts", // Vitest setup files
"(^|/)src/@types/",
], ],
}, },
to: {}, to: {},
@ -53,7 +52,7 @@ module.exports = {
comment: comment:
"A module depends on a node core module that has been deprecated. Find an alternative - these are " + "A module depends on a node core module that has been deprecated. Find an alternative - these are " +
"bound to exist - node doesn't deprecate lightly.", "bound to exist - node doesn't deprecate lightly.",
severity: "warn", severity: "error",
from: {}, from: {},
to: { to: {
dependencyTypes: ["core"], dependencyTypes: ["core"],
@ -86,7 +85,7 @@ module.exports = {
comment: comment:
"This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " + "This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " +
"version of that module, or find an alternative. Deprecated modules are a security risk.", "version of that module, or find an alternative. Deprecated modules are a security risk.",
severity: "warn", severity: "error",
from: {}, from: {},
to: { to: {
dependencyTypes: ["deprecated"], dependencyTypes: ["deprecated"],
@ -122,7 +121,7 @@ module.exports = {
"Likely this module depends on an external ('npm') package that occurs more than once " + "Likely this module depends on an external ('npm') package that occurs more than once " +
"in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
"maintenance problems later on.", "maintenance problems later on.",
severity: "warn", severity: "error",
from: {}, from: {},
to: { to: {
moreThanOneDependencyType: true, moreThanOneDependencyType: true,
@ -133,7 +132,7 @@ module.exports = {
}, },
}, },
/* rules you might want to tweak for your specific situation: */ // rules you might want to tweak for your specific situation:
{ {
name: "not-to-spec", name: "not-to-spec",
@ -188,7 +187,7 @@ module.exports = {
"in your package.json. This makes sense if your package is e.g. a plugin, but in " + "in your package.json. This makes sense if your package is e.g. a plugin, but in " +
"other cases - maybe not so much. If the use of a peer dependency is intentional " + "other cases - maybe not so much. If the use of a peer dependency is intentional " +
"add an exception to your dependency-cruiser configuration.", "add an exception to your dependency-cruiser configuration.",
severity: "warn", severity: "error",
from: {}, from: {},
to: { to: {
dependencyTypes: ["npm-peer"], dependencyTypes: ["npm-peer"],
@ -196,6 +195,7 @@ module.exports = {
}, },
], ],
options: { options: {
exclude: ["src/plugins/vite/*", "src/vite.env.d.ts"],
/* Which modules not to follow further when encountered */ /* Which modules not to follow further when encountered */
doNotFollow: { doNotFollow: {
/* path: an array of regular expressions in strings to match against */ /* path: an array of regular expressions in strings to match against */
@ -235,7 +235,7 @@ module.exports = {
true: also detect dependencies that only exist before typescript-to-javascript compilation true: also detect dependencies that only exist before typescript-to-javascript compilation
"specify": for each dependency identify whether it only exists before compilation or also after "specify": for each dependency identify whether it only exists before compilation or also after
*/ */
// tsPreCompilationDeps: false, tsPreCompilationDeps: true,
/* list of extensions to scan that aren't javascript or compile-to-javascript. /* list of extensions to scan that aren't javascript or compile-to-javascript.
Empty by default. Only put extensions in here that you want to take into Empty by default. Only put extensions in here that you want to take into

View File

@ -2,25 +2,28 @@
<!-- Feel free to look at other PRs for examples --> <!-- Feel free to look at other PRs for examples -->
<!-- <!--
Make sure the title includes categorization (choose the one that best fits): Make sure the title includes categorization (choose the one that best fits):
- [Bug]: If the PR is primarily a bug fix - [Bug]: If the PR is primarily a bug fix
- [Move]: If a move has new or changed functionality - [Move]: If a move has new or changed functionality
- [Ability]: If an ability has new or changed functionality - [Ability]: If an ability has new or changed functionality
- [Item]: For new or modified items - [Item]: For new or modified items
- [Mystery]: For new or modified Mystery Encounters - [Mystery]: For new or modified Mystery Encounters
- [Test]: If the PR is primarily adding or modifying tests - [Test]: If the PR is primarily adding or modifying tests
- [UI/UX]: If the PR is changing UI/UX elements - [UI/UX]: If the PR is changing UI/UX elements
- [Audio]: If the PR is adding or changing music/sfx - [Audio]: If the PR is adding or changing music/sfx
- [Sprite]: If the PR is adding or changing sprites - [Sprite]: If the PR is adding or changing sprites
- [Balance]: If the PR is related to game balance - [Balance]: If the PR is related to game balance
- [Challenge]: If the PR is adding or modifying challenges - [Challenge]: If the PR is adding or modifying challenges
- [Refactor]: If the PR is primarily rewriting existing code - [Refactor]: If the PR is primarily rewriting existing code
- [Docs]: If the PR is just adding or modifying documentation (such as tsdocs/code comments) - [Dev]: If the PR is primarily changing something pertaining to development (lefthook hooks, linter rules, etc.)
- [GitHub]: For changes to GitHub workflows/templates/etc - [i18n]: If the PR is primarily adding/changing locale keys or key usage (may come with an associated locales PR)
- [Misc]: If no other category fits the PR - [Docs]: If the PR is adding or modifying documentation (such as tsdocs/code comments)
- [GitHub]: For changes to GitHub workflows/templates/etc
- [Misc]: If no other category fits the PR
--> -->
<!-- <!--
Make sure that this PR is not overlapping with someone else's work Make sure that this PR is not overlapping with someone else's work
Please try to keep the PR self-contained (and small) Please try to keep the PR self-contained (and small!)
--> -->
## What are the changes the user will see? ## What are the changes the user will see?
@ -66,7 +69,7 @@ Do the reviewers need to do something special in order to test your changes?
- [ ] Have I provided a clear explanation of the changes? - [ ] Have I provided a clear explanation of the changes?
- [ ] Have I tested the changes manually? - [ ] Have I tested the changes manually?
- [ ] Are all unit tests still passing? (`npm run test:silent`) - [ ] Are all unit tests still passing? (`npm run test:silent`)
- [ ] Have I created new automated tests (`npm run create-test`) or updated existing tests related to the PR's changes? - [ ] Have I created new automated tests (`npm run test:create`) or updated existing tests related to the PR's changes?
- [ ] Have I provided screenshots/videos of the changes (if applicable)? - [ ] Have I provided screenshots/videos of the changes (if applicable)?
- [ ] Have I made sure that any UI change works for both UI themes (default and legacy)? - [ ] Have I made sure that any UI change works for both UI themes (default and legacy)?

42
.github/workflows/linting.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Linting
on:
push:
branches:
- main
- beta
pull_request:
branches:
- main
- beta
merge_group:
types: [checks_requested]
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install Node.js dependencies
run: npm ci
- name: Run ESLint
run: npm run eslint-ci
- name: Lint with Biome
run: npm run biome-ci
- name: Check dependencies with depcruise
run: npm run depcruise

View File

@ -1,41 +0,0 @@
name: Biome Code Quality
on:
# Trigger the workflow on push or pull request,
# but only for the main branch
push:
branches:
- main # Trigger on push events to the main branch
- beta # Trigger on push events to the beta branch
pull_request:
branches:
- main # Trigger on pull request events targeting the main branch
- beta # Trigger on pull request events targeting the beta branch
merge_group:
types: [checks_requested]
jobs:
run-linters: # Define a job named "run-linters"
name: Run linters # Human-readable name for the job
runs-on: ubuntu-latest # Specify the latest Ubuntu runner for the job
steps:
- name: Check out Git repository # Step to check out the repository
uses: actions/checkout@v4 # Use the checkout action version 4
with:
submodules: 'recursive'
- name: Set up Node.js # Step to set up Node.js environment
uses: actions/setup-node@v4 # Use the setup-node action version 4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install Node.js dependencies # Step to install Node.js dependencies
run: npm ci # Use 'npm ci' to install dependencies
- name: eslint # Step to run linters
run: npm run eslint-ci
- name: Lint with Biome # Step to run linters
run: npm run biome-ci

View File

@ -28,7 +28,6 @@
".vscode/*", ".vscode/*",
"*.css", // TODO? "*.css", // TODO?
"*.html", // TODO? "*.html", // TODO?
"src/overrides.ts",
// TODO: these files are too big and complex, ignore them until their respective refactors // TODO: these files are too big and complex, ignore them until their respective refactors
"src/data/moves/move.ts", "src/data/moves/move.ts",
@ -47,8 +46,8 @@
"correctness": { "correctness": {
"noUndeclaredVariables": "off", "noUndeclaredVariables": "off",
"noUnusedVariables": "error", "noUnusedVariables": "error",
"noSwitchDeclarations": "warn", // TODO: refactor and make this an error "noSwitchDeclarations": "error",
"noVoidTypeReturn": "warn", // TODO: Refactor and make this an error "noVoidTypeReturn": "error",
"noUnusedImports": "error" "noUnusedImports": "error"
}, },
"style": { "style": {
@ -85,7 +84,7 @@
"useLiteralKeys": "off", "useLiteralKeys": "off",
"noForEach": "off", // Foreach vs for of is not that simple. "noForEach": "off", // Foreach vs for of is not that simple.
"noUselessSwitchCase": "off", // Explicit > Implicit "noUselessSwitchCase": "off", // Explicit > Implicit
"noUselessConstructor": "warn", // TODO: Refactor and make this an error "noUselessConstructor": "error",
"noBannedTypes": "warn" // TODO: Refactor and make this an error "noBannedTypes": "warn" // TODO: Refactor and make this an error
}, },
"nursery": { "nursery": {
@ -120,6 +119,28 @@
} }
} }
} }
},
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes)
{
"include": ["src/overrides.ts", "src/enums/*"],
"linter": {
"rules": {
"correctness": {
"noUnusedImports": "off"
}
}
}
},
{
"include": ["src/overrides.ts"],
"linter": {
"rules": {
"style": {
"useImportType": "off"
}
}
}
} }
] ]
} }

View File

@ -18,9 +18,9 @@
"eslint": "eslint --fix .", "eslint": "eslint --fix .",
"eslint-ci": "eslint .", "eslint-ci": "eslint .",
"biome": "biome check --write --changed --no-errors-on-unmatched", "biome": "biome check --write --changed --no-errors-on-unmatched",
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --changed --no-errors-on-unmatched", "biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
"docs": "typedoc", "docs": "typedoc",
"depcruise": "depcruise src", "depcruise": "depcruise src test",
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg", "depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg",
"postinstall": "npx lefthook install && npx lefthook run post-merge", "postinstall": "npx lefthook install && npx lefthook run post-merge",
"update-version:patch": "npm version patch --force --no-git-tag-version", "update-version:patch": "npm version patch --force --no-git-tag-version",

View File

@ -29,4 +29,4 @@ export type ModifierString = keyof ModifierConstructorMap;
export type ModifierPool = { export type ModifierPool = {
[tier: string]: WeightedModifierType[]; [tier: string]: WeightedModifierType[];
} };

View File

@ -7,7 +7,6 @@ import {
isNullOrUndefined, isNullOrUndefined,
randSeedItem, randSeedItem,
randSeedInt, randSeedInt,
type Constructor,
randSeedFloat, randSeedFloat,
coerceArray, coerceArray,
} from "#app/utils/common"; } from "#app/utils/common";
@ -35,12 +34,11 @@ import { Command } from "#enums/command";
import { BerryModifierType } from "#app/modifier/modifier-type"; import { BerryModifierType } from "#app/modifier/modifier-type";
import { getPokeballName } from "#app/data/pokeball"; import { getPokeballName } from "#app/data/pokeball";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import type { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { allAbilities } from "#app/data/data-lists"; import { allAbilities } from "#app/data/data-lists";
// Enum imports // Enum imports
import { Stat, type BattleStat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey, type EffectiveStat } from "#enums/stat"; import { Stat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey } from "#enums/stat";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
@ -54,12 +52,16 @@ import { SwitchType } from "#enums/switch-type";
import { MoveFlags } from "#enums/MoveFlags"; import { MoveFlags } from "#enums/MoveFlags";
import { MoveTarget } from "#enums/MoveTarget"; import { MoveTarget } from "#enums/MoveTarget";
import { MoveCategory } from "#enums/MoveCategory"; import { MoveCategory } from "#enums/MoveCategory";
import type { BerryType } from "#enums/berry-type";
import { CommonAnim } from "#enums/move-anims-common"; import { CommonAnim } from "#enums/move-anims-common";
import { getBerryEffectFunc } from "../berry"; import { getBerryEffectFunc } from "#app/data/berry";
import { BerryUsedEvent } from "#app/events/battle-scene"; import { BerryUsedEvent } from "#app/events/battle-scene";
import { noAbilityTypeOverrideMoves } from "#app/data/moves/invalid-moves";
import { MoveUseMode } from "#enums/move-use-mode";
// Type imports // Type imports
import type { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import type { BattleStat, EffectiveStat } from "#enums/stat";
import type { BerryType } from "#enums/berry-type";
import type { EnemyPokemon } from "#app/field/pokemon"; import type { EnemyPokemon } from "#app/field/pokemon";
import type { PokemonMove } from "../moves/pokemon-move"; import type { PokemonMove } from "../moves/pokemon-move";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
@ -76,7 +78,7 @@ import type {
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import type Move from "#app/data/moves/move"; import type Move from "#app/data/moves/move";
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag"; import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves"; import type { Constructor } from "#app/utils/common";
import type { Localizable } from "#app/@types/locales"; import type { Localizable } from "#app/@types/locales";
import { applyAbAttrs } from "./apply-ab-attrs"; import { applyAbAttrs } from "./apply-ab-attrs";
@ -1915,7 +1917,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
_args: any[], _args: any[],
): boolean { ): boolean {
return ( return (
attacker.getTag(BattlerTagType.DISABLED) === null && isNullOrUndefined(attacker.getTag(BattlerTagType.DISABLED)) &&
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) && move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) &&
(this.chance === -1 || pokemon.randBattleSeedInt(100) < this.chance) (this.chance === -1 || pokemon.randBattleSeedInt(100) < this.chance)
); );
@ -2733,7 +2735,6 @@ export class AllyStatMultiplierAbAttr extends AbAttr {
/** /**
* Takes effect whenever a move succesfully executes, such as gorilla tactics' move-locking. * Takes effect whenever a move succesfully executes, such as gorilla tactics' move-locking.
* (More specifically, whenever a move is pushed to the move history) * (More specifically, whenever a move is pushed to the move history)
* @extends AbAttr
*/ */
export class ExecutedMoveAbAttr extends AbAttr { export class ExecutedMoveAbAttr extends AbAttr {
canApplyExecutedMove(_pokemon: Pokemon, _simulated: boolean): boolean { canApplyExecutedMove(_pokemon: Pokemon, _simulated: boolean): boolean {
@ -2744,16 +2745,16 @@ export class ExecutedMoveAbAttr extends AbAttr {
} }
/** /**
* Ability attribute for Gorilla Tactics * Ability attribute for {@linkcode AbilityId.GORILLA_TACTICS | Gorilla Tactics}
* @extends ExecutedMoveAbAttr * to lock the user into its first selected move.
*/ */
export class GorillaTacticsAbAttr extends ExecutedMoveAbAttr { export class GorillaTacticsAbAttr extends ExecutedMoveAbAttr {
constructor(showAbility = false) { constructor(showAbility = false) {
super(showAbility); super(showAbility);
} }
override canApplyExecutedMove(pokemon: Pokemon, simulated: boolean): boolean { override canApplyExecutedMove(pokemon: Pokemon, _simulated: boolean): boolean {
return simulated || !pokemon.getTag(BattlerTagType.GORILLA_TACTICS); return !pokemon.getTag(BattlerTagType.GORILLA_TACTICS);
} }
override applyExecutedMove(pokemon: Pokemon, simulated: boolean): void { override applyExecutedMove(pokemon: Pokemon, simulated: boolean): void {
@ -6063,14 +6064,19 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
): void { ): void {
if (!simulated) { if (!simulated) {
dancer.turnData.extraTurns++; dancer.turnData.extraTurns++;
const phaseManager = globalScene.phaseManager;
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance // If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) { if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
const target = this.getTarget(dancer, source, targets); const target = this.getTarget(dancer, source, targets);
phaseManager.unshiftNew("MovePhase", dancer, target, move, true, true); globalScene.phaseManager.unshiftNew("MovePhase", dancer, target, move, MoveUseMode.INDIRECT);
} else if (move.getMove().is("SelfStatusMove")) { } else if (move.getMove().is("SelfStatusMove")) {
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself // If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
phaseManager.unshiftNew("MovePhase", dancer, [dancer.getBattlerIndex()], move, true, true); globalScene.phaseManager.unshiftNew(
"MovePhase",
dancer,
[dancer.getBattlerIndex()],
move,
MoveUseMode.INDIRECT,
);
} }
} }
} }
@ -7378,6 +7384,7 @@ class ForceSwitchOutHelper {
* @param pokemon The {@linkcode Pokemon} attempting to switch out. * @param pokemon The {@linkcode Pokemon} attempting to switch out.
* @returns `true` if the switch is successful * @returns `true` if the switch is successful
*/ */
// TODO: Make this cancel pending move phases on the switched out target
public switchOutLogic(pokemon: Pokemon): boolean { public switchOutLogic(pokemon: Pokemon): boolean {
const switchOutTarget = pokemon; const switchOutTarget = pokemon;
/** /**
@ -8378,10 +8385,10 @@ export function initAbilities() {
.attr(WonderSkinAbAttr) .attr(WonderSkinAbAttr)
.ignorable(), .ignorable(),
new Ability(AbilityId.ANALYTIC, 5) new Ability(AbilityId.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user, _target, _move) => { .attr(MovePowerBoostAbAttr, (user) =>
const movePhase = globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id); // Boost power if all other Pokemon have already moved (no other moves are slated to execute)
return isNullOrUndefined(movePhase); !globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id),
}, 1.3), 1.3),
new Ability(AbilityId.ILLUSION, 5) new Ability(AbilityId.ILLUSION, 5)
// The Pokemon generate an illusion if it's available // The Pokemon generate an illusion if it's available
.attr(IllusionPreSummonAbAttr, false) .attr(IllusionPreSummonAbAttr, false)
@ -8655,7 +8662,13 @@ export function initAbilities() {
.attr(PostFaintHPDamageAbAttr) .attr(PostFaintHPDamageAbAttr)
.bypassFaint(), .bypassFaint(),
new Ability(AbilityId.DANCER, 7) new Ability(AbilityId.DANCER, 7)
.attr(PostDancingMoveAbAttr), .attr(PostDancingMoveAbAttr)
/* Incorrect interations with:
* Petal Dance (should not lock in or count down timer; currently does both)
* Flinches (due to tag being removed earlier)
* Failed/protected moves (should not trigger if original move is protected against)
*/
.edgeCase(),
new Ability(AbilityId.BATTERY, 7) new Ability(AbilityId.BATTERY, 7)
.attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL ], 1.3), .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL ], 1.3),
new Ability(AbilityId.FLUFFY, 7) new Ability(AbilityId.FLUFFY, 7)
@ -8796,9 +8809,11 @@ export function initAbilities() {
new Ability(AbilityId.WANDERING_SPIRIT, 8) new Ability(AbilityId.WANDERING_SPIRIT, 8)
.attr(PostDefendAbilitySwapAbAttr) .attr(PostDefendAbilitySwapAbAttr)
.bypassFaint() .bypassFaint()
.edgeCase(), // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil .edgeCase(), // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil
new Ability(AbilityId.GORILLA_TACTICS, 8) new Ability(AbilityId.GORILLA_TACTICS, 8)
.attr(GorillaTacticsAbAttr), .attr(GorillaTacticsAbAttr)
// TODO: Verify whether Gorilla Tactics increases struggle's power or not
.edgeCase(),
new Ability(AbilityId.NEUTRALIZING_GAS, 8, 2) new Ability(AbilityId.NEUTRALIZING_GAS, 8, 2)
.attr(PostSummonAddArenaTagAbAttr, true, ArenaTagType.NEUTRALIZING_GAS, 0) .attr(PostSummonAddArenaTagAbAttr, true, ArenaTagType.NEUTRALIZING_GAS, 0)
.attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr) .attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr)

View File

@ -20,6 +20,7 @@ import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { MoveUseMode } from "#enums/move-use-mode";
export abstract class ArenaTag { export abstract class ArenaTag {
constructor( constructor(
@ -875,13 +876,13 @@ export class DelayedAttackTag extends ArenaTag {
const ret = super.lapse(arena); const ret = super.lapse(arena);
if (!ret) { if (!ret) {
// TODO: This should not add to move history (for Spite)
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"MoveEffectPhase", "MoveEffectPhase",
this.sourceId!, this.sourceId!,
[this.targetIndex], [this.targetIndex],
allMoves[this.sourceMove!], allMoves[this.sourceMove!],
false, MoveUseMode.FOLLOW_UP,
true,
); // TODO: are those bangs correct? ); // TODO: are those bangs correct?
} }

View File

@ -1,12 +1,18 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { allMoves } from "./data-lists"; import { allMoves } from "#app/data/data-lists";
import { MoveFlags } from "#enums/MoveFlags"; import { MoveFlags } from "#enums/MoveFlags";
import type Pokemon from "../field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { type nil, getFrameMs, getEnumKeys, getEnumValues, animationFileName, coerceArray } from "../utils/common"; import {
type nil,
getFrameMs,
getEnumKeys,
getEnumValues,
animationFileName,
coerceArray,
isNullOrUndefined,
} from "#app/utils/common";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SubstituteTag } from "./battler-tags";
import { isNullOrUndefined } from "../utils/common";
import Phaser from "phaser"; import Phaser from "phaser";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { AnimBlendType, AnimFrameTarget, AnimFocus, ChargeAnim, CommonAnim } from "#enums/move-anims-common"; import { AnimBlendType, AnimFrameTarget, AnimFocus, ChargeAnim, CommonAnim } from "#enums/move-anims-common";
@ -845,7 +851,7 @@ export abstract class BattleAnim {
return; return;
} }
const targetSubstitute = !!onSubstitute && user !== target ? target.getTag(SubstituteTag) : null; const targetSubstitute = !!onSubstitute && user !== target ? target.getTag(BattlerTagType.SUBSTITUTE) : null;
const userSprite = user.getSprite(); const userSprite = user.getSprite();
const targetSprite = targetSubstitute?.sprite ?? target.getSprite(); const targetSprite = targetSubstitute?.sprite ?? target.getSprite();

View File

@ -31,8 +31,15 @@ import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { isNullOrUndefined } from "#app/utils/common"; import { isNullOrUndefined } from "#app/utils/common";
import { MoveUseMode } from "#enums/move-use-mode";
import { invalidEncoreMoves } from "./moves/invalid-moves";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
/**
* A {@linkcode BattlerTag} represents a semi-persistent effect that can be attached to a {@linkcode Pokemon}.
* Tags can trigger various effects throughout a turn, and are cleared on switching out
* or through their respective {@linkcode BattlerTag.lapse | lapse} methods.
*/
export class BattlerTag { export class BattlerTag {
public tagType: BattlerTagType; public tagType: BattlerTagType;
public lapseTypes: BattlerTagLapseType[]; public lapseTypes: BattlerTagLapseType[];
@ -69,7 +76,7 @@ export class BattlerTag {
/** /**
* Tick down this {@linkcode BattlerTag}'s duration. * Tick down this {@linkcode BattlerTag}'s duration.
* @returns `true` if the tag should be kept (`turnCount` > 0`) * @returns `true` if the tag should be kept (`turnCount > 0`)
*/ */
lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
// TODO: Maybe flip this (return `true` if tag needs removal) // TODO: Maybe flip this (return `true` if tag needs removal)
@ -125,16 +132,6 @@ export interface TerrainBattlerTag {
* Players and enemies should not be allowed to select restricted moves. * Players and enemies should not be allowed to select restricted moves.
*/ */
export abstract class MoveRestrictionBattlerTag extends BattlerTag { export abstract class MoveRestrictionBattlerTag extends BattlerTag {
constructor(
tagType: BattlerTagType,
lapseType: BattlerTagLapseType | BattlerTagLapseType[],
turnCount: number,
sourceMove?: MoveId,
sourceId?: number,
) {
super(tagType, lapseType, turnCount, sourceMove, sourceId);
}
/** @override */ /** @override */
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.PRE_MOVE) { if (lapseType === BattlerTagLapseType.PRE_MOVE) {
@ -277,17 +274,18 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
/** /**
* @override * @override
* *
* Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@linkcode moveId} and shows a message. * Attempt to disable the target's last move by setting this tag's {@linkcode moveId}
* Otherwise the move ID will not get assigned and this tag will get removed next turn. * and showing a message.
*/ */
override onAdd(pokemon: Pokemon): void { override onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon); // Disable fails against struggle or an empty move history
// TODO: Confirm if this is redundant given Disable/Cursed Body's disable conditions
const move = pokemon.getLastXMoves(-1).find(m => !m.virtual); const move = pokemon.getLastNonVirtualMove();
if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE || move.move === MoveId.NONE) { if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE) {
return; return;
} }
super.onAdd(pokemon);
this.moveId = move.move; this.moveId = move.move;
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
@ -337,7 +335,6 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
/** /**
* Tag used by Gorilla Tactics to restrict the user to using only one move. * Tag used by Gorilla Tactics to restrict the user to using only one move.
* @extends MoveRestrictionBattlerTag
*/ */
export class GorillaTacticsTag extends MoveRestrictionBattlerTag { export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
private moveId = MoveId.NONE; private moveId = MoveId.NONE;
@ -346,34 +343,30 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0); super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0);
} }
/** @override */
override isMoveRestricted(move: MoveId): boolean { override isMoveRestricted(move: MoveId): boolean {
return move !== this.moveId; return move !== this.moveId;
} }
/** /**
* @override * Ensures that move history exists on {@linkcode Pokemon} and has a valid move to lock into.
* @param {Pokemon} pokemon the {@linkcode Pokemon} to check if the tag can be added * @param pokemon - The {@linkcode Pokemon} to add the tag to
* @returns `true` if the pokemon has a valid move and no existing {@linkcode GorillaTacticsTag}; `false` otherwise * @returns `true` if the tag can be added
*/ */
override canAdd(pokemon: Pokemon): boolean { override canAdd(pokemon: Pokemon): boolean {
return this.getLastValidMove(pokemon) !== undefined && !pokemon.getTag(GorillaTacticsTag); // Choice items ignore struggle, so Gorilla Tactics should too
const lastSelectedMove = pokemon.getLastNonVirtualMove();
return !isNullOrUndefined(lastSelectedMove) && lastSelectedMove.move !== MoveId.STRUGGLE;
} }
/** /**
* Ensures that move history exists on {@linkcode Pokemon} and has a valid move. * Sets this tag's {@linkcode moveId} and increases the user's Attack by 50%.
* If so, sets the {@linkcode moveId} and increases the user's Attack by 50%. * @param pokemon - The {@linkcode Pokemon} to add the tag to
* @override
* @param {Pokemon} pokemon the {@linkcode Pokemon} to add the tag to
*/ */
override onAdd(pokemon: Pokemon): void { override onAdd(pokemon: Pokemon): void {
const lastValidMove = this.getLastValidMove(pokemon); super.onAdd(pokemon);
if (!lastValidMove) { // Bang is justified as tag is not added if prior move doesn't exist
return; this.moveId = pokemon.getLastNonVirtualMove()!.move;
}
this.moveId = lastValidMove;
pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false); pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false);
} }
@ -388,29 +381,16 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
} }
/** /**
* * Return the text displayed when a move is restricted.
* @override * @param pokemon - The {@linkcode Pokemon} with this tag.
* @param {Pokemon} pokemon n/a * @returns A string containing the text to display when the move is denied
* @param {MoveId} _move {@linkcode MoveId} ID of the move being denied
* @returns {string} text to display when the move is denied
*/ */
override selectionDeniedText(pokemon: Pokemon, _move: MoveId): string { override selectionDeniedText(pokemon: Pokemon): string {
return i18next.t("battle:canOnlyUseMove", { return i18next.t("battle:canOnlyUseMove", {
moveName: allMoves[this.moveId].name, moveName: allMoves[this.moveId].name,
pokemonName: getPokemonNameWithAffix(pokemon), pokemonName: getPokemonNameWithAffix(pokemon),
}); });
} }
/**
* Gets the last valid move from the pokemon's move history.
* @param {Pokemon} pokemon {@linkcode Pokemon} to get the last valid move from
* @returns {MoveId | undefined} the last valid move from the pokemon's move history
*/
getLastValidMove(pokemon: Pokemon): MoveId | undefined {
const move = pokemon.getLastXMoves().find(m => m.move !== MoveId.NONE && m.move !== MoveId.STRUGGLE && !m.virtual);
return move?.move;
}
} }
/** /**
@ -424,8 +404,8 @@ export class RechargingTag extends BattlerTag {
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon); super.onAdd(pokemon);
// Queue a placeholder move for the Pokemon to "use" next turn // Queue a placeholder move for the Pokemon to "use" next turn.
pokemon.getMoveQueue().push({ move: MoveId.NONE, targets: [] }); pokemon.pushMoveQueue({ move: MoveId.NONE, targets: [], useMode: MoveUseMode.NORMAL });
} }
/** Cancels the source's move this turn and queues a "__ must recharge!" message */ /** Cancels the source's move this turn and queues a "__ must recharge!" message */
@ -670,6 +650,7 @@ export class InterruptedTag extends BattlerTag {
move: MoveId.NONE, move: MoveId.NONE,
result: MoveResult.OTHER, result: MoveResult.OTHER,
targets: [], targets: [],
useMode: MoveUseMode.NORMAL,
}); });
} }
@ -995,42 +976,45 @@ export class PowderTag extends BattlerTag {
} }
/** /**
* Applies Powder's effects before the tag owner uses a Fire-type move. * Applies Powder's effects before the tag owner uses a Fire-type move, damaging and canceling its action.
* Also causes the tag to expire at the end of turn. * Lasts until the end of the turn.
* @param pokemon {@linkcode Pokemon} the owner of this tag * @param pokemon - The {@linkcode Pokemon} with this tag.
* @param lapseType {@linkcode BattlerTagLapseType} the type of lapse functionality to carry out * @param lapseType - The {@linkcode BattlerTagLapseType} dictating how this tag is being activated
* @returns `true` if the tag should not expire after this lapse; `false` otherwise. * @returns `true` if the tag should remain active.
*/ */
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.PRE_MOVE) { const movePhase = globalScene.phaseManager.getCurrentPhase();
const movePhase = globalScene.phaseManager.getCurrentPhase(); if (lapseType !== BattlerTagLapseType.PRE_MOVE || !movePhase?.is("MovePhase")) {
if (movePhase?.is("MovePhase")) { return false;
const move = movePhase.move.getMove(); }
const weather = globalScene.arena.weather;
if (
pokemon.getMoveType(move) === PokemonType.FIRE &&
!(weather && weather.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed())
) {
movePhase.fail();
movePhase.showMoveText();
const idx = pokemon.getBattlerIndex(); const move = movePhase.move.getMove();
const weather = globalScene.arena.weather;
globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER); if (
pokemon.getMoveType(move) !== PokemonType.FIRE ||
const cancelDamage = new BooleanHolder(false); (weather?.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed()) // Heavy rain takes priority over powder
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelDamage); ) {
if (!cancelDamage.value) {
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
}
// "When the flame touched the powder\non the Pokémon, it exploded!"
globalScene.phaseManager.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name }));
}
}
return true; return true;
} }
return super.lapse(pokemon, lapseType);
// Disable the target's fire type move and damage it (subject to Magic Guard)
movePhase.showMoveText();
movePhase.fail();
const idx = pokemon.getBattlerIndex();
globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER);
const cancelDamage = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelDamage);
if (!cancelDamage.value) {
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
}
// "When the flame touched the powder\non the Pokémon, it exploded!"
globalScene.phaseManager.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name }));
return true;
} }
} }
@ -1125,34 +1109,22 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
} }
canAdd(pokemon: Pokemon): boolean { canAdd(pokemon: Pokemon): boolean {
const lastMoves = pokemon.getLastXMoves(1); const lastMove = pokemon.getLastNonVirtualMove();
if (!lastMoves.length) { if (!lastMove) {
return false; return false;
} }
const repeatableMove = lastMoves[0]; if (invalidEncoreMoves.has(lastMove.move)) {
if (!repeatableMove.move || repeatableMove.virtual) {
return false; return false;
} }
switch (repeatableMove.move) { this.moveId = lastMove.move;
case MoveId.MIMIC:
case MoveId.MIRROR_MOVE:
case MoveId.TRANSFORM:
case MoveId.STRUGGLE:
case MoveId.SKETCH:
case MoveId.SLEEP_TALK:
case MoveId.ENCORE:
return false;
}
this.moveId = repeatableMove.move;
return true; return true;
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
// TODO: shouldn't this be `onAdd`?
super.onRemove(pokemon); super.onRemove(pokemon);
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
@ -1168,7 +1140,13 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
const lastMove = pokemon.getLastXMoves(1)[0]; const lastMove = pokemon.getLastXMoves(1)[0];
globalScene.phaseManager.tryReplacePhase( globalScene.phaseManager.tryReplacePhase(
m => m.is("MovePhase") && m.pokemon === pokemon, m => m.is("MovePhase") && m.pokemon === pokemon,
globalScene.phaseManager.create("MovePhase", pokemon, lastMove.targets ?? [], movesetMove), globalScene.phaseManager.create(
"MovePhase",
pokemon,
lastMove.targets ?? [],
movesetMove,
MoveUseMode.NORMAL,
),
); );
} }
} }
@ -1470,16 +1448,6 @@ export class WrapTag extends DamagingTrapTag {
} }
export abstract class VortexTrapTag extends DamagingTrapTag { export abstract class VortexTrapTag extends DamagingTrapTag {
constructor(
tagType: BattlerTagType,
commonAnim: CommonAnim,
turnCount: number,
sourceMove: MoveId,
sourceId: number,
) {
super(tagType, commonAnim, turnCount, sourceMove, sourceId);
}
getTrapMessage(pokemon: Pokemon): string { getTrapMessage(pokemon: Pokemon): string {
return i18next.t("battlerTags:vortexOnTrap", { return i18next.t("battlerTags:vortexOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
@ -1904,24 +1872,29 @@ export class TruantTag extends AbilityBattlerTag {
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (!pokemon.hasAbility(AbilityId.TRUANT)) { if (!pokemon.hasAbility(AbilityId.TRUANT)) {
// remove tag if mon lacks ability
return super.lapse(pokemon, lapseType); return super.lapse(pokemon, lapseType);
} }
const passive = pokemon.getAbility().id !== AbilityId.TRUANT;
const lastMove = pokemon.getLastXMoves().find(() => true); const lastMove = pokemon.getLastXMoves()[0];
if (lastMove && lastMove.move !== MoveId.NONE) { if (!lastMove) {
(globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel(); // Don't interrupt move if last move was `Moves.NONE` OR no prior move was found
// TODO: Ability displays should be handled by the ability return true;
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
globalScene.phaseManager.queueMessage(
i18next.t("battlerTags:truantLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false);
} }
// Interrupt move usage in favor of slacking off
const passive = pokemon.getAbility().id !== AbilityId.TRUANT;
(globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel();
// TODO: Ability displays should be handled by the ability
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
globalScene.phaseManager.queueMessage(
i18next.t("battlerTags:truantLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false);
return true; return true;
} }
} }

View File

@ -1,6 +1,6 @@
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
/** Set of moves that cannot be called by {@linkcode MoveId.METRONOME Metronome} */ /** Set of moves that cannot be called by {@linkcode MoveId.METRONOME | Metronome}. */
export const invalidMetronomeMoves: ReadonlySet<MoveId> = new Set([ export const invalidMetronomeMoves: ReadonlySet<MoveId> = new Set([
MoveId.AFTER_YOU, MoveId.AFTER_YOU,
MoveId.ASSIST, MoveId.ASSIST,
@ -255,3 +255,28 @@ export const noAbilityTypeOverrideMoves: ReadonlySet<MoveId> = new Set([
MoveId.TECHNO_BLAST, MoveId.TECHNO_BLAST,
MoveId.HIDDEN_POWER, MoveId.HIDDEN_POWER,
]); ]);
/** Set of all moves that cannot be copied by {@linkcode Moves.SKETCH}. */
export const invalidSketchMoves: ReadonlySet<MoveId> = new Set([
MoveId.NONE,
MoveId.CHATTER,
MoveId.MIRROR_MOVE,
MoveId.SLEEP_TALK,
MoveId.STRUGGLE,
MoveId.SKETCH,
MoveId.REVIVAL_BLESSING,
MoveId.TERA_STARSTORM,
MoveId.BREAKNECK_BLITZ__PHYSICAL,
MoveId.BREAKNECK_BLITZ__SPECIAL,
]);
/** Set of all moves that cannot be locked into by {@linkcode Moves.ENCORE}. */
export const invalidEncoreMoves: ReadonlySet<MoveId> = new Set([
MoveId.MIMIC,
MoveId.MIRROR_MOVE,
MoveId.TRANSFORM,
MoveId.STRUGGLE,
MoveId.SKETCH,
MoveId.SLEEP_TALK,
MoveId.ENCORE,
]);

View File

@ -70,13 +70,9 @@ import {
getStatKey, getStatKey,
Stat, Stat,
} from "#app/enums/stat"; } from "#app/enums/stat";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { SpeciesFormChangeRevertWeatherFormTrigger } from "../pokemon-forms/form-change-triggers"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "../pokemon-forms/form-change-triggers";
import type { GameMode } from "#app/game-mode"; import type { GameMode } from "#app/game-mode";
@ -85,18 +81,14 @@ import { ChallengeType } from "#enums/challenge-type";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase";
import { LoadMoveAnimPhase } from "#app/phases/load-move-anim-phase";
import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase";
import { MoveAnimPhase } from "#app/phases/move-anim-phase";
import { loggedInUser } from "#app/account"; import { loggedInUser } from "#app/account";
import { MoveCategory } from "#enums/MoveCategory"; import { MoveCategory } from "#enums/MoveCategory";
import { MoveTarget } from "#enums/MoveTarget"; import { MoveTarget } from "#enums/MoveTarget";
import { MoveFlags } from "#enums/MoveFlags"; import { MoveFlags } from "#enums/MoveFlags";
import { MoveEffectTrigger } from "#enums/MoveEffectTrigger"; import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
import { MultiHitType } from "#enums/MultiHitType"; import { MultiHitType } from "#enums/MultiHitType";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves";
import { SelectBiomePhase } from "#app/phases/select-biome-phase"; import { isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types"; import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types";
import { applyMoveAttrs } from "./apply-attrs"; import { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils"; import { frenzyMissFunc, getMoveTargets } from "./move-utils";
@ -2156,6 +2148,7 @@ export class PlantHealAttr extends WeatherHealAttr {
case WeatherType.SANDSTORM: case WeatherType.SANDSTORM:
case WeatherType.HAIL: case WeatherType.HAIL:
case WeatherType.SNOW: case WeatherType.SNOW:
case WeatherType.FOG:
case WeatherType.HEAVY_RAIN: case WeatherType.HEAVY_RAIN:
return 0.25; return 0.25;
default: default:
@ -3123,7 +3116,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
overridden.value = true; overridden.value = true;
globalScene.phaseManager.unshiftNew("MoveAnimPhase", new MoveChargeAnim(this.chargeAnim, move.id, user)); globalScene.phaseManager.unshiftNew("MoveAnimPhase", new MoveChargeAnim(this.chargeAnim, move.id, user));
globalScene.phaseManager.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user))); globalScene.phaseManager.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER }); user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER, useMode: MoveUseMode.NORMAL });
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex()); globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
} else { } else {
@ -4080,15 +4073,26 @@ export class OpponentHighHpPowerAttr extends VariablePowerAttr {
} }
} }
/**
* Attribute to double this move's power if the target hasn't acted yet in the current turn.
* Used by {@linkcode Moves.BOLT_BEAK} and {@linkcode Moves.FISHIOUS_REND}
*/
export class FirstAttackDoublePowerAttr extends VariablePowerAttr { export class FirstAttackDoublePowerAttr extends VariablePowerAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { /**
console.log(target.getLastXMoves(1), globalScene.currentBattle.turn); * Double this move's power if the user is acting before the target.
if (!target.getLastXMoves(1).find(m => m.turn === globalScene.currentBattle.turn)) { * @param user - Unused
(args[0] as NumberHolder).value *= 2; * @param target - The {@linkcode Pokemon} being targeted by this move
return true; * @param move - Unused
* @param args `[0]` - A {@linkcode NumberHolder} containing move base power
* @returns Whether the attribute was successfully applied
*/
apply(_user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean {
if (target.turnData.acted) {
return false;
} }
return false; args[0].value *= 2;
return true;
} }
} }
@ -4157,6 +4161,7 @@ export class AntiSunlightPowerDecreaseAttr extends VariablePowerAttr {
case WeatherType.SANDSTORM: case WeatherType.SANDSTORM:
case WeatherType.HAIL: case WeatherType.HAIL:
case WeatherType.SNOW: case WeatherType.SNOW:
case WeatherType.FOG:
case WeatherType.HEAVY_RAIN: case WeatherType.HEAVY_RAIN:
power.value *= 0.5; power.value *= 0.5;
return true; return true;
@ -5457,13 +5462,20 @@ export class FrenzyAttr extends MoveEffectAttr {
return false; return false;
} }
if (!user.getTag(BattlerTagType.FRENZY) && !user.getMoveQueue().length) { // TODO: Disable if used via dancer
const turnCount = user.randBattleSeedIntRange(1, 2); // TODO: Add support for moves that don't add the frenzy tag (Uproar, Rollout, etc.)
new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true }));
// If frenzy is not active, add a tag and push 1-2 extra turns of attacks to the user's move queue.
// Otherwise, tick down the existing tag.
if (!user.getTag(BattlerTagType.FRENZY) && user.getMoveQueue().length === 0) {
const turnCount = user.randBattleSeedIntRange(1, 2); // excludes initial use
for (let i = 0; i < turnCount; i++) {
user.pushMoveQueue({ move: move.id, targets: [ target.getBattlerIndex() ], useMode: MoveUseMode.IGNORE_PP });
}
user.addTag(BattlerTagType.FRENZY, turnCount, move.id, user.id); user.addTag(BattlerTagType.FRENZY, turnCount, move.id, user.id);
} else { } else {
applyMoveAttrs("AddBattlerTagAttr", user, target, move, args); applyMoveAttrs("AddBattlerTagAttr", user, target, move, args);
user.lapseTag(BattlerTagType.FRENZY); // if FRENZY is already in effect (moveQueue.length > 0), lapse the tag user.lapseTag(BattlerTagType.FRENZY);
} }
return true; return true;
@ -6218,6 +6230,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon); globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon);
// If the pokemon being revived was alive earlier in the turn, cancel its move // If the pokemon being revived was alive earlier in the turn, cancel its move
// (revived pokemon can't move in the turn they're brought back) // (revived pokemon can't move in the turn they're brought back)
// TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move)
globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
if (user.fieldPosition === FieldPosition.CENTER) { if (user.fieldPosition === FieldPosition.CENTER) {
user.setFieldPosition(FieldPosition.LEFT); user.setFieldPosition(FieldPosition.LEFT);
@ -6759,20 +6772,26 @@ export class FirstMoveTypeAttr extends MoveEffectAttr {
class CallMoveAttr extends OverrideMoveEffectAttr { class CallMoveAttr extends OverrideMoveEffectAttr {
protected invalidMoves: ReadonlySet<MoveId>; protected invalidMoves: ReadonlySet<MoveId>;
protected hasTarget: boolean; protected hasTarget: boolean;
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// Get eligible targets for move, failing if we can't target anything
const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined; const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined;
const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget); const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget);
if (moveTargets.targets.length === 0) { if (moveTargets.targets.length === 0) {
globalScene.phaseManager.queueMessage(i18next.t("battle:attackFailed")); globalScene.phaseManager.queueMessage(i18next.t("battle:attackFailed"));
console.log("CallMoveAttr failed due to no targets.");
return false; return false;
} }
// Spread moves and ones with only 1 valid target will use their normal targeting.
// If not, target the Mirror Move recipient or else a random enemy in our target list
const targets = moveTargets.multiple || moveTargets.targets.length === 1 const targets = moveTargets.multiple || moveTargets.targets.length === 1
? moveTargets.targets ? moveTargets.targets
: [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already : [this.hasTarget
user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true }); ? target.getBattlerIndex()
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id); globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id, 0, 0, true), true, true); globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP);
return true; return true;
} }
} }
@ -6867,9 +6886,10 @@ export class RandomMovesetMoveAttr extends CallMoveAttr {
} }
} }
// TODO: extend CallMoveAttr
export class NaturePowerAttr extends OverrideMoveEffectAttr { export class NaturePowerAttr extends OverrideMoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
let moveId; let moveId = MoveId.NONE;
switch (globalScene.arena.getTerrainType()) { switch (globalScene.arena.getTerrainType()) {
// this allows terrains to 'override' the biome move // this allows terrains to 'override' the biome move
case TerrainType.NONE: case TerrainType.NONE:
@ -6994,14 +7014,14 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
moveId = MoveId.PSYCHIC; moveId = MoveId.PSYCHIC;
break; break;
default: default:
// Just in case there's no match // Just in case there's no match
moveId = MoveId.TRI_ATTACK; moveId = MoveId.TRI_ATTACK;
break; break;
} }
user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true }); // Load the move's animation if we didn't already and unshift a new usage phase
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId); globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true); globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP);
return true; return true;
} }
} }
@ -7020,64 +7040,63 @@ export class CopyMoveAttr extends CallMoveAttr {
this.invalidMoves = invalidMoves; this.invalidMoves = invalidMoves;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, _move: Move, args: any[]): boolean {
this.hasTarget = this.mirrorMove; this.hasTarget = this.mirrorMove;
const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : globalScene.currentBattle.lastMove; // bang is correct as condition func returns `false` and fails move if no last move exists
const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)!.move : globalScene.currentBattle.lastMove;
return super.apply(user, target, allMoves[lastMove], args); return super.apply(user, target, allMoves[lastMove], args);
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user, target, move) => { return (_user, target, _move) => {
if (this.mirrorMove) { const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)?.move : globalScene.currentBattle.lastMove;
const lastMove = target.getLastXMoves()[0]?.move; return !isNullOrUndefined(lastMove) && !this.invalidMoves.has(lastMove);
return !!lastMove && !this.invalidMoves.has(lastMove);
} else {
const lastMove = globalScene.currentBattle.lastMove;
return lastMove !== undefined && !this.invalidMoves.has(lastMove);
}
}; };
} }
} }
/** /**
* Attribute used for moves that causes the target to repeat their last used move. * Attribute used for moves that cause the target to repeat their last used move.
* *
* Used for [Instruct](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move)). * Used for [Instruct](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move)).
*/ */
export class RepeatMoveAttr extends MoveEffectAttr { export class RepeatMoveAttr extends MoveEffectAttr {
private movesetMove: PokemonMove;
constructor() { constructor() {
super(false, { trigger: MoveEffectTrigger.POST_APPLY }); // needed to ensure correct protect interaction super(false, { trigger: MoveEffectTrigger.POST_APPLY }); // needed to ensure correct protect interaction
} }
/** /**
* Forces the target to re-use their last used move again * Forces the target to re-use their last used move again.
* * @param user - The {@linkcode Pokemon} using the attack
* @param user {@linkcode Pokemon} that used the attack * @param target - The {@linkcode Pokemon} being targeted by the attack
* @param target {@linkcode Pokemon} targeted by the attack
* @param move N/A
* @param args N/A
* @returns `true` if the move succeeds * @returns `true` if the move succeeds
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon): boolean {
// get the last move used (excluding status based failures) as well as the corresponding moveset slot // get the last move used (excluding status based failures) as well as the corresponding moveset slot
const lastMove = target.getLastXMoves(-1).find(m => m.move !== MoveId.NONE)!; // bangs are justified as Instruct fails if no prior move or moveset move exists
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move)!; // TODO: How does instruct work when copying a move called via Copycat that the user itself knows?
// If the last move used can hit more than one target or has variable targets, const lastMove = target.getLastNonVirtualMove()!;
// re-compute the targets for the attack const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)!
// (mainly for alternating double/single battle shenanigans)
// Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct
// TODO: Fix this once dragon darts gets smart targeting
let moveTargets = movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, lastMove.move).targets : lastMove.targets;
/** In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible // If the last move used can hit more than one target or has variable targets,
Normally, all yet-unexecuted move phases would swap over when the enemy in question faints // re-compute the targets for the attack (mainly for alternating double/single battles)
(see `redirectPokemonMoves` in `battle-scene.ts`), // Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct,
but since instruct adds a new move phase pre-emptively, we need to handle this interaction manually. // nor is Dragon Darts (due to its smart targeting bypassing normal target selection)
*/ let moveTargets = this.movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, this.movesetMove.moveId).targets : lastMove.targets;
// In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible.
// Normally, all yet-unexecuted move phases would swap targets after any foe faints or flees (see `redirectPokemonMoves` in `battle-scene.ts`),
// but since Instruct adds a new move phase _after_ all that occurs, we need to handle this interaction manually.
const firstTarget = globalScene.getField()[moveTargets[0]]; const firstTarget = globalScene.getField()[moveTargets[0]];
if (globalScene.currentBattle.double && moveTargets.length === 1 && firstTarget.isFainted() && firstTarget !== target.getAlly()) { if (
globalScene.currentBattle.double
&& moveTargets.length === 1
&& firstTarget.isFainted()
&& firstTarget !== target.getAlly()
) {
const ally = firstTarget.getAlly(); const ally = firstTarget.getAlly();
if (!isNullOrUndefined(ally) && ally.isActive()) { // ally exists, is not dead and can sponge the blast if (!isNullOrUndefined(ally) && ally.isActive()) {
moveTargets = [ ally.getBattlerIndex() ]; moveTargets = [ ally.getBattlerIndex() ];
} }
} }
@ -7086,15 +7105,15 @@ export class RepeatMoveAttr extends MoveEffectAttr {
userPokemonName: getPokemonNameWithAffix(user), userPokemonName: getPokemonNameWithAffix(user),
targetPokemonName: getPokemonNameWithAffix(target) targetPokemonName: getPokemonNameWithAffix(target)
})); }));
target.getMoveQueue().unshift({ move: lastMove.move, targets: moveTargets, ignorePP: false });
target.turnData.extraTurns++; target.turnData.extraTurns++;
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove); globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL);
return true; return true;
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user, target, move) => { return (_user, target, _move) => {
const lastMove = target.getLastXMoves(-1).find(m => m.move !== MoveId.NONE); // TODO: Check instruct behavior with struggle - ignore, fail or success
const lastMove = target.getLastNonVirtualMove();
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move); const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
const uninstructableMoves = [ const uninstructableMoves = [
// Locking/Continually Executed moves // Locking/Continually Executed moves
@ -7104,6 +7123,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
MoveId.PETAL_DANCE, MoveId.PETAL_DANCE,
MoveId.THRASH, MoveId.THRASH,
MoveId.ICE_BALL, MoveId.ICE_BALL,
MoveId.UPROAR,
// Multi-turn Moves // Multi-turn Moves
MoveId.BIDE, MoveId.BIDE,
MoveId.SHELL_TRAP, MoveId.SHELL_TRAP,
@ -7141,23 +7161,34 @@ export class RepeatMoveAttr extends MoveEffectAttr {
MoveId.SOLAR_BEAM, MoveId.SOLAR_BEAM,
MoveId.SOLAR_BLADE, MoveId.SOLAR_BLADE,
MoveId.METEOR_BEAM, MoveId.METEOR_BEAM,
// Other moves // Copying/Move-Calling moves
MoveId.ASSIST,
MoveId.COPYCAT,
MoveId.ME_FIRST,
MoveId.METRONOME,
MoveId.MIRROR_MOVE,
MoveId.NATURE_POWER,
MoveId.SLEEP_TALK,
MoveId.SNATCH,
MoveId.INSTRUCT, MoveId.INSTRUCT,
// Misc moves
MoveId.KINGS_SHIELD, MoveId.KINGS_SHIELD,
MoveId.SKETCH, MoveId.SKETCH,
MoveId.TRANSFORM, MoveId.TRANSFORM,
MoveId.MIMIC, MoveId.MIMIC,
MoveId.STRUGGLE, MoveId.STRUGGLE,
// TODO: Add Max/G-Move blockage if or when they are implemented // TODO: Add Max/G-Max/Z-Move blockage if or when they are implemented
]; ];
if (!lastMove?.move // no move to instruct if (!lastMove?.move // no move to instruct
|| !movesetMove // called move not in target's moveset (forgetting the move, etc.) || !movesetMove // called move not in target's moveset (forgetting the move, etc.)
|| movesetMove.ppUsed === movesetMove.getMovePp() // move out of pp || movesetMove.ppUsed === movesetMove.getMovePp() // move out of pp
// TODO: This next line is likely redundant as all charging moves are in the above list
|| allMoves[lastMove.move].isChargingMove() // called move is a charging/recharging move || allMoves[lastMove.move].isChargingMove() // called move is a charging/recharging move
|| uninstructableMoves.includes(lastMove.move)) { // called move is in the banlist || uninstructableMoves.includes(lastMove.move)) { // called move is in the banlist
return false; return false;
} }
this.movesetMove = movesetMove;
return true; return true;
}; };
} }
@ -7188,53 +7219,48 @@ export class ReducePpMoveAttr extends MoveEffectAttr {
/** /**
* Reduces the PP of the target's last-used move by an amount based on this attribute instance's {@linkcode reduction}. * Reduces the PP of the target's last-used move by an amount based on this attribute instance's {@linkcode reduction}.
* *
* @param user {@linkcode Pokemon} that used the attack * @param user - N/A
* @param target {@linkcode Pokemon} targeted by the attack * @param target - The {@linkcode Pokemon} targeted by the attack
* @param move N/A * @param move - N/A
* @param args N/A * @param args - N/A
* @returns `true` * @returns always `true`
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// Null checks can be skipped due to condition function /** The last move the target themselves used */
const lastMove = target.getLastXMoves()[0]; const lastMove = target.getLastNonVirtualMove();
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move)!; const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)!; // bang is correct as condition prevents this from being nullish
const lastPpUsed = movesetMove.ppUsed; const lastPpUsed = movesetMove.ppUsed;
movesetMove.ppUsed = Math.min((lastPpUsed) + this.reduction, movesetMove.getMovePp()); movesetMove.ppUsed = Math.min(lastPpUsed + this.reduction, movesetMove.getMovePp());
const message = i18next.t("battle:ppReduced", { targetName: getPokemonNameWithAffix(target), moveName: movesetMove.getName(), reduction: (movesetMove.ppUsed) - lastPpUsed });
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(target.id, movesetMove.getMove(), movesetMove.ppUsed)); globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(target.id, movesetMove.getMove(), movesetMove.ppUsed));
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(i18next.t("battle:ppReduced", { targetName: getPokemonNameWithAffix(target), moveName: movesetMove.getName(), reduction: (movesetMove.ppUsed) - lastPpUsed }));
return true; return true;
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user, target, move) => { return (user, target, move) => {
const lastMove = target.getLastXMoves()[0]; const lastMove = target.getLastNonVirtualMove();
if (lastMove) { const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move); return !!movesetMove?.getPpRatio();
return !!movesetMove?.getPpRatio();
}
return false;
}; };
} }
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const lastMove = target.getLastXMoves()[0]; const lastMove = target.getLastNonVirtualMove();
if (lastMove) { const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move); if (!movesetMove) {
if (movesetMove) { return 0;
const maxPp = movesetMove.getMovePp();
const ppLeft = maxPp - movesetMove.ppUsed;
const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5));
if (ppLeft < 4) {
return (value / 4) * ppLeft;
}
return value;
}
} }
return 0; const maxPp = movesetMove.getMovePp();
const ppLeft = maxPp - movesetMove.ppUsed;
const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5));
if (ppLeft < 4) {
return (value / 4) * ppLeft;
}
return value;
} }
} }
@ -7250,40 +7276,36 @@ export class AttackReducePpMoveAttr extends ReducePpMoveAttr {
/** /**
* Checks if the target has used a move prior to the attack. PP-reduction is applied through the super class if so. * Checks if the target has used a move prior to the attack. PP-reduction is applied through the super class if so.
* *
* @param user {@linkcode Pokemon} that used the attack * @param user - The {@linkcode Pokemon} using the move
* @param target {@linkcode Pokemon} targeted by the attack * @param target -The {@linkcode Pokemon} targeted by the attack
* @param move {@linkcode Move} being used * @param move - The {@linkcode Move} being used
* @param args N/A * @param args - N/A
* @returns {boolean} true * @returns - always `true`
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const lastMove = target.getLastXMoves().find(() => true); const lastMove = target.getLastNonVirtualMove();
if (lastMove) { const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move); if (movesetMove?.getPpRatio()) {
if (Boolean(movesetMove?.getPpRatio())) { super.apply(user, target, move, args);
super.apply(user, target, move, args);
}
} }
return true; return true;
} }
// Override condition function to always perform damage. Instead, perform pp-reduction condition check in apply function above /**
getCondition(): MoveConditionFunc { * Override condition function to always perform damage.
return (user, target, move) => true; * Instead, perform pp-reduction condition check in {@linkcode apply}.
* (A failed condition will prevent damage which is not what we want here)
* @returns always `true`
*/
override getCondition(): MoveConditionFunc {
return () => true;
} }
} }
// TODO: Review this
const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => { const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
const targetMoves = target.getMoveHistory().filter(m => !m.virtual); const copiableMove = target.getLastNonVirtualMove();
if (!targetMoves.length) { if (!copiableMove?.move) {
return false;
}
const copiableMove = targetMoves[0];
if (!copiableMove.move) {
return false; return false;
} }
@ -7296,14 +7318,18 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
return true; return true;
}; };
/**
* Attribute to temporarily copy the last move in the target's moveset.
* Used by {@linkcode Moves.MIMIC}.
*/
export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr { export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const targetMoves = target.getMoveHistory().filter(m => !m.virtual); const lastMove = target.getLastNonVirtualMove()
if (!targetMoves.length) { if (!lastMove?.move) {
return false; return false;
} }
const copiedMove = allMoves[targetMoves[0].move]; const copiedMove = allMoves[lastMove.move];
const thisMoveIndex = user.getMoveset().findIndex(m => m.moveId === move.id); const thisMoveIndex = user.getMoveset().findIndex(m => m.moveId === move.id);
@ -7311,8 +7337,9 @@ export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr {
return false; return false;
} }
// Populate summon data with a copy of the current moveset, replacing the copying move with the copied move
user.summonData.moveset = user.getMoveset().slice(0); user.summonData.moveset = user.getMoveset().slice(0);
user.summonData.moveset[thisMoveIndex] = new PokemonMove(copiedMove.id, 0, 0); user.summonData.moveset[thisMoveIndex] = new PokemonMove(copiedMove.id);
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:copiedMove", { pokemonName: getPokemonNameWithAffix(user), moveName: copiedMove.name })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:copiedMove", { pokemonName: getPokemonNameWithAffix(user), moveName: copiedMove.name }));
@ -7350,9 +7377,9 @@ export class SketchAttr extends MoveEffectAttr {
return false; return false;
} }
const targetMove = target.getLastXMoves(-1) const targetMove = target.getLastNonVirtualMove()
.find(m => m.move !== MoveId.NONE && m.move !== MoveId.STRUGGLE && !m.virtual);
if (!targetMove) { if (!targetMove) {
// failsafe for TS compiler
return false; return false;
} }
@ -7375,28 +7402,10 @@ export class SketchAttr extends MoveEffectAttr {
return false; return false;
} }
const targetMove = target.getMoveHistory().filter(m => !m.virtual).at(-1); const targetMove = target.getLastNonVirtualMove();
if (!targetMove) { return !isNullOrUndefined(targetMove)
return false; && !invalidSketchMoves.has(targetMove.move)
} && user.getMoveset().every(m => m.moveId !== targetMove.move)
const unsketchableMoves = [
MoveId.CHATTER,
MoveId.MIRROR_MOVE,
MoveId.SLEEP_TALK,
MoveId.STRUGGLE,
MoveId.SKETCH,
MoveId.REVIVAL_BLESSING,
MoveId.TERA_STARSTORM,
MoveId.BREAKNECK_BLITZ__PHYSICAL,
MoveId.BREAKNECK_BLITZ__SPECIAL
];
if (unsketchableMoves.includes(targetMove.move)) {
return false;
}
return !user.getMoveset().some(m => m.moveId === targetMove.move);
}; };
} }
} }
@ -7836,19 +7845,19 @@ export class LastResortAttr extends MoveAttr {
// TODO: Verify behavior as Bulbapedia page is _extremely_ poorly documented // TODO: Verify behavior as Bulbapedia page is _extremely_ poorly documented
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user: Pokemon, _target: Pokemon, move: Move) => { return (user: Pokemon, _target: Pokemon, move: Move) => {
const movesInMoveset = new Set<MoveId>(user.getMoveset().map(m => m.moveId)); const otherMovesInMoveset = new Set<MoveId>(user.getMoveset().map(m => m.moveId));
if (!movesInMoveset.delete(move.id) || !movesInMoveset.size) { if (!otherMovesInMoveset.delete(move.id) || !otherMovesInMoveset.size) {
return false; // Last resort fails if used when not in user's moveset or no other moves exist return false; // Last resort fails if used when not in user's moveset or no other moves exist
} }
const movesInHistory = new Set( const movesInHistory = new Set<MoveId>(
user.getMoveHistory() user.getMoveHistory()
.filter(m => !m.virtual) // TODO: Change to (m) => m < MoveUseType.INDIRECT after Dancer PR refactors virtual into enum .filter(m => !isVirtual(m.useMode)) // Last resort ignores virtual moves
.map(m => m.move) .map(m => m.move)
); );
// Since `Set.intersection()` is only present in ESNext, we have to coerce it to an array to check inclusion // Since `Set.intersection()` is only present in ESNext, we have to do this to check inclusion
return [...movesInMoveset].every(m => movesInHistory.has(m)) return [...otherMovesInMoveset].every(m => movesInHistory.has(m))
}; };
} }
} }
@ -7870,27 +7879,26 @@ export class VariableTargetAttr extends MoveAttr {
} }
/** /**
* Attribute for {@linkcode MoveId.AFTER_YOU} * Attribute to cause the target to move immediately after the user.
* *
* [After You - Move | Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/After_You_(move)) * Used by {@linkcode Moves.AFTER_YOU}.
*/ */
export class AfterYouAttr extends MoveEffectAttr { export class AfterYouAttr extends MoveEffectAttr {
/** /**
* Allows the target of this move to act right after the user. * Cause the target of this move to act right after the user.
* * @param user - Unused
* @param user {@linkcode Pokemon} that is using the move. * @param target - The {@linkcode Pokemon} targeted by this move
* @param target {@linkcode Pokemon} that will move right after this move is used. * @param _move - Unused
* @param move {@linkcode Move} {@linkcode MoveId.AFTER_YOU} * @param _args - Unused
* @param _args N/A * @returns `true`
* @returns true
*/ */
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
//Will find next acting phase of the targeted pokémon, delete it and queue it next on successful delete. // Will find next acting phase of the targeted pokémon, delete it and queue it right after us.
const nextAttackPhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target); const targetNextPhase = globalScene.phaseManager.findPhase<MovePhase>(phase => phase.pokemon === target);
if (nextAttackPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
globalScene.phaseManager.prependNewToPhase("MovePhase", "MovePhase", target, [ ...nextAttackPhase.targets ], nextAttackPhase.move); globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase");
} }
return true; return true;
@ -7915,6 +7923,7 @@ export class ForceLastAttr extends MoveEffectAttr {
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
// TODO: Refactor this to be more readable and less janky
const targetMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target); const targetMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target);
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
// Finding the phase to insert the move in front of - // Finding the phase to insert the move in front of -
@ -7927,7 +7936,7 @@ export class ForceLastAttr extends MoveEffectAttr {
globalScene.phaseManager.phaseQueue.splice( globalScene.phaseManager.phaseQueue.splice(
globalScene.phaseManager.phaseQueue.indexOf(prependPhase), globalScene.phaseManager.phaseQueue.indexOf(prependPhase),
0, 0,
globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, false, false, false, true) globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true)
); );
} }
} }
@ -7935,7 +7944,13 @@ export class ForceLastAttr extends MoveEffectAttr {
} }
} }
/** Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target} */ /**
* Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}.
* TODO:
- Make this a class method
- Make this look at speed order from TurnStartPhase
*/
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => { const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
let slower: boolean; let slower: boolean;
// quashed pokemon still have speed ties // quashed pokemon still have speed ties
@ -8030,8 +8045,7 @@ export class UpperHandCondition extends MoveCondition {
super((user, target, move) => { super((user, target, move) => {
const targetCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()]; const targetCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
return !!targetCommand return targetCommand?.command === Command.FIGHT
&& targetCommand.command === Command.FIGHT
&& !target.turnData.acted && !target.turnData.acted
&& !!targetCommand.move?.move && !!targetCommand.move?.move
&& allMoves[targetCommand.move.move].category !== MoveCategory.STATUS && allMoves[targetCommand.move.move].category !== MoveCategory.STATUS
@ -8064,6 +8078,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true); super(true);
} }
/** /**
* User changes its type to a random type that resists the target's last used move * User changes its type to a random type that resists the target's last used move
* @param {Pokemon} user Pokemon that used the move and will change types * @param {Pokemon} user Pokemon that used the move and will change types
@ -8077,7 +8092,8 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
return false; return false;
} }
const [ targetMove ] = target.getLastXMoves(1); // target's most recent move // TODO: Confirm how this interacts with status-induced failures and called moves
const targetMove = target.getLastXMoves(1)[0]; // target's most recent move
if (!targetMove) { if (!targetMove) {
return false; return false;
} }
@ -8119,9 +8135,9 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
// TODO: Does this count dancer?
return (user, target, move) => { return (user, target, move) => {
const moveHistory = target.getLastXMoves(); return target.getLastXMoves(-1).some(tm => tm.move !== MoveId.NONE);
return moveHistory.length !== 0;
}; };
} }
} }
@ -8539,9 +8555,9 @@ export function initMoves() {
.attr(FixedDamageAttr, 20), .attr(FixedDamageAttr, 20),
new StatusMove(MoveId.DISABLE, PokemonType.NORMAL, 100, 20, -1, 0, 1) new StatusMove(MoveId.DISABLE, PokemonType.NORMAL, 100, 20, -1, 0, 1)
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
.condition((user, target, move) => { .condition((_user, target, _move) => {
const lastRealMove = target.getLastXMoves(-1).find(m => !m.virtual); const lastNonVirtualMove = target.getLastNonVirtualMove();
return !isNullOrUndefined(lastRealMove) && lastRealMove.move !== MoveId.NONE && lastRealMove.move !== MoveId.STRUGGLE; return !isNullOrUndefined(lastNonVirtualMove) && lastNonVirtualMove.move !== MoveId.STRUGGLE;
}) })
.ignoresSubstitute() .ignoresSubstitute()
.reflectable(), .reflectable(),
@ -9090,7 +9106,10 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
.ignoresSubstitute() .ignoresSubstitute()
.condition((user, target, move) => new EncoreTag(user.id).canAdd(target)) .condition((user, target, move) => new EncoreTag(user.id).canAdd(target))
.reflectable(), .reflectable()
// Can lock infinitely into struggle; has incorrect interactions with Blood Moon/Gigaton Hammer
// Also may or may not incorrectly select targets for replacement move (needs verification)
.edgeCase(),
new AttackMove(MoveId.PURSUIT, PokemonType.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2) new AttackMove(MoveId.PURSUIT, PokemonType.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2)
.partial(), // No effect implemented .partial(), // No effect implemented
new AttackMove(MoveId.RAPID_SPIN, PokemonType.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2) new AttackMove(MoveId.RAPID_SPIN, PokemonType.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2)
@ -9972,7 +9991,14 @@ export function initMoves() {
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
.condition(failOnGravityCondition) .condition(failOnGravityCondition)
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
.partial(), // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/ /*
* Cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/:
* Should immobilize and give target semi-invulnerability
* Flying types should take no damage
* Should fail on targets above a certain weight threshold
* Should remove all redirection effects on successful takeoff (Rage Poweder, etc.)
*/
.partial(),
new SelfStatusMove(MoveId.SHIFT_GEAR, PokemonType.STEEL, -1, 10, -1, 0, 5) new SelfStatusMove(MoveId.SHIFT_GEAR, PokemonType.STEEL, -1, 10, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
@ -10543,9 +10569,12 @@ export function initMoves() {
new StatusMove(MoveId.INSTRUCT, PokemonType.PSYCHIC, -1, 15, -1, 0, 7) new StatusMove(MoveId.INSTRUCT, PokemonType.PSYCHIC, -1, 15, -1, 0, 7)
.ignoresSubstitute() .ignoresSubstitute()
.attr(RepeatMoveAttr) .attr(RepeatMoveAttr)
// incorrect interactions with Gigaton Hammer, Blood Moon & Torment /*
// Also has incorrect interactions with Dancer due to the latter * Incorrect interactions with Gigaton Hammer, Blood Moon & Torment due to them _failing on use_, not merely being unselectable.
// erroneously adding copied moves to move history. * Incorrectly ticks down Encore's fail counter
* TODO: Verify whether Instruct can repeat Struggle
* TODO: Verify whether Instruct can fail when using a copied move also in one's own moveset
*/
.edgeCase(), .edgeCase(),
new AttackMove(MoveId.BEAK_BLAST, PokemonType.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7) new AttackMove(MoveId.BEAK_BLAST, PokemonType.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7)
.attr(BeakBlastHeaderAttr) .attr(BeakBlastHeaderAttr)
@ -10603,7 +10632,13 @@ export function initMoves() {
.bitingMove() .bitingMove()
.attr(RemoveScreensAttr), .attr(RemoveScreensAttr),
new AttackMove(MoveId.STOMPING_TANTRUM, PokemonType.GROUND, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 7) new AttackMove(MoveId.STOMPING_TANTRUM, PokemonType.GROUND, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 7)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.getLastXMoves(2)[1]?.result === MoveResult.MISS || user.getLastXMoves(2)[1]?.result === MoveResult.FAIL ? 2 : 1), .attr(MovePowerMultiplierAttr, (user) => {
// Stomping tantrum triggers on most failures (including sleep/freeze)
const lastNonDancerMove = user.getLastXMoves(2)[1] as TurnMove | undefined;
return lastNonDancerMove && (lastNonDancerMove.result === MoveResult.MISS || lastNonDancerMove.result === MoveResult.FAIL) ? 2 : 1
})
// TODO: Review mainline accuracy and draft tests as needed
.edgeCase(),
new AttackMove(MoveId.SHADOW_BONE, PokemonType.GHOST, MoveCategory.PHYSICAL, 85, 100, 10, 20, 0, 7) new AttackMove(MoveId.SHADOW_BONE, PokemonType.GHOST, MoveCategory.PHYSICAL, 85, 100, 10, 20, 0, 7)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.makesContact(false), .makesContact(false),

View File

@ -21,7 +21,6 @@ export class PokemonMove {
public moveId: MoveId; public moveId: MoveId;
public ppUsed: number; public ppUsed: number;
public ppUp: number; public ppUp: number;
public virtual: boolean;
/** /**
* If defined and nonzero, overrides the maximum PP of the move (e.g., due to move being copied by Transform). * If defined and nonzero, overrides the maximum PP of the move (e.g., due to move being copied by Transform).
@ -29,11 +28,10 @@ export class PokemonMove {
*/ */
public maxPpOverride?: number; public maxPpOverride?: number;
constructor(moveId: MoveId, ppUsed = 0, ppUp = 0, virtual = false, maxPpOverride?: number) { constructor(moveId: MoveId, ppUsed = 0, ppUp = 0, maxPpOverride?: number) {
this.moveId = moveId; this.moveId = moveId;
this.ppUsed = ppUsed; this.ppUsed = ppUsed;
this.ppUp = ppUp; this.ppUp = ppUp;
this.virtual = virtual;
this.maxPpOverride = maxPpOverride; this.maxPpOverride = maxPpOverride;
} }
@ -47,6 +45,7 @@ export class PokemonMove {
* @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`. * @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`.
*/ */
isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean { isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean {
// TODO: Add Sky Drop's 1 turn stall
if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)) { if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)) {
return false; return false;
} }
@ -88,6 +87,6 @@ export class PokemonMove {
* @returns A valid {@linkcode PokemonMove} object * @returns A valid {@linkcode PokemonMove} object
*/ */
static loadMove(source: PokemonMove | any): PokemonMove { static loadMove(source: PokemonMove | any): PokemonMove {
return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.virtual, source.maxPpOverride); return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.maxPpOverride);
} }
} }

View File

@ -38,6 +38,7 @@ import type HeldModifierConfig from "#app/@types/held-modifier-config";
import type { BerryType } from "#enums/berry-type"; import type { BerryType } from "#enums/berry-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import i18next from "i18next"; import i18next from "i18next";
import { MoveUseMode } from "#enums/move-use-mode";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/absoluteAvarice"; const namespace = "mysteryEncounters/absoluteAvarice";
@ -307,7 +308,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.ENEMY], targets: [BattlerIndex.ENEMY],
move: new PokemonMove(MoveId.STUFF_CHEEKS), move: new PokemonMove(MoveId.STUFF_CHEEKS),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}); });
await transitionMysteryEncounterIntroVisuals(true, true, 500); await transitionMysteryEncounterIntroVisuals(true, true, 500);

View File

@ -13,7 +13,6 @@ import { TrainerPartyCompoundTemplate } from "#app/data/trainers/TrainerPartyTem
import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate";
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
@ -49,7 +48,8 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { allAbilities } from "#app/data/data-lists"; import { MoveUseMode } from "#enums/move-use-mode";
import { allAbilities, modifierTypes } from "#app/data/data-lists";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/clowningAround"; const namespace = "mysteryEncounters/clowningAround";
@ -210,19 +210,19 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.ENEMY_2], targets: [BattlerIndex.ENEMY_2],
move: new PokemonMove(MoveId.ROLE_PLAY), move: new PokemonMove(MoveId.ROLE_PLAY),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}, },
{ {
sourceBattlerIndex: BattlerIndex.ENEMY_2, sourceBattlerIndex: BattlerIndex.ENEMY_2,
targets: [BattlerIndex.PLAYER], targets: [BattlerIndex.PLAYER],
move: new PokemonMove(MoveId.TAUNT), move: new PokemonMove(MoveId.TAUNT),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}, },
{ {
sourceBattlerIndex: BattlerIndex.ENEMY_2, sourceBattlerIndex: BattlerIndex.ENEMY_2,
targets: [BattlerIndex.PLAYER_2], targets: [BattlerIndex.PLAYER_2],
move: new PokemonMove(MoveId.TAUNT), move: new PokemonMove(MoveId.TAUNT),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}, },
); );

View File

@ -40,6 +40,7 @@ import { PokeballType } from "#enums/pokeball";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import i18next from "i18next"; import i18next from "i18next";
import { MoveUseMode } from "#enums/move-use-mode";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/dancingLessons"; const namespace = "mysteryEncounters/dancingLessons";
@ -214,7 +215,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER], targets: [BattlerIndex.PLAYER],
move: new PokemonMove(MoveId.REVELATION_DANCE), move: new PokemonMove(MoveId.REVELATION_DANCE),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}); });
await hideOricorioPokemon(); await hideOricorioPokemon();

View File

@ -10,7 +10,6 @@ import {
generateModifierType, generateModifierType,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; import type { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/data/data-lists";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
@ -46,7 +45,8 @@ import { AbilityId } from "#enums/ability-id";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { allAbilities } from "#app/data/data-lists"; import { MoveUseMode } from "#enums/move-use-mode";
import { allAbilities, modifierTypes } from "#app/data/data-lists";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fieryFallout"; const namespace = "mysteryEncounters/fieryFallout";
@ -201,13 +201,13 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER], targets: [BattlerIndex.PLAYER],
move: new PokemonMove(MoveId.FIRE_SPIN), move: new PokemonMove(MoveId.FIRE_SPIN),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}, },
{ {
sourceBattlerIndex: BattlerIndex.ENEMY_2, sourceBattlerIndex: BattlerIndex.ENEMY_2,
targets: [BattlerIndex.PLAYER_2], targets: [BattlerIndex.PLAYER_2],
move: new PokemonMove(MoveId.FIRE_SPIN), move: new PokemonMove(MoveId.FIRE_SPIN),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}, },
); );
await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]);

View File

@ -11,10 +11,7 @@ import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requir
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
import type { ModifierTypeOption } from "#app/modifier/modifier-type"; import type { ModifierTypeOption } from "#app/modifier/modifier-type";
import { import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
getPlayerModifierTypeOptions,
regenerateModifierPoolThresholds,
} from "#app/modifier/modifier-type";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";

View File

@ -7,10 +7,7 @@ import { TrainerSlot } from "#enums/trainer-slot";
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
import { MusicPreference } from "#app/system/settings/settings"; import { MusicPreference } from "#app/system/settings/settings";
import type { ModifierTypeOption } from "#app/modifier/modifier-type"; import type { ModifierTypeOption } from "#app/modifier/modifier-type";
import { import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
getPlayerModifierTypeOptions,
regenerateModifierPoolThresholds,
} from "#app/modifier/modifier-type";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";

View File

@ -31,6 +31,7 @@ import { BerryType } from "#enums/berry-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";
import { MoveUseMode } from "#enums/move-use-mode";
/** i18n namespace for the encounter */ /** i18n namespace for the encounter */
const namespace = "mysteryEncounters/slumberingSnorlax"; const namespace = "mysteryEncounters/slumberingSnorlax";
@ -137,7 +138,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER], targets: [BattlerIndex.PLAYER],
move: new PokemonMove(MoveId.SNORE), move: new PokemonMove(MoveId.SNORE),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}); });
await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]);
}, },

View File

@ -28,6 +28,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { MoveUseMode } from "#enums/move-use-mode";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theStrongStuff"; const namespace = "mysteryEncounters/theStrongStuff";
@ -214,13 +215,13 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER], targets: [BattlerIndex.PLAYER],
move: new PokemonMove(MoveId.GASTRO_ACID), move: new PokemonMove(MoveId.GASTRO_ACID),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}, },
{ {
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER], targets: [BattlerIndex.PLAYER],
move: new PokemonMove(MoveId.STEALTH_ROCK), move: new PokemonMove(MoveId.STEALTH_ROCK),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}, },
); );

View File

@ -28,6 +28,7 @@ import { BattlerIndex } from "#enums/battler-index";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";
import { MoveUseMode } from "#enums/move-use-mode";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/trashToTreasure"; const namespace = "mysteryEncounters/trashToTreasure";
@ -207,13 +208,13 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER], targets: [BattlerIndex.PLAYER],
move: new PokemonMove(MoveId.TOXIC), move: new PokemonMove(MoveId.TOXIC),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}, },
{ {
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.ENEMY], targets: [BattlerIndex.ENEMY],
move: new PokemonMove(MoveId.STOCKPILE), move: new PokemonMove(MoveId.STOCKPILE),
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}, },
); );
await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]);

View File

@ -36,6 +36,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun
import { BerryModifier } from "#app/modifier/modifier"; import { BerryModifier } from "#app/modifier/modifier";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { MoveUseMode } from "#enums/move-use-mode";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/uncommonBreed"; const namespace = "mysteryEncounters/uncommonBreed";
@ -180,7 +181,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder.
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [target], targets: [target],
move: pokemonMove, move: pokemonMove,
ignorePp: true, useMode: MoveUseMode.IGNORE_PP,
}); });
} }

View File

@ -29,14 +29,14 @@ import type { GameModes } from "#enums/game-modes";
import type { EncounterAnim } from "#enums/encounter-anims"; import type { EncounterAnim } from "#enums/encounter-anims";
import type { Challenges } from "#enums/challenges"; import type { Challenges } from "#enums/challenges";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { MoveUseMode } from "#enums/move-use-mode";
export interface EncounterStartOfBattleEffect { export interface EncounterStartOfBattleEffect {
sourcePokemon?: Pokemon; sourcePokemon?: Pokemon;
sourceBattlerIndex?: BattlerIndex; sourceBattlerIndex?: BattlerIndex;
targets: BattlerIndex[]; targets: BattlerIndex[];
move: PokemonMove; move: PokemonMove;
ignorePp: boolean; useMode: MoveUseMode; // TODO: This should always be ignore PP...
followUp?: boolean;
} }
const DEFAULT_MAX_ALLOWED_ENCOUNTERS = 2; const DEFAULT_MAX_ALLOWED_ENCOUNTERS = 2;
@ -254,7 +254,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
*/ */
selectedOption?: MysteryEncounterOption; selectedOption?: MysteryEncounterOption;
/** /**
* Will be set by option select handlers automatically, and can be used to refer to which option was chosen by later phases * Array containing data pertaining to free moves used at the start of a battle mystery envounter.
*/ */
startOfBattleEffects: EncounterStartOfBattleEffect[] = []; startOfBattleEffects: EncounterStartOfBattleEffect[] = [];
/** /**

View File

@ -1,5 +1,4 @@
import type Battle from "#app/battle"; import type Battle from "#app/battle";
import { BattlerIndex } from "#enums/battler-index";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { biomeLinks, BiomePoolTier } from "#app/data/balance/biomes"; import { biomeLinks, BiomePoolTier } from "#app/data/balance/biomes";
import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
@ -974,33 +973,8 @@ export function handleMysteryEncounterBattleStartEffects() {
) { ) {
const effects = encounter.startOfBattleEffects; const effects = encounter.startOfBattleEffects;
effects.forEach(effect => { effects.forEach(effect => {
let source: EnemyPokemon | Pokemon; const source = effect.sourcePokemon ?? globalScene.getField()[effect.sourceBattlerIndex ?? 0];
if (effect.sourcePokemon) { globalScene.phaseManager.pushNew("MovePhase", source, effect.targets, effect.move, effect.useMode);
source = effect.sourcePokemon;
} else if (!isNullOrUndefined(effect.sourceBattlerIndex)) {
if (effect.sourceBattlerIndex === BattlerIndex.ATTACKER) {
source = globalScene.getEnemyField()[0];
} else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY) {
source = globalScene.getEnemyField()[0];
} else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY_2) {
source = globalScene.getEnemyField()[1];
} else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER) {
source = globalScene.getPlayerField()[0];
} else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER_2) {
source = globalScene.getPlayerField()[1];
}
} else {
source = globalScene.getEnemyField()[0];
}
globalScene.phaseManager.pushNew(
"MovePhase",
// @ts-expect-error: source is guaranteed to be defined
source,
effect.targets,
effect.move,
effect.followUp,
effect.ignorePp,
);
}); });
// Pseudo turn end phase to reset flinch states, Endure, etc. // Pseudo turn end phase to reset flinch states, Endure, etc.

View File

@ -84,13 +84,14 @@ export const normalForm: SpeciesId[] = [
/** /**
* Gets the {@linkcode PokemonSpecies} object associated with the {@linkcode SpeciesId} enum given * Gets the {@linkcode PokemonSpecies} object associated with the {@linkcode SpeciesId} enum given
* @param species The species to fetch * @param species - The {@linkcode SpeciesId} to fetch.
* If an array of `SpeciesId`s is passed (such as for named trainer spawn pools),
* one will be selected at random.
* @returns The associated {@linkcode PokemonSpecies} object * @returns The associated {@linkcode PokemonSpecies} object
*/ */
export function getPokemonSpecies(species: SpeciesId | SpeciesId[]): PokemonSpecies { export function getPokemonSpecies(species: SpeciesId | SpeciesId[]): PokemonSpecies {
// If a special pool (named trainers) is used here it CAN happen that they have a array as species (which means choose one of those two). So we catch that with this code block
if (Array.isArray(species)) { if (Array.isArray(species)) {
// Pick a random species from the list // TODO: this RNG roll should not be handled by this function
species = species[Math.floor(Math.random() * species.length)]; species = species[Math.floor(Math.random() * species.length)];
} }
if (species >= 2000) { if (species >= 2000) {

View File

@ -1,12 +1,37 @@
/**
* Enum representing the possible ways a given BattlerTag can activate and/or tick down.
* Each tag can have multiple different behaviors attached to different lapse types.
*/
export enum BattlerTagLapseType { export enum BattlerTagLapseType {
// TODO: This is unused...
FAINT, FAINT,
/**
* Tag activate before the holder uses a non-virtual move, possibly interrupting its action.
* @see MoveUseMode for more information
*/
MOVE, MOVE,
/** Tag activates before the holder uses **any** move, triggering effects or interrupting its action. */
PRE_MOVE, PRE_MOVE,
/** Tag activates immediately after the holder's move finishes triggering (successful or not). */
AFTER_MOVE, AFTER_MOVE,
/**
* Tag activates before move effects are applied.
* TODO: Stop using this as a catch-all "semi-invulnerability" tag
*/
MOVE_EFFECT, MOVE_EFFECT,
/** Tag activates at the end of the turn. */
TURN_END, TURN_END,
/**
* Tag activates after the holder is hit by an attack, but before damage is applied.
* Occurs even if the user's {@linkcode SubstituteTag | Substitute} is hit.
*/
HIT, HIT,
/** Tag lapses AFTER_HIT, applying its effects even if the user faints */ /**
* Tag activates after the holder is directly hit by an attack.
* Does **not** occur on hits to the holder's {@linkcode SubstituteTag | Substitute},
* but still triggers on being KO'd.
*/
AFTER_HIT, AFTER_HIT,
CUSTOM /** The tag has some other custom activation or removal condition. */
CUSTOM,
} }

View File

@ -1,13 +0,0 @@
// biome-ignore lint/correctness/noUnusedImports: Used in tsdoc
import type ConfirmUiHandler from "#app/ui/confirm-ui-handler";
/**
* Used by {@linkcode ConfirmUiHandler} to determine whether the cursor should start on Yes or No
*/
export const ConfirmUiMode = Object.freeze({
/** Start cursor on Yes */
DEFAULT_YES: 1,
/** Start cursor on No */
DEFAULT_NO: 2
});
export type ConfirmUiMode = typeof ConfirmUiMode[keyof typeof ConfirmUiMode];

149
src/enums/move-use-mode.ts Normal file
View File

@ -0,0 +1,149 @@
import type { PostDancingMoveAbAttr } from "#app/data/abilities/ability";
import type { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
/**
* Enum representing all the possible means through which a given move can be executed.
* Each one inherits the properties (or exclusions) of all types preceding it.
* Properties newly found on a given use mode will be **bolded**,
* while oddities breaking a previous trend will be listed in _italics_.
* Callers should refrain from performing non-equality checks on `MoveUseMode`s directly,
* instead using the available helper functions
* ({@linkcode isVirtual}, {@linkcode isIgnoreStatus}, {@linkcode isIgnorePP} and {@linkcode isReflected}).
*/
export const MoveUseMode = {
/**
* This move was used normally (i.e. clicking on the button) or called via Instruct.
* It deducts PP from the user's moveset (failing if out of PP), and interacts normally with other moves and abilities.
*/
NORMAL: 1,
/**
* This move was called by an effect that ignores PP, such as a consecutively executed move (e.g. Outrage).
*
* PP-ignoring moves (as their name implies) **do not consume PP** when used
* and **will not fail** if none is left prior to execution.
* All other effects remain identical to {@linkcode MoveUseMode.NORMAL}.
*
* PP can still be reduced by other effects (such as Spite or Eerie Spell).
*/
IGNORE_PP: 2,
/**
* This move was called indirectly by an out-of-turn effect other than Instruct or the user's previous move.
* Currently only used by {@linkcode PostDancingMoveAbAttr | Dancer}.
*
* Indirect moves ignore PP checks similar to {@linkcode MoveUseMode.IGNORE_PP}, but additionally **cannot be copied**
* by all move-copying effects (barring reflection).
* They are also **"skipped over" by most moveset and move history-related effects** (PP reduction, Last Resort, etc).
*
* They still respect the user's volatile status conditions and confusion (though will uniquely _cure freeze and sleep before use_).
*/
INDIRECT: 3,
/**
* This move was called as part of another move's effect (such as for most {@link https://bulbapedia.bulbagarden.net/wiki/Category:Moves_that_call_other_moves | Move-calling moves}).
* Follow-up moves **bypass cancellation** from all **non-volatile status conditions** and **{@linkcode BattlerTagLapseType.MOVE}-type effects**
* (having been checked already on the calling move).
* They are _not ignored_ by other move-calling moves and abilities (unlike {@linkcode MoveUseMode.FOLLOW_UP} and {@linkcode MoveUseMode.REFLECTED}),
* but still inherit the former's disregard for moveset-related effects.
*/
FOLLOW_UP: 4,
/**
* This move was reflected by Magic Coat or Magic Bounce.
* Reflected moves ignore all the same cancellation checks as {@linkcode MoveUseMode.INDIRECT}
* and retain the same copy prevention as {@linkcode MoveUseMode.FOLLOW_UP}, but additionally
* **cannot be reflected by other reflecting effects**.
*/
REFLECTED: 5
// TODO: Add use type TRANSPARENT for Future Sight and Doom Desire to prevent move history pushing
} as const;
export type MoveUseMode = (typeof MoveUseMode)[keyof typeof MoveUseMode];
// # HELPER FUNCTIONS
// Please update the markdown tables if any new `MoveUseMode`s get added.
/**
* Check if a given {@linkcode MoveUseMode} is virtual (i.e. called by another move or effect).
* Virtual moves are ignored by most moveset-related effects due to not being executed directly.
* @returns Whether {@linkcode useMode} is virtual.
* @remarks
* This function is equivalent to the following truth table:
*
* | Use Type | Returns |
* |------------------------------------|---------|
* | {@linkcode MoveUseMode.NORMAL} | `false` |
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
* | {@linkcode MoveUseMode.INDIRECT} | `true` |
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
*/
export function isVirtual(useMode: MoveUseMode): boolean {
return useMode >= MoveUseMode.INDIRECT
}
/**
* Check if a given {@linkcode MoveUseMode} should ignore pre-move cancellation checks
* from {@linkcode StatusEffect.PARALYSIS} and {@linkcode BattlerTagLapseType.MOVE}-type effects.
* @param useMode - The {@linkcode MoveUseMode} to check.
* @returns Whether {@linkcode useMode} should ignore status and otehr cancellation checks.
* @remarks
* This function is equivalent to the following truth table:
*
* | Use Type | Returns |
* |------------------------------------|---------|
* | {@linkcode MoveUseMode.NORMAL} | `false` |
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
* | {@linkcode MoveUseMode.INDIRECT} | `false` |
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
*/
export function isIgnoreStatus(useMode: MoveUseMode): boolean {
return useMode >= MoveUseMode.FOLLOW_UP;
}
/**
* Check if a given {@linkcode MoveUseMode} should ignore PP.
* PP-ignoring moves will ignore normal PP consumption as well as associated failure checks.
* @param useMode - The {@linkcode MoveUseMode} to check.
* @returns Whether {@linkcode useMode} ignores PP.
* @remarks
* This function is equivalent to the following truth table:
*
* | Use Type | Returns |
* |------------------------------------|---------|
* | {@linkcode MoveUseMode.NORMAL} | `false` |
* | {@linkcode MoveUseMode.IGNORE_PP} | `true` |
* | {@linkcode MoveUseMode.INDIRECT} | `true` |
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
*/
export function isIgnorePP(useMode: MoveUseMode): boolean {
return useMode >= MoveUseMode.IGNORE_PP;
}
/**
* Check if a given {@linkcode MoveUseMode} is reflected.
* Reflected moves cannot be reflected, copied, or cancelled by status effects,
* nor will they trigger {@linkcode PostDancingMoveAbAttr | Dancer}.
* @param useMode - The {@linkcode MoveUseMode} to check.
* @returns Whether {@linkcode useMode} is reflected.
* @remarks
* This function is equivalent to the following truth table:
*
* | Use Type | Returns |
* |------------------------------------|---------|
* | {@linkcode MoveUseMode.NORMAL} | `false` |
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
* | {@linkcode MoveUseMode.INDIRECT} | `false` |
* | {@linkcode MoveUseMode.FOLLOW_UP} | `false` |
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
*/
export function isReflected(useMode: MoveUseMode): boolean {
return useMode === MoveUseMode.REFLECTED;
}

View File

@ -262,7 +262,7 @@ export class Arena {
return 5; return 5;
} }
break; break;
case SpeciesId.LYCANROC: case SpeciesId.LYCANROC: {
const timeOfDay = this.getTimeOfDay(); const timeOfDay = this.getTimeOfDay();
switch (timeOfDay) { switch (timeOfDay) {
case TimeOfDay.DAY: case TimeOfDay.DAY:
@ -274,6 +274,7 @@ export class Arena {
return 1; return 1;
} }
break; break;
}
} }
return 0; return 0;
@ -764,6 +765,9 @@ export class Arena {
); );
} }
// TODO: Add an overload similar to `Array.prototype.find` if the predicate func is of the form
// `(x): x is T`
/** /**
* Uses {@linkcode findTagsOnSide} to filter (using the parameter function) for specific tags that apply to both sides * Uses {@linkcode findTagsOnSide} to filter (using the parameter function) for specific tags that apply to both sides
* @param tagPredicate a function mapping {@linkcode ArenaTag}s to `boolean`s * @param tagPredicate a function mapping {@linkcode ArenaTag}s to `boolean`s

View File

@ -38,7 +38,6 @@ import {
deltaRgb, deltaRgb,
isBetween, isBetween,
randSeedFloat, randSeedFloat,
type nil,
type Constructor, type Constructor,
randSeedIntRange, randSeedIntRange,
coerceArray, coerceArray,
@ -186,6 +185,7 @@ import { doShinySparkleAnim } from "#app/field/anims";
import { MoveFlags } from "#enums/MoveFlags"; import { MoveFlags } from "#enums/MoveFlags";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader"; import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader";
import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { LearnMoveSituation } from "#enums/learn-move-situation"; import { LearnMoveSituation } from "#enums/learn-move-situation";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
@ -324,7 +324,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
super(globalScene, x, y); super(globalScene, x, y);
if (!species.isObtainable() && this.isPlayer()) { if (!species.isObtainable() && this.isPlayer()) {
throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`; throw `Cannot create a player Pokemon for species "${species.getName(formIndex)}"`;
} }
this.species = species; this.species = species;
@ -3137,7 +3137,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
while (rand > stabMovePool[index][1]) { while (rand > stabMovePool[index][1]) {
rand -= stabMovePool[index++][1]; rand -= stabMovePool[index++][1];
} }
this.moveset.push(new PokemonMove(stabMovePool[index][0], 0, 0)); this.moveset.push(new PokemonMove(stabMovePool[index][0]));
} }
while (baseWeights.length > this.moveset.length && this.moveset.length < 4) { while (baseWeights.length > this.moveset.length && this.moveset.length < 4) {
@ -3190,7 +3190,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
while (rand > movePool[index][1]) { while (rand > movePool[index][1]) {
rand -= movePool[index++][1]; rand -= movePool[index++][1];
} }
this.moveset.push(new PokemonMove(movePool[index][0], 0, 0)); this.moveset.push(new PokemonMove(movePool[index][0]));
} }
// Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes // Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes
@ -4109,7 +4109,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/**@overload */ /**@overload */
getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | nil; getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined;
/** @overload */ /** @overload */
getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined;
@ -4326,10 +4326,41 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return moveHistory.slice(0).reverse(); return moveHistory.slice(0).reverse();
} }
/**
* Return the most recently executed {@linkcode TurnMove} this {@linkcode Pokemon} has used that is:
* - Not {@linkcode MoveId.NONE}
* - Non-virtual ({@linkcode MoveUseMode | useMode} < {@linkcode MoveUseMode.INDIRECT})
* @param ignoreStruggle - Whether to additionally ignore {@linkcode Moves.STRUGGLE}; default `false`
* @param ignoreFollowUp - Whether to ignore moves with a use type of {@linkcode MoveUseMode.FOLLOW_UP}
* (e.g. ones called by Copycat/Mirror Move); default `true`.
* @returns The last move this Pokemon has used satisfying the aforementioned conditions,
* or `undefined` if no applicable moves have been used since switching in.
*/
getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined {
return this.getLastXMoves(-1).find(
m =>
m.move !== MoveId.NONE &&
(!ignoreStruggle || m.move !== MoveId.STRUGGLE) &&
(!isVirtual(m.useMode) || (!ignoreFollowUp && m.useMode === MoveUseMode.FOLLOW_UP)),
);
}
/**
* Return this Pokemon's move queue, consisting of all the moves it is slated to perform.
* @returns An array of {@linkcode TurnMove}, as described above
*/
getMoveQueue(): TurnMove[] { getMoveQueue(): TurnMove[] {
return this.summonData.moveQueue; return this.summonData.moveQueue;
} }
/**
* Add a new entry to the end of this Pokemon's move queue.
* @param queuedMove - A {@linkcode TurnMove} to push to this Pokemon's queue.
*/
pushMoveQueue(queuedMove: TurnMove): void {
this.summonData.moveQueue.push(queuedMove);
}
changeForm(formChange: SpeciesFormChange): Promise<void> { changeForm(formChange: SpeciesFormChange): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
this.formIndex = Math.max( this.formIndex = Math.max(
@ -4385,14 +4416,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// biome-ignore lint: there are a ton of issues.. // biome-ignore lint: there are a ton of issues..
faintCry(callback: Function): void { faintCry(callback: Function): void {
if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) { if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) {
return this.fusionFaintCry(callback); this.fusionFaintCry(callback);
return;
} }
const key = this.species.getCryKey(this.formIndex); const key = this.species.getCryKey(this.formIndex);
let rate = 0.85; let rate = 0.85;
const cry = globalScene.playSound(key, { rate: rate }) as AnySound; const cry = globalScene.playSound(key, { rate: rate }) as AnySound;
if (!cry || globalScene.fieldVolume === 0) { if (!cry || globalScene.fieldVolume === 0) {
return callback(); callback();
return;
} }
const sprite = this.getSprite(); const sprite = this.getSprite();
const tintSprite = this.getTintSprite(); const tintSprite = this.getTintSprite();
@ -4460,7 +4493,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
rate: rate, rate: rate,
}) as AnySound; }) as AnySound;
if (!cry || !fusionCry || globalScene.fieldVolume === 0) { if (!cry || !fusionCry || globalScene.fieldVolume === 0) {
return callback(); callback();
return;
} }
fusionCry.stop(); fusionCry.stop();
duration = Math.min(duration, fusionCry.totalDuration * 1000); duration = Math.min(duration, fusionCry.totalDuration * 1000);
@ -5984,7 +6018,7 @@ export class PlayerPokemon extends Pokemon {
copyMoveset(): PokemonMove[] { copyMoveset(): PokemonMove[] {
const newMoveset: PokemonMove[] = []; const newMoveset: PokemonMove[] = [];
this.moveset.forEach(move => { this.moveset.forEach(move => {
newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.virtual, move.maxPpOverride)); newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.maxPpOverride));
}); });
return newMoveset; return newMoveset;
@ -6164,33 +6198,39 @@ export class EnemyPokemon extends Pokemon {
* the Pokemon the move will target. * the Pokemon the move will target.
* @returns this Pokemon's next move in the format {move, moveTargets} * @returns this Pokemon's next move in the format {move, moveTargets}
*/ */
// TODO: split this up and move it elsewhere
getNextMove(): TurnMove { getNextMove(): TurnMove {
// If this Pokemon has a move already queued, return it. // If this Pokemon has a usable move already queued, return it,
// removing all unusable moves before it in the queue.
const moveQueue = this.getMoveQueue(); const moveQueue = this.getMoveQueue();
if (moveQueue.length !== 0) { for (const [i, queuedMove] of moveQueue.entries()) {
const queuedMove = moveQueue[0]; const movesetMove = this.getMoveset().find(m => m.moveId === queuedMove.move);
if (queuedMove) { // If the queued move was called indirectly, ignore all PP and usability checks.
const moveIndex = this.getMoveset().findIndex(m => m.moveId === queuedMove.move); // Otherwise, ensure that the move being used is actually usable & in our moveset.
if ( // TODO: What should happen if a pokemon forgets a charging move mid-use?
(moveIndex > -1 && this.getMoveset()[moveIndex].isUsable(this, queuedMove.ignorePP)) || if (isVirtual(queuedMove.useMode) || movesetMove?.isUsable(this, isIgnorePP(queuedMove.useMode))) {
queuedMove.virtual moveQueue.splice(0, i); // TODO: This should not be done here
) { return queuedMove;
return queuedMove;
}
this.getMoveQueue().shift();
return this.getNextMove();
} }
} }
// We went through the entire queue without a match; clear the entire thing.
this.summonData.moveQueue = [];
// Filter out any moves this Pokemon cannot use // Filter out any moves this Pokemon cannot use
let movePool = this.getMoveset().filter(m => m.isUsable(this)); let movePool = this.getMoveset().filter(m => m.isUsable(this));
// If no moves are left, use Struggle. Otherwise, continue with move selection // If no moves are left, use Struggle. Otherwise, continue with move selection
if (movePool.length) { if (movePool.length) {
// If there's only 1 move in the move pool, use it. // If there's only 1 move in the move pool, use it.
if (movePool.length === 1) { if (movePool.length === 1) {
return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) }; return {
move: movePool[0].moveId,
targets: this.getNextTargets(movePool[0].moveId),
useMode: MoveUseMode.NORMAL,
};
} }
// If a move is forced because of Encore, use it. // If a move is forced because of Encore, use it.
// Said moves are executed normally
const encoreTag = this.getTag(EncoreTag) as EncoreTag; const encoreTag = this.getTag(EncoreTag) as EncoreTag;
if (encoreTag) { if (encoreTag) {
const encoreMove = movePool.find(m => m.moveId === encoreTag.moveId); const encoreMove = movePool.find(m => m.moveId === encoreTag.moveId);
@ -6198,6 +6238,7 @@ export class EnemyPokemon extends Pokemon {
return { return {
move: encoreMove.moveId, move: encoreMove.moveId,
targets: this.getNextTargets(encoreMove.moveId), targets: this.getNextTargets(encoreMove.moveId),
useMode: MoveUseMode.NORMAL,
}; };
} }
} }
@ -6205,7 +6246,7 @@ export class EnemyPokemon extends Pokemon {
// No enemy should spawn with this AI type in-game // No enemy should spawn with this AI type in-game
case AiType.RANDOM: { case AiType.RANDOM: {
const moveId = movePool[globalScene.randBattleSeedInt(movePool.length)].moveId; const moveId = movePool[globalScene.randBattleSeedInt(movePool.length)].moveId;
return { move: moveId, targets: this.getNextTargets(moveId) }; return { move: moveId, targets: this.getNextTargets(moveId), useMode: MoveUseMode.NORMAL };
} }
case AiType.SMART_RANDOM: case AiType.SMART_RANDOM:
case AiType.SMART: { case AiType.SMART: {
@ -6374,14 +6415,20 @@ export class EnemyPokemon extends Pokemon {
r, r,
sortedMovePool.map(m => m.getName()), sortedMovePool.map(m => m.getName()),
); );
return { move: sortedMovePool[r]!.moveId, targets: moveTargets[sortedMovePool[r]!.moveId] }; return {
move: sortedMovePool[r]!.moveId,
targets: moveTargets[sortedMovePool[r]!.moveId],
useMode: MoveUseMode.NORMAL,
};
} }
} }
} }
// No moves left means struggle
return { return {
move: MoveId.STRUGGLE, move: MoveId.STRUGGLE,
targets: this.getNextTargets(MoveId.STRUGGLE), targets: this.getNextTargets(MoveId.STRUGGLE),
useMode: MoveUseMode.IGNORE_PP,
}; };
} }
@ -6727,10 +6774,9 @@ interface IllusionData {
export interface TurnMove { export interface TurnMove {
move: MoveId; move: MoveId;
targets: BattlerIndex[]; targets: BattlerIndex[];
useMode: MoveUseMode;
result?: MoveResult; result?: MoveResult;
virtual?: boolean;
turn?: number; turn?: number;
ignorePP?: boolean;
} }
export interface AttackMoveResult { export interface AttackMoveResult {
@ -6749,6 +6795,12 @@ export interface AttackMoveResult {
export class PokemonSummonData { export class PokemonSummonData {
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */ /** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
public statStages: number[] = [0, 0, 0, 0, 0, 0, 0]; public statStages: number[] = [0, 0, 0, 0, 0, 0, 0];
/**
* A queue of moves yet to be executed, used by charging, recharging and frenzy moves.
* So long as this array is nonempty, this Pokemon's corresponding `CommandPhase` will be skipped over entirely
* in favor of using the queued move.
* TODO: Clean up a lot of the code surrounding the move queue.
*/
public moveQueue: TurnMove[] = []; public moveQueue: TurnMove[] = [];
public tags: BattlerTag[] = []; public tags: BattlerTag[] = [];
public abilitySuppressed = false; public abilitySuppressed = false;
@ -6868,7 +6920,6 @@ export class PokemonWaveData {
* Resets at the start of a new turn, as well as on switch. * Resets at the start of a new turn, as well as on switch.
*/ */
export class PokemonTurnData { export class PokemonTurnData {
public flinched = false;
public acted = false; public acted = false;
/** How many times the current move should hit the target(s) */ /** How many times the current move should hit the target(s) */
public hitCount = 0; public hitCount = 0;
@ -6890,8 +6941,9 @@ export class PokemonTurnData {
public failedRunAway = false; public failedRunAway = false;
public joinedRound = false; public joinedRound = false;
/** /**
* The amount of times this Pokemon has acted again and used a move in the current turn.
* Used to make sure multi-hits occur properly when the user is * Used to make sure multi-hits occur properly when the user is
* forced to act again in the same turn * forced to act again in the same turn, and **must be incremented** by any effects that grant extra actions.
*/ */
public extraTurns = 0; public extraTurns = 0;
/** /**

View File

@ -2393,8 +2393,6 @@ export interface ModifierPool {
[tier: string]: WeightedModifierType[]; [tier: string]: WeightedModifierType[];
} }
const modifierPool: ModifierPool = {};
let modifierPoolThresholds = {}; let modifierPoolThresholds = {};
let ignoredPoolIndexes = {}; let ignoredPoolIndexes = {};
@ -2859,7 +2857,7 @@ function getNewModifierTypeOption(
} }
tier += upgradeCount; tier += upgradeCount;
while (tier && (!modifierPool.hasOwnProperty(tier) || !modifierPool[tier].length)) { while (tier && (!pool.hasOwnProperty(tier) || !pool[tier].length)) {
tier--; tier--;
if (upgradeCount) { if (upgradeCount) {
upgradeCount--; upgradeCount--;
@ -2870,7 +2868,7 @@ function getNewModifierTypeOption(
if (tier < ModifierTier.MASTER && allowLuckUpgrades) { if (tier < ModifierTier.MASTER && allowLuckUpgrades) {
const partyLuckValue = getPartyLuckValue(party); const partyLuckValue = getPartyLuckValue(party);
const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4)); const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4));
while (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[tier + upgradeCount + 1].length) { while (pool.hasOwnProperty(tier + upgradeCount + 1) && pool[tier + upgradeCount + 1].length) {
if (randSeedInt(upgradeOdds) < 4) { if (randSeedInt(upgradeOdds) < 4) {
upgradeCount++; upgradeCount++;
} else { } else {
@ -2920,6 +2918,7 @@ function getNewModifierTypeOption(
} }
export function getDefaultModifierTypeForTier(tier: ModifierTier): ModifierType { export function getDefaultModifierTypeForTier(tier: ModifierTier): ModifierType {
const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER);
let modifierType: ModifierType | WeightedModifierType = modifierPool[tier || ModifierTier.COMMON][0]; let modifierType: ModifierType | WeightedModifierType = modifierPool[tier || ModifierTier.COMMON][0];
if (modifierType instanceof WeightedModifierType) { if (modifierType instanceof WeightedModifierType) {
modifierType = (modifierType as WeightedModifierType).modifierType; modifierType = (modifierType as WeightedModifierType).modifierType;

View File

@ -285,17 +285,17 @@ export const defaultOverrides = new DefaultOverrides();
export default { export default {
...defaultOverrides, ...defaultOverrides,
...overrides ...overrides,
} satisfies InstanceType<typeof DefaultOverrides>; } satisfies InstanceType<typeof DefaultOverrides>;
export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles"; export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles";
export type RandomTrainerOverride = { export type RandomTrainerOverride = {
/** The Type of trainer to force */ /** The Type of trainer to force */
trainerType: Exclude<TrainerType, TrainerType.UNKNOWN>, trainerType: Exclude<TrainerType, TrainerType.UNKNOWN>;
/* If the selected trainer type has a double version, it will always use its double version. */ /* If the selected trainer type has a double version, it will always use its double version. */
alwaysDouble?: boolean alwaysDouble?: boolean;
} };
/** The type of the {@linkcode DefaultOverrides} class */ /** The type of the {@linkcode DefaultOverrides} class */
export type OverridesType = typeof DefaultOverrides; export type OverridesType = typeof DefaultOverrides;

View File

@ -397,7 +397,7 @@ export class PhaseManager {
* @returns the found phase or undefined if none found * @returns the found phase or undefined if none found
*/ */
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined { findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
return this.phaseQueue.find(phaseFilter) as P; return this.phaseQueue.find(phaseFilter) as P | undefined;
} }
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean { tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {

View File

@ -1,8 +1,5 @@
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
import { import { regenerateModifierPoolThresholds, getEnemyBuffModifierForWave } from "#app/modifier/modifier-type";
regenerateModifierPoolThresholds,
getEnemyBuffModifierForWave,
} from "#app/modifier/modifier-type";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
import { EnemyPersistentModifier } from "#app/modifier/modifier"; import { EnemyPersistentModifier } from "#app/modifier/modifier";
import { Phase } from "#app/phase"; import { Phase } from "#app/phase";

View File

@ -22,6 +22,7 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { isNullOrUndefined } from "#app/utils/common"; import { isNullOrUndefined } from "#app/utils/common";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#app/enums/arena-tag-type"; import { ArenaTagType } from "#app/enums/arena-tag-type";
import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode";
export class CommandPhase extends FieldPhase { export class CommandPhase extends FieldPhase {
public readonly phaseName = "CommandPhase"; public readonly phaseName = "CommandPhase";
@ -80,7 +81,7 @@ export class CommandPhase extends FieldPhase {
) { ) {
globalScene.currentBattle.turnCommands[this.fieldIndex] = { globalScene.currentBattle.turnCommands[this.fieldIndex] = {
command: Command.FIGHT, command: Command.FIGHT,
move: { move: MoveId.NONE, targets: [] }, move: { move: MoveId.NONE, targets: [], useMode: MoveUseMode.NORMAL },
skip: true, skip: true,
}; };
} }
@ -103,29 +104,31 @@ export class CommandPhase extends FieldPhase {
moveQueue.length && moveQueue.length &&
moveQueue[0] && moveQueue[0] &&
moveQueue[0].move && moveQueue[0].move &&
!moveQueue[0].virtual && !isVirtual(moveQueue[0].useMode) &&
(!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) || (!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) ||
!playerPokemon !playerPokemon
.getMoveset() .getMoveset()
[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable( [playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(
playerPokemon, playerPokemon,
moveQueue[0].ignorePP, isIgnorePP(moveQueue[0].useMode),
)) ))
) { ) {
moveQueue.shift(); moveQueue.shift();
} }
// TODO: Refactor this. I did a few simple find/replace matches but this is just ABHORRENTLY structured
if (moveQueue.length > 0) { if (moveQueue.length > 0) {
const queuedMove = moveQueue[0]; const queuedMove = moveQueue[0];
if (!queuedMove.move) { if (!queuedMove.move) {
this.handleCommand(Command.FIGHT, -1); this.handleCommand(Command.FIGHT, -1, MoveUseMode.NORMAL);
} else { } else {
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move); const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
if ( if (
(moveIndex > -1 && playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, queuedMove.ignorePP)) || (moveIndex > -1 &&
queuedMove.virtual playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))) ||
isVirtual(queuedMove.useMode)
) { ) {
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, queuedMove); this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove);
} else { } else {
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
} }
@ -143,18 +146,23 @@ export class CommandPhase extends FieldPhase {
} }
} }
/**
* TODO: Remove `args` and clean this thing up
* Code will need to be copied over from pkty except replacing the `virtual` and `ignorePP` args with a corresponding `MoveUseMode`.
*/
handleCommand(command: Command, cursor: number, ...args: any[]): boolean { handleCommand(command: Command, cursor: number, ...args: any[]): boolean {
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
let success = false; let success = false;
switch (command) { switch (command) {
// TODO: We don't need 2 args for this - moveUseMode is carried over from queuedMove
case Command.TERA: case Command.TERA:
case Command.FIGHT: case Command.FIGHT: {
let useStruggle = false; let useStruggle = false;
const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined; const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined;
if ( if (
cursor === -1 || cursor === -1 ||
playerPokemon.trySelectMove(cursor, args[0] as boolean) || playerPokemon.trySelectMove(cursor, isIgnorePP(args[0] as MoveUseMode)) ||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length) (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)
) { ) {
let moveId: MoveId; let moveId: MoveId;
@ -171,7 +179,7 @@ export class CommandPhase extends FieldPhase {
const turnCommand: TurnCommand = { const turnCommand: TurnCommand = {
command: Command.FIGHT, command: Command.FIGHT,
cursor: cursor, cursor: cursor,
move: { move: moveId, targets: [], ignorePP: args[0] }, move: { move: moveId, targets: [], useMode: args[0] },
args: args, args: args,
}; };
const preTurnCommand: TurnCommand = { const preTurnCommand: TurnCommand = {
@ -233,7 +241,8 @@ export class CommandPhase extends FieldPhase {
); );
} }
break; break;
case Command.BALL: }
case Command.BALL: {
const notInDex = const notInDex =
globalScene globalScene
.getEnemyField() .getEnemyField()
@ -337,8 +346,9 @@ export class CommandPhase extends FieldPhase {
} }
} }
break; break;
}
case Command.POKEMON: case Command.POKEMON:
case Command.RUN: case Command.RUN: {
const isSwitch = command === Command.POKEMON; const isSwitch = command === Command.POKEMON;
const { currentBattle, arena } = globalScene; const { currentBattle, arena } = globalScene;
const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed; const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed;
@ -445,6 +455,7 @@ export class CommandPhase extends FieldPhase {
} }
} }
break; break;
}
} }
if (success) { if (success) {

View File

@ -12,7 +12,6 @@ import { UiMode } from "#enums/ui-mode";
import i18next from "i18next"; import i18next from "i18next";
import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase"; import { PlayerPartyMemberPokemonPhase } from "#app/phases/player-party-member-pokemon-phase";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { ConfirmUiMode } from "#enums/confirm-ui-mode";
import { LearnMoveType } from "#enums/learn-move-type"; import { LearnMoveType } from "#enums/learn-move-type";
export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
@ -164,10 +163,6 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
globalScene.ui.setMode(this.messageMode); globalScene.ui.setMode(this.messageMode);
this.replaceMoveCheck(move, pokemon); this.replaceMoveCheck(move, pokemon);
}, },
false,
0,
0,
ConfirmUiMode.DEFAULT_NO,
); );
} }

View File

@ -8,10 +8,11 @@ import { MoveResult } from "#enums/move-result";
import { BooleanHolder } from "#app/utils/common"; import { BooleanHolder } from "#app/utils/common";
import { PokemonPhase } from "#app/phases/pokemon-phase"; import { PokemonPhase } from "#app/phases/pokemon-phase";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import type { MoveUseMode } from "#enums/move-use-mode";
import type { ChargingMove } from "#app/@types/move-types";
/** /**
* Phase for the "charging turn" of two-turn moves (e.g. Dig). * Phase for the "charging turn" of two-turn moves (e.g. Dig).
* @extends {@linkcode PokemonPhase}
*/ */
export class MoveChargePhase extends PokemonPhase { export class MoveChargePhase extends PokemonPhase {
public readonly phaseName = "MoveChargePhase"; public readonly phaseName = "MoveChargePhase";
@ -20,10 +21,21 @@ export class MoveChargePhase extends PokemonPhase {
/** The field index targeted by the move (Charging moves assume single target) */ /** The field index targeted by the move (Charging moves assume single target) */
public targetIndex: BattlerIndex; public targetIndex: BattlerIndex;
constructor(battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove) { /** The {@linkcode MoveUseMode} of the move that triggered the charge; passed on from move phase */
private useMode: MoveUseMode;
/**
* Create a new MoveChargePhase.
* @param battlerIndex - The {@linkcode BattlerIndex} of the user.
* @param targetIndex - The {@linkcode BattlerIndex} of the target.
* @param move - The {@linkcode PokemonMove} being used
* @param useMode - The move's {@linkcode MoveUseMode}
*/
constructor(battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove, useMode: MoveUseMode) {
super(battlerIndex); super(battlerIndex);
this.move = move; this.move = move;
this.targetIndex = targetIndex; this.targetIndex = targetIndex;
this.useMode = useMode;
} }
public override start() { public override start() {
@ -37,7 +49,8 @@ export class MoveChargePhase extends PokemonPhase {
// immediately end this phase. // immediately end this phase.
if (!target || !move.isChargingMove()) { if (!target || !move.isChargingMove()) {
console.warn("Invalid parameters for MoveChargePhase"); console.warn("Invalid parameters for MoveChargePhase");
return super.end(); super.end();
return;
} }
new MoveChargeAnim(move.chargeAnim, move.id, user).play(false, () => { new MoveChargeAnim(move.chargeAnim, move.id, user).play(false, () => {
@ -52,29 +65,30 @@ export class MoveChargePhase extends PokemonPhase {
/** Checks the move's instant charge conditions, then ends this phase. */ /** Checks the move's instant charge conditions, then ends this phase. */
public override end() { public override end() {
const user = this.getUserPokemon(); const user = this.getUserPokemon();
const move = this.move.getMove(); // Checked for `ChargingMove` in `this.start()`
const move = this.move.getMove() as ChargingMove;
if (move.isChargingMove()) { const instantCharge = new BooleanHolder(false);
const instantCharge = new BooleanHolder(false); applyMoveChargeAttrs("InstantChargeAttr", user, null, move, instantCharge);
applyMoveChargeAttrs("InstantChargeAttr", user, null, move, instantCharge); // If instantly charging, remove the pending MoveEndPhase and queue a new MovePhase for the "attack" portion of the move.
// Otherwise, add the attack portion to the user's move queue to execute next turn.
if (instantCharge.value) { // TODO: This checks status twice for a single-turn usage...
// this MoveEndPhase will be duplicated by the queued MovePhase if not removed if (instantCharge.value) {
globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user); globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user);
// queue a new MovePhase for this move's attack phase globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, this.useMode);
globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, false); } else {
} else { user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useMode: this.useMode });
user.getMoveQueue().push({ move: move.id, targets: [this.targetIndex] });
}
// Add this move's charging phase to the user's move history
user.pushMoveHistory({
move: this.move.moveId,
targets: [this.targetIndex],
result: MoveResult.OTHER,
});
} }
// Add this move's charging phase to the user's move history
user.pushMoveHistory({
move: this.move.moveId,
targets: [this.targetIndex],
result: MoveResult.OTHER,
useMode: this.useMode,
});
super.end(); super.end();
} }

View File

@ -54,20 +54,25 @@ import { HitCheckResult } from "#enums/hit-check-result";
import type Move from "#app/data/moves/move"; import type Move from "#app/data/moves/move";
import { isFieldTargeted } from "#app/data/moves/move-utils"; import { isFieldTargeted } from "#app/data/moves/move-utils";
import { DamageAchv } from "#app/system/achv"; import { DamageAchv } from "#app/system/achv";
import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode";
type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier]; export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
export class MoveEffectPhase extends PokemonPhase { export class MoveEffectPhase extends PokemonPhase {
public readonly phaseName = "MoveEffectPhase"; public readonly phaseName = "MoveEffectPhase";
public move: Move; public move: Move;
private virtual = false;
protected targets: BattlerIndex[]; protected targets: BattlerIndex[];
protected reflected = false; protected useMode: MoveUseMode;
/** The result of the hit check against each target */ /** The result of the hit check against each target */
private hitChecks: HitCheckEntry[]; private hitChecks: HitCheckEntry[];
/** The move history entry for the move */ /**
* Log to be entered into the user's move history once the move result is resolved.
* Note that `result` logs whether the move was successfully
* used in the sense of "Does it have an effect on the user?".
*/
private moveHistoryEntry: TurnMove; private moveHistoryEntry: TurnMove;
/** Is this the first strike of a move? */ /** Is this the first strike of a move? */
@ -75,19 +80,20 @@ export class MoveEffectPhase extends PokemonPhase {
/** Is this the last strike of a move? */ /** Is this the last strike of a move? */
private lastHit: boolean; private lastHit: boolean;
/** Phases queued during moves */ /**
* Phases queued during moves; used to add a new MovePhase for reflected moves after triggering.
* TODO: Remove this and move the reflection logic to ability-side
*/
private queuedPhases: Phase[] = []; private queuedPhases: Phase[] = [];
/** /**
* @param reflected Indicates that the move was reflected by the user due to magic coat or magic bounce * @param useMode - The {@linkcode MoveUseMode} corresponding to how this move was used.
* @param virtual Indicates that the move is a virtual move (i.e. called by metronome)
*/ */
constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: Move, reflected = false, virtual = false) { constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: Move, useMode: MoveUseMode) {
super(battlerIndex); super(battlerIndex);
this.move = move; this.move = move;
this.virtual = virtual; this.useMode = useMode;
this.reflected = reflected;
/** /**
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies * In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
* with no party members available to switch in, then the right Pokemon takes the index * with no party members available to switch in, then the right Pokemon takes the index
@ -158,7 +164,7 @@ export class MoveEffectPhase extends PokemonPhase {
* Queue the phaes that should occur when the target reflects the move back to the user * Queue the phaes that should occur when the target reflects the move back to the user
* @param user - The {@linkcode Pokemon} using this phase's invoked move * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - The {@linkcode Pokemon} that is reflecting the move * @param target - The {@linkcode Pokemon} that is reflecting the move
* * TODO: Rework this to use `onApply` of Magic Coat
*/ */
private queueReflectedMove(user: Pokemon, target: Pokemon): void { private queueReflectedMove(user: Pokemon, target: Pokemon): void {
const newTargets = this.move.isMultiTarget() const newTargets = this.move.isMultiTarget()
@ -181,10 +187,8 @@ export class MoveEffectPhase extends PokemonPhase {
"MovePhase", "MovePhase",
target, target,
newTargets, newTargets,
new PokemonMove(this.move.id, 0, 0, true), new PokemonMove(this.move.id),
true, MoveUseMode.REFLECTED,
true,
true,
), ),
); );
} }
@ -278,8 +282,18 @@ export class MoveEffectPhase extends PokemonPhase {
const overridden = new BooleanHolder(false); const overridden = new BooleanHolder(false);
const move = this.move; const move = this.move;
// Assume single target for override // Apply effects to override a move effect.
applyMoveAttrs("OverrideMoveEffectAttr", user, this.getFirstTarget() ?? null, move, overridden, this.virtual); // Assuming single target here works as this is (currently)
// only used for Future Sight, calling and Pledge moves.
// TODO: change if any other move effect overrides are introduced
applyMoveAttrs(
"OverrideMoveEffectAttr",
user,
this.getFirstTarget() ?? null,
move,
overridden,
isVirtual(this.useMode),
);
// If other effects were overriden, stop this phase before they can be applied // If other effects were overriden, stop this phase before they can be applied
if (overridden.value) { if (overridden.value) {
@ -290,8 +304,8 @@ export class MoveEffectPhase extends PokemonPhase {
// Lapse `MOVE_EFFECT` effects (i.e. semi-invulnerability) when applicable // Lapse `MOVE_EFFECT` effects (i.e. semi-invulnerability) when applicable
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
// If the user is acting again (such as due to Instruct), reset hitsLeft/hitCount so that // If the user is acting again (such as due to Instruct or Dancer), reset hitsLeft/hitCount and
// the move executes correctly (ensures all hits of a multi-hit are properly calculated) // recalculate hit count for multi-hit moves.
if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) { if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) {
user.turnData.hitsLeft = -1; user.turnData.hitsLeft = -1;
user.turnData.hitCount = 0; user.turnData.hitCount = 0;
@ -316,16 +330,11 @@ export class MoveEffectPhase extends PokemonPhase {
user.turnData.hitsLeft = hitCount.value; user.turnData.hitsLeft = hitCount.value;
} }
/*
* Log to be entered into the user's move history once the move result is resolved.
* Note that `result` logs whether the move was successfully
* used in the sense of "Does it have an effect on the user?".
*/
this.moveHistoryEntry = { this.moveHistoryEntry = {
move: this.move.id, move: this.move.id,
targets: this.targets, targets: this.targets,
result: MoveResult.PENDING, result: MoveResult.PENDING,
virtual: this.virtual, useMode: this.useMode,
}; };
const fieldMove = isFieldTargeted(move); const fieldMove = isFieldTargeted(move);
@ -390,29 +399,35 @@ export class MoveEffectPhase extends PokemonPhase {
public override end(): void { public override end(): void {
const user = this.getUserPokemon(); const user = this.getUserPokemon();
/** if (!user) {
* If this phase isn't for the invoked move's last strike, super.end();
* unshift another MoveEffectPhase for the next strike. return;
* Otherwise, queue a message indicating the number of times the move has struck
* (if the move has struck more than once), then apply the heal from Shell Bell
* to the user.
*/
if (user) {
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
globalScene.phaseManager.unshiftPhase(this.getNewHitPhase());
} else {
// Queue message for number of hits made by multi-move
// If multi-hit attack only hits once, still want to render a message
const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0);
if (hitsTotal > 1 || (user.turnData.hitsLeft && user.turnData.hitsLeft > 0)) {
// If there are multiple hits, or if there are hits of the multi-hit move left
globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
}
globalScene.applyModifiers(HitHealModifier, this.player, user);
this.getTargets().forEach(target => (target.turnData.moveEffectiveness = null));
}
} }
/**
* If this phase isn't for the invoked move's last strike (and we still have something to hit),
* unshift another MoveEffectPhase for the next strike before ending this phase.
*/
if (--user.turnData.hitsLeft >= 1 && this.getFirstTarget()) {
this.addNextHitPhase();
super.end();
return;
}
/**
* All hits of the move have resolved by now.
* Queue message for multi-strike moves before applying Shell Bell heals & proccing Dancer-like effects.
*/
const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0);
if (hitsTotal > 1 || user.turnData.hitsLeft > 0) {
// Queue message if multiple hits occurred or were slated to occur (such as a Triple Axel miss)
globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
}
globalScene.applyModifiers(HitHealModifier, this.player, user);
this.getTargets().forEach(target => {
target.turnData.moveEffectiveness = null;
});
super.end(); super.end();
} }
@ -422,7 +437,6 @@ export class MoveEffectPhase extends PokemonPhase {
* @param user - The {@linkcode Pokemon} using this phase's invoked move * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move * @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param hitResult - The {@linkcode HitResult} of the attempted move * @param hitResult - The {@linkcode HitResult} of the attempted move
* @returns a `Promise` intended to be passed into a `then()` call.
*/ */
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void { protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void {
applyPostDefendAbAttrs("PostDefendAbAttr", target, user, this.move, hitResult); applyPostDefendAbAttrs("PostDefendAbAttr", target, user, this.move, hitResult);
@ -434,7 +448,6 @@ export class MoveEffectPhase extends PokemonPhase {
* @param user - The {@linkcode Pokemon} using this phase's invoked move * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move * @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param dealsDamage - `true` if the attempted move successfully dealt damage * @param dealsDamage - `true` if the attempted move successfully dealt damage
* @returns a function intended to be passed into a `then()` call.
*/ */
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean): void { protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean): void {
if (this.move.hasAttr("FlinchAttr")) { if (this.move.hasAttr("FlinchAttr")) {
@ -458,8 +471,9 @@ export class MoveEffectPhase extends PokemonPhase {
* @param user - The {@linkcode Pokemon} using this phase's invoked move * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the target to check for protection * @param target - {@linkcode Pokemon} the target to check for protection
* @param move - The {@linkcode Move} being used * @param move - The {@linkcode Move} being used
* @returns Whether the pokemon was protected
*/ */
private protectedCheck(user: Pokemon, target: Pokemon) { private protectedCheck(user: Pokemon, target: Pokemon): boolean {
/** The {@linkcode ArenaTagSide} to which the target belongs */ /** The {@linkcode ArenaTagSide} to which the target belongs */
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
@ -480,14 +494,15 @@ export class MoveEffectPhase extends PokemonPhase {
); );
} }
// TODO: Break up this chunky boolean to make it more palatable
return ( return (
![MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.move.moveTarget) && ![MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.move.moveTarget) &&
(bypassIgnoreProtect.value || !this.move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target })) && (bypassIgnoreProtect.value || !this.move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target })) &&
(hasConditionalProtectApplied.value || (hasConditionalProtectApplied.value ||
(!target.findTags(t => t instanceof DamageProtectedTag).length && (!target.findTags(t => t instanceof DamageProtectedTag).length &&
target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) || target.findTags(t => t instanceof ProtectedTag).some(t => target.lapseTag(t.tagType))) ||
(this.move.category !== MoveCategory.STATUS && (this.move.category !== MoveCategory.STATUS &&
target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))) target.findTags(t => t instanceof DamageProtectedTag).some(t => target.lapseTag(t.tagType))))
); );
} }
@ -547,7 +562,8 @@ export class MoveEffectPhase extends PokemonPhase {
return [HitCheckResult.PROTECTED, 0]; return [HitCheckResult.PROTECTED, 0];
} }
if (!this.reflected && move.doesFlagEffectApply({ flag: MoveFlags.REFLECTABLE, user, target })) { // Reflected moves cannot be reflected again
if (!isReflected(this.useMode) && move.doesFlagEffectApply({ flag: MoveFlags.REFLECTABLE, user, target })) {
return [HitCheckResult.REFLECTED, 0]; return [HitCheckResult.REFLECTED, 0];
} }
@ -660,12 +676,17 @@ export class MoveEffectPhase extends PokemonPhase {
return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex]; return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex];
} }
/** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */ /**
* @returns An array of {@linkcode Pokemon} that are:
* - On-field and active
* - Non-fainted
* - Targeted by this phase's invoked move
*/
public getTargets(): Pokemon[] { public getTargets(): Pokemon[] {
return globalScene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); return globalScene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
} }
/** @returns The first target of this phase's invoked move */ /** @returns The first active, non-fainted target of this phase's invoked move. */
public getFirstTarget(): Pokemon | undefined { public getFirstTarget(): Pokemon | undefined {
return this.getTargets()[0]; return this.getTargets()[0];
} }
@ -705,9 +726,12 @@ export class MoveEffectPhase extends PokemonPhase {
} }
} }
/** @returns A new `MoveEffectPhase` with the same properties as this phase */ /**
protected getNewHitPhase(): MoveEffectPhase { * Unshifts a new `MoveEffectPhase` with the same properties as this phase.
return new MoveEffectPhase(this.battlerIndex, this.targets, this.move, this.reflected, this.virtual); * Used to queue the next hit of multi-strike moves.
*/
protected addNextHitPhase(): void {
globalScene.phaseManager.unshiftNew("MoveEffectPhase", this.battlerIndex, this.targets, this.move, this.useMode);
} }
/** Removes all substitutes that were broken by this phase's invoked move */ /** Removes all substitutes that were broken by this phase's invoked move */
@ -729,7 +753,6 @@ export class MoveEffectPhase extends PokemonPhase {
* @param firstTarget Whether the target is the first to be hit by the current strike * @param firstTarget Whether the target is the first to be hit by the current strike
* @param selfTarget If defined, limits the effects triggered to either self-targeted * @param selfTarget If defined, limits the effects triggered to either self-targeted
* effects (if set to `true`) or targeted effects (if set to `false`). * effects (if set to `true`) or targeted effects (if set to `false`).
* @returns a `Promise` applying the relevant move effects.
*/ */
protected triggerMoveEffects( protected triggerMoveEffects(
triggerType: MoveEffectTrigger, triggerType: MoveEffectTrigger,
@ -775,6 +798,7 @@ export class MoveEffectPhase extends PokemonPhase {
const hitResult = this.applyMove(user, target, effectiveness); const hitResult = this.applyMove(user, target, effectiveness);
// Apply effects to the user (always) and the target (if not blocked by substitute).
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true); this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true);
if (!this.move.hitsSubstitute(user, target)) { if (!this.move.hitsSubstitute(user, target)) {
this.applyOnTargetEffects(user, target, hitResult, firstTarget); this.applyOnTargetEffects(user, target, hitResult, firstTarget);

View File

@ -25,9 +25,9 @@ export class MoveEndPhase extends PokemonPhase {
if (!this.wasFollowUp && pokemon?.isActive(true)) { if (!this.wasFollowUp && pokemon?.isActive(true)) {
pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
} }
globalScene.arena.setIgnoreAbilities(false);
// Remove effects which were set on a Pokemon which removes them on summon (i.e. via Mold Breaker) // Remove effects which were set on a Pokemon which removes them on summon (i.e. via Mold Breaker)
globalScene.arena.setIgnoreAbilities(false);
for (const target of this.targets) { for (const target of this.targets) {
if (target) { if (target) {
applyPostSummonAbAttrs("PostSummonRemoveEffectAbAttr", target); applyPostSummonAbAttrs("PostSummonRemoveEffectAbAttr", target);

View File

@ -5,7 +5,6 @@ import type { DelayedAttackTag } from "#app/data/arena-tag";
import { CommonAnim } from "#enums/move-anims-common"; import { CommonAnim } from "#enums/move-anims-common";
import { CenterOfAttentionTag } from "#app/data/battler-tags"; import { CenterOfAttentionTag } from "#app/data/battler-tags";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import type { HealStatusEffectAttr } from "#app/data/moves/move";
import { applyMoveAttrs } from "#app/data/moves/apply-attrs"; import { applyMoveAttrs } from "#app/data/moves/apply-attrs";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { MoveFlags } from "#enums/MoveFlags"; import { MoveFlags } from "#enums/MoveFlags";
@ -20,13 +19,14 @@ import { MoveResult } from "#enums/move-result";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { BattlePhase } from "#app/phases/battle-phase"; import { BattlePhase } from "#app/phases/battle-phase";
import { NumberHolder } from "#app/utils/common"; import { enumValueToKey, NumberHolder } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode";
import { frenzyMissFunc } from "#app/data/moves/move-utils"; import { frenzyMissFunc } from "#app/data/moves/move-utils";
export class MovePhase extends BattlePhase { export class MovePhase extends BattlePhase {
@ -34,17 +34,19 @@ export class MovePhase extends BattlePhase {
protected _pokemon: Pokemon; protected _pokemon: Pokemon;
protected _move: PokemonMove; protected _move: PokemonMove;
protected _targets: BattlerIndex[]; protected _targets: BattlerIndex[];
protected followUp: boolean; public readonly useMode: MoveUseMode; // Made public for quash
protected ignorePp: boolean;
protected forcedLast: boolean; protected forcedLast: boolean;
/** Whether the current move should fail but still use PP */
protected failed = false; protected failed = false;
/** Whether the current move should cancel and retain PP */
protected cancelled = false; protected cancelled = false;
protected reflected = false;
public get pokemon(): Pokemon { public get pokemon(): Pokemon {
return this._pokemon; return this._pokemon;
} }
// TODO: Do we need public getters but only protected setters?
protected set pokemon(pokemon: Pokemon) { protected set pokemon(pokemon: Pokemon) {
this._pokemon = pokemon; this._pokemon = pokemon;
} }
@ -66,51 +68,42 @@ export class MovePhase extends BattlePhase {
} }
/** /**
* @param followUp Indicates that the move being used is a "follow-up" - for example, a move being used by Metronome or Dancer. * Create a new MovePhase for using moves.
* Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc. * @param pokemon - The {@linkcode Pokemon} using the move
* @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce. * @param move - The {@linkcode PokemonMove} to use
* Reflected moves cannot be reflected again and will not trigger Dancer. * @param useMode - The {@linkcode MoveUseMode} corresponding to this move's means of execution (usually `MoveUseMode.NORMAL`).
* Not marked optional to ensure callers correctly pass on `useModes`.
* @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false`
*/ */
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) {
constructor(
pokemon: Pokemon,
targets: BattlerIndex[],
move: PokemonMove,
followUp = false,
ignorePp = false,
reflected = false,
forcedLast = false,
) {
super(); super();
this.pokemon = pokemon; this.pokemon = pokemon;
this.targets = targets; this.targets = targets;
this.move = move; this.move = move;
this.followUp = followUp; this.useMode = useMode;
this.ignorePp = ignorePp;
this.reflected = reflected;
this.forcedLast = forcedLast; this.forcedLast = forcedLast;
} }
/** /**
* Checks if the pokemon is active, if the move is usable, and that the move is targetting something. * Checks if the pokemon is active, if the move is usable, and that the move is targeting something.
* @param ignoreDisableTags `true` to not check if the move is disabled * @param ignoreDisableTags `true` to not check if the move is disabled
* @returns `true` if all the checks pass * @returns `true` if all the checks pass
*/ */
public canMove(ignoreDisableTags = false): boolean { public canMove(ignoreDisableTags = false): boolean {
return ( return (
this.pokemon.isActive(true) && this.pokemon.isActive(true) &&
this.move.isUsable(this.pokemon, this.ignorePp, ignoreDisableTags) && this.move.isUsable(this.pokemon, isIgnorePP(this.useMode), ignoreDisableTags) &&
!!this.targets.length this.targets.length > 0
); );
} }
/**Signifies the current move should fail but still use PP */ /** Signifies the current move should fail but still use PP */
public fail(): void { public fail(): void {
this.failed = true; this.failed = true;
} }
/**Signifies the current move should cancel and retain PP */ /** Signifies the current move should cancel and retain PP */
public cancel(): void { public cancel(): void {
this.cancelled = true; this.cancelled = true;
} }
@ -118,7 +111,7 @@ export class MovePhase extends BattlePhase {
/** /**
* Shows whether the current move has been forced to the end of the turn * Shows whether the current move has been forced to the end of the turn
* Needed for speed order, see {@linkcode MoveId.QUASH} * Needed for speed order, see {@linkcode MoveId.QUASH}
* */ */
public isForcedLast(): boolean { public isForcedLast(): boolean {
return this.forcedLast; return this.forcedLast;
} }
@ -126,35 +119,37 @@ export class MovePhase extends BattlePhase {
public start(): void { public start(): void {
super.start(); super.start();
console.log(MoveId[this.move.moveId]); console.log(MoveId[this.move.moveId], enumValueToKey(MoveUseMode, this.useMode));
// Check if move is unusable (e.g. because it's out of PP due to a mid-turn Spite). // Check if move is unusable (e.g. running out of PP due to a mid-turn Spite
// or the user no longer being on field), ending the phase early if not.
if (!this.canMove(true)) { if (!this.canMove(true)) {
if (this.pokemon.isActive(true)) { if (this.pokemon.isActive(true)) {
this.fail(); this.fail();
this.showMoveText(); this.showMoveText();
this.showFailedText(); this.showFailedText();
} }
return this.end(); this.end();
return;
} }
this.pokemon.turnData.acted = true; this.pokemon.turnData.acted = true;
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats) // Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
if (this.followUp) { if (isVirtual(this.useMode)) {
this.pokemon.turnData.hitsLeft = -1; this.pokemon.turnData.hitsLeft = -1;
this.pokemon.turnData.hitCount = 0; this.pokemon.turnData.hitCount = 0;
} }
// Check move to see if arena.ignoreAbilities should be true. // Check move to see if arena.ignoreAbilities should be true.
if (!this.followUp || this.reflected) { if (
if ( this.move.getMove().doesFlagEffectApply({
this.move flag: MoveFlags.IGNORE_ABILITIES,
.getMove() user: this.pokemon,
.doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user: this.pokemon, isFollowUp: this.followUp }) isFollowUp: isVirtual(this.useMode), // Sunsteel strike and co. don't work when called indirectly
) { })
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); ) {
} globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
} }
this.resolveRedirectTarget(); this.resolveRedirectTarget();
@ -187,7 +182,7 @@ export class MovePhase extends BattlePhase {
if ( if (
(targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) || (targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) ||
(moveQueue.length && moveQueue[0].move === MoveId.NONE) (moveQueue.length > 0 && moveQueue[0].move === MoveId.NONE)
) { ) {
this.showMoveText(); this.showMoveText();
this.showFailedText(); this.showFailedText();
@ -200,83 +195,98 @@ export class MovePhase extends BattlePhase {
} }
/** /**
* Handles {@link StatusEffect.SLEEP Sleep}/{@link StatusEffect.PARALYSIS Paralysis}/{@link StatusEffect.FREEZE Freeze} rolls and side effects. * Handles {@link StatusEffect.SLEEP | Sleep}/{@link StatusEffect.PARALYSIS | Paralysis}/{@link StatusEffect.FREEZE | Freeze} rolls and side effects.
*/ */
protected resolvePreMoveStatusEffects(): void { protected resolvePreMoveStatusEffects(): void {
if (!this.followUp && this.pokemon.status && !this.pokemon.status.isPostTurn()) { // Skip for follow ups/reflected moves, no status condition or post turn statuses (e.g. Poison/Toxic)
this.pokemon.status.incrementTurn(); if (!this.pokemon.status?.effect || this.pokemon.status.isPostTurn() || isIgnoreStatus(this.useMode)) {
let activated = false; return;
let healed = false; }
switch (this.pokemon.status.effect) { if (
case StatusEffect.PARALYSIS: this.useMode === MoveUseMode.INDIRECT &&
activated = [StatusEffect.SLEEP, StatusEffect.FREEZE].includes(this.pokemon.status.effect)
(!this.pokemon.randBattleSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && ) {
Overrides.STATUS_ACTIVATION_OVERRIDE !== false; // Dancer thaws out or wakes up a frozen/sleeping user prior to use
break; this.pokemon.resetStatus(false);
case StatusEffect.SLEEP: { return;
applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove()); }
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
applyAbAttrs(
"ReduceStatusEffectDurationAbAttr",
this.pokemon,
null,
false,
this.pokemon.status.effect,
turnsRemaining,
);
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
break;
}
case StatusEffect.FREEZE:
healed =
!!this.move
.getMove()
.findAttr(
attr =>
attr.is("HealStatusEffectAttr") &&
attr.selfTarget &&
(attr as unknown as HealStatusEffectAttr).isOfEffect(StatusEffect.FREEZE),
) ||
(!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) ||
Overrides.STATUS_ACTIVATION_OVERRIDE === false;
activated = !healed; this.pokemon.status.incrementTurn();
break;
/** Whether to prevent us from using the move */
let activated = false;
/** Whether to cure the status */
let healed = false;
switch (this.pokemon.status.effect) {
case StatusEffect.PARALYSIS:
activated =
(this.pokemon.randBattleSeedInt(4) === 0 || Overrides.STATUS_ACTIVATION_OVERRIDE === true) &&
Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
break;
case StatusEffect.SLEEP: {
applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove());
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
applyAbAttrs(
"ReduceStatusEffectDurationAbAttr",
this.pokemon,
null,
false,
this.pokemon.status.effect,
turnsRemaining,
);
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
break;
} }
case StatusEffect.FREEZE:
healed =
!!this.move
.getMove()
.findAttr(
attr => attr.is("HealStatusEffectAttr") && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE),
) ||
(!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) ||
Overrides.STATUS_ACTIVATION_OVERRIDE === false;
if (activated) { activated = !healed;
this.cancel(); break;
globalScene.phaseManager.queueMessage( }
getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
); if (activated) {
globalScene.phaseManager.unshiftNew( // Cancel move activation and play effect
"CommonAnimPhase", this.cancel();
this.pokemon.getBattlerIndex(), globalScene.phaseManager.queueMessage(
undefined, getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
CommonAnim.POISON + (this.pokemon.status.effect - 1), );
); globalScene.phaseManager.unshiftNew(
} else if (healed) { "CommonAnimPhase",
globalScene.phaseManager.queueMessage( this.pokemon.getBattlerIndex(),
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)), undefined,
); CommonAnim.POISON + (this.pokemon.status.effect - 1), // offset anim # by effect #
this.pokemon.resetStatus(); );
this.pokemon.updateInfo(); } else if (healed) {
} // cure status and play effect
globalScene.phaseManager.queueMessage(
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
);
this.pokemon.resetStatus();
this.pokemon.updateInfo();
} }
} }
/** /**
* Lapse {@linkcode BattlerTagLapseType.PRE_MOVE PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed. * Lapse {@linkcode BattlerTagLapseType.PRE_MOVE | PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed.
* Also lapse {@linkcode BattlerTagLapseType.MOVE MOVE} tags if the move should be successful. * Also lapse {@linkcode BattlerTagLapseType.MOVE | MOVE} tags if the move is successful and not called indirectly.
*/ */
protected lapsePreMoveAndMoveTags(): void { protected lapsePreMoveAndMoveTags(): void {
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE); this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
// TODO: does this intentionally happen before the no targets/MoveId.NONE on queue cancellation case is checked? // TODO: does this intentionally happen before the no targets/MoveId.NONE on queue cancellation case is checked?
if (!this.followUp && this.canMove() && !this.cancelled) { // (In other words, check if truant can proc on a move w/o targets)
if (!isIgnoreStatus(this.useMode) && this.canMove() && !this.cancelled) {
this.pokemon.lapseTags(BattlerTagLapseType.MOVE); this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
} }
} }
@ -284,11 +294,12 @@ export class MovePhase extends BattlePhase {
protected useMove(): void { protected useMove(): void {
const targets = this.getActiveTargetPokemon(); const targets = this.getActiveTargetPokemon();
const moveQueue = this.pokemon.getMoveQueue(); const moveQueue = this.pokemon.getMoveQueue();
const move = this.move.getMove();
// form changes happen even before we know that the move wll execute. // form changes happen even before we know that the move wll execute.
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger); globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
const isDelayedAttack = this.move.getMove().hasAttr("DelayedAttackAttr"); const isDelayedAttack = move.hasAttr("DelayedAttackAttr");
if (isDelayedAttack) { if (isDelayedAttack) {
// Check the player side arena if future sight is active // Check the player side arena if future sight is active
const futureSightTags = globalScene.arena.findTags(t => t.tagType === ArenaTagType.FUTURE_SIGHT); const futureSightTags = globalScene.arena.findTags(t => t.tagType === ArenaTagType.FUTURE_SIGHT);
@ -310,7 +321,8 @@ export class MovePhase extends BattlePhase {
if (fail) { if (fail) {
this.showMoveText(); this.showMoveText();
this.showFailedText(); this.showFailedText();
return this.end(); this.end();
return;
} }
} }
@ -327,21 +339,21 @@ export class MovePhase extends BattlePhase {
this.showMoveText(); this.showMoveText();
} }
if (moveQueue.length > 0) { // Clear out any two turn moves once they've been used.
// Using .shift here clears out two turn moves once they've been used // TODO: Refactor move queues and remove this assignment;
this.ignorePp = moveQueue.shift()?.ignorePP ?? false; // Move queues should be handled by the calling `CommandPhase` or a manager for it
} // @ts-expect-error - useMode is readonly and shouldn't normally be assigned to
this.useMode = moveQueue.shift()?.useMode ?? this.useMode;
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) { if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
this.pokemon.lapseTag(BattlerTagType.CHARGING); this.pokemon.lapseTag(BattlerTagType.CHARGING);
} }
// "commit" to using the move, deducting PP. if (!isIgnorePP(this.useMode)) {
if (!this.ignorePp) { // "commit" to using the move, deducting PP.
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
this.move.usePp(ppUsed); this.move.usePp(ppUsed);
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed)); globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, move, this.move.ppUsed));
} }
/** /**
@ -355,8 +367,6 @@ export class MovePhase extends BattlePhase {
* TODO: These steps are straightforward, but the implementation below is extremely convoluted. * TODO: These steps are straightforward, but the implementation below is extremely convoluted.
*/ */
const move = this.move.getMove();
/** /**
* Move conditions assume the move has a single target * Move conditions assume the move has a single target
* TODO: is this sustainable? * TODO: is this sustainable?
@ -392,8 +402,7 @@ export class MovePhase extends BattlePhase {
this.pokemon.getBattlerIndex(), this.pokemon.getBattlerIndex(),
this.targets, this.targets,
move, move,
this.reflected, this.useMode,
this.move.virtual,
); );
} else { } else {
if ([MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)) { if ([MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)) {
@ -404,7 +413,7 @@ export class MovePhase extends BattlePhase {
move: this.move.moveId, move: this.move.moveId,
targets: this.targets, targets: this.targets,
result: MoveResult.FAIL, result: MoveResult.FAIL,
virtual: this.move.virtual, useMode: this.useMode,
}); });
const failureMessage = move.getFailedText(this.pokemon, targets[0], move); const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
@ -424,8 +433,10 @@ export class MovePhase extends BattlePhase {
} }
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`). // Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
// Note that the `!this.followUp` check here prevents an infinite Dancer loop. // Note the MoveUseMode check here prevents an infinite Dancer loop.
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !this.followUp) { const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const;
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
// TODO: Fix in dancer PR to move to MEP for hit checks
globalScene.getField(true).forEach(pokemon => { globalScene.getField(true).forEach(pokemon => {
applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets); applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets);
}); });
@ -437,23 +448,16 @@ export class MovePhase extends BattlePhase {
const move = this.move.getMove(); const move = this.move.getMove();
const targets = this.getActiveTargetPokemon(); const targets = this.getActiveTargetPokemon();
if (move.applyConditions(this.pokemon, targets[0], move)) { this.showMoveText();
// Protean and Libero apply on the charging turn of charge moves
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
this.showMoveText(); // Conditions currently assume single target
globalScene.phaseManager.unshiftNew( // TODO: Is this sustainable?
"MoveChargePhase", if (!move.applyConditions(this.pokemon, targets[0], move)) {
this.pokemon.getBattlerIndex(),
this.targets[0],
this.move,
);
} else {
this.pokemon.pushMoveHistory({ this.pokemon.pushMoveHistory({
move: this.move.moveId, move: this.move.moveId,
targets: this.targets, targets: this.targets,
result: MoveResult.FAIL, result: MoveResult.FAIL,
virtual: this.move.virtual, useMode: this.useMode,
}); });
const failureMessage = move.getFailedText(this.pokemon, targets[0], move); const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
@ -462,7 +466,19 @@ export class MovePhase extends BattlePhase {
// Remove the user from its semi-invulnerable state (if applicable) // Remove the user from its semi-invulnerable state (if applicable)
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
return;
} }
// Protean and Libero apply on the charging turn of charge moves
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
globalScene.phaseManager.unshiftNew(
"MoveChargePhase",
this.pokemon.getBattlerIndex(),
this.targets[0],
this.move,
this.useMode,
);
} }
/** /**
@ -473,7 +489,7 @@ export class MovePhase extends BattlePhase {
"MoveEndPhase", "MoveEndPhase",
this.pokemon.getBattlerIndex(), this.pokemon.getBattlerIndex(),
this.getActiveTargetPokemon(), this.getActiveTargetPokemon(),
this.followUp, isVirtual(this.useMode),
); );
super.end(); super.end();
@ -605,7 +621,7 @@ export class MovePhase extends BattlePhase {
protected handlePreMoveFailures(): void { protected handlePreMoveFailures(): void {
if (this.cancelled || this.failed) { if (this.cancelled || this.failed) {
if (this.failed) { if (this.failed) {
const ppUsed = this.ignorePp ? 0 : 1; const ppUsed = isIgnorePP(this.useMode) ? 0 : 1;
if (ppUsed) { if (ppUsed) {
this.move.usePp(); this.move.usePp();
@ -622,6 +638,7 @@ export class MovePhase extends BattlePhase {
move: MoveId.NONE, move: MoveId.NONE,
result: MoveResult.FAIL, result: MoveResult.FAIL,
targets: this.targets, targets: this.targets,
useMode: this.useMode,
}); });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
@ -645,7 +662,7 @@ export class MovePhase extends BattlePhase {
} }
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t(this.reflected ? "battle:magicCoatActivated" : "battle:useMove", { i18next.t(isReflected(this.useMode) ? "battle:magicCoatActivated" : "battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName(), moveName: this.move.getName(),
}), }),

View File

@ -53,7 +53,8 @@ export class PokemonAnimPhase extends BattlePhase {
private doSubstituteAddAnim(): void { private doSubstituteAddAnim(): void {
const substitute = this.pokemon.getTag(SubstituteTag); const substitute = this.pokemon.getTag(SubstituteTag);
if (isNullOrUndefined(substitute)) { if (isNullOrUndefined(substitute)) {
return this.end(); this.end();
return;
} }
const getSprite = () => { const getSprite = () => {
@ -116,12 +117,14 @@ export class PokemonAnimPhase extends BattlePhase {
private doSubstitutePreMoveAnim(): void { private doSubstitutePreMoveAnim(): void {
if (this.fieldAssets.length !== 1) { if (this.fieldAssets.length !== 1) {
return this.end(); this.end();
return;
} }
const subSprite = this.fieldAssets[0]; const subSprite = this.fieldAssets[0];
if (subSprite === undefined) { if (subSprite === undefined) {
return this.end(); this.end();
return;
} }
globalScene.tweens.add({ globalScene.tweens.add({
@ -145,12 +148,14 @@ export class PokemonAnimPhase extends BattlePhase {
private doSubstitutePostMoveAnim(): void { private doSubstitutePostMoveAnim(): void {
if (this.fieldAssets.length !== 1) { if (this.fieldAssets.length !== 1) {
return this.end(); this.end();
return;
} }
const subSprite = this.fieldAssets[0]; const subSprite = this.fieldAssets[0];
if (subSprite === undefined) { if (subSprite === undefined) {
return this.end(); this.end();
return;
} }
globalScene.tweens.add({ globalScene.tweens.add({
@ -174,12 +179,14 @@ export class PokemonAnimPhase extends BattlePhase {
private doSubstituteRemoveAnim(): void { private doSubstituteRemoveAnim(): void {
if (this.fieldAssets.length !== 1) { if (this.fieldAssets.length !== 1) {
return this.end(); this.end();
return;
} }
const subSprite = this.fieldAssets[0]; const subSprite = this.fieldAssets[0];
if (subSprite === undefined) { if (subSprite === undefined) {
return this.end(); this.end();
return;
} }
const getSprite = () => { const getSprite = () => {
@ -244,12 +251,14 @@ export class PokemonAnimPhase extends BattlePhase {
private doCommanderApplyAnim(): void { private doCommanderApplyAnim(): void {
if (!globalScene.currentBattle?.double) { if (!globalScene.currentBattle?.double) {
return this.end(); this.end();
return;
} }
const dondozo = this.pokemon.getAlly(); const dondozo = this.pokemon.getAlly();
if (dondozo?.species?.speciesId !== SpeciesId.DONDOZO) { if (dondozo?.species?.speciesId !== SpeciesId.DONDOZO) {
return this.end(); this.end();
return;
} }
const tatsugiriX = this.pokemon.x + this.pokemon.getSprite().x; const tatsugiriX = this.pokemon.x + this.pokemon.getSprite().x;
@ -329,7 +338,8 @@ export class PokemonAnimPhase extends BattlePhase {
const tatsugiri = this.pokemon.getAlly(); const tatsugiri = this.pokemon.getAlly();
if (isNullOrUndefined(tatsugiri)) { if (isNullOrUndefined(tatsugiri)) {
console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined"); console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined");
return this.end(); this.end();
return;
} }
const tatsuSprite = globalScene.addPokemonSprite( const tatsuSprite = globalScene.addPokemonSprite(

View File

@ -4,6 +4,10 @@ import type Pokemon from "#app/field/pokemon";
import { FieldPhase } from "./field-phase"; import { FieldPhase } from "./field-phase";
export abstract class PokemonPhase extends FieldPhase { export abstract class PokemonPhase extends FieldPhase {
/**
* The battler index this phase refers to, or the pokemon ID if greater than 3.
* TODO: Make this either use IDs or `BattlerIndex`es, not a weird mix of both
*/
protected battlerIndex: BattlerIndex | number; protected battlerIndex: BattlerIndex | number;
public player: boolean; public player: boolean;
public fieldIndex: number; public fieldIndex: number;
@ -15,10 +19,12 @@ export abstract class PokemonPhase extends FieldPhase {
battlerIndex ?? battlerIndex ??
globalScene globalScene
.getField() .getField()
.find(p => p?.isActive())! // TODO: is the bang correct here? .find(p => p?.isActive())
.getBattlerIndex(); ?.getBattlerIndex();
if (battlerIndex === undefined) { if (battlerIndex === undefined) {
console.warn("There are no Pokemon on the field!"); // TODO: figure out a suitable fallback behavior // TODO: figure out a suitable fallback behavior
console.warn("There are no Pokemon on the field!");
battlerIndex = BattlerIndex.PLAYER;
} }
this.battlerIndex = battlerIndex; this.battlerIndex = battlerIndex;

View File

@ -29,7 +29,8 @@ export class PokemonTransformPhase extends PokemonPhase {
const target = globalScene.getField(true).find(p => p.getBattlerIndex() === this.targetIndex); const target = globalScene.getField(true).find(p => p.getBattlerIndex() === this.targetIndex);
if (!target) { if (!target) {
return this.end(); this.end();
return;
} }
user.summonData.speciesForm = target.getSpeciesForm(); user.summonData.speciesForm = target.getSpeciesForm();
@ -52,7 +53,7 @@ export class PokemonTransformPhase extends PokemonPhase {
user.summonData.moveset = target.getMoveset().map(m => { user.summonData.moveset = target.getMoveset().map(m => {
if (m) { if (m) {
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5. // If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5)); return new PokemonMove(m.moveId, 0, 0, Math.min(m.getMove().pp, 5));
} }
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`); console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
return new PokemonMove(MoveId.NONE); return new PokemonMove(MoveId.NONE);

View File

@ -29,7 +29,8 @@ export class QuietFormChangePhase extends BattlePhase {
super.start(); super.start();
if (this.pokemon.formIndex === this.pokemon.species.forms.findIndex(f => f.formKey === this.formChange.formKey)) { if (this.pokemon.formIndex === this.pokemon.species.forms.findIndex(f => f.formKey === this.formChange.formKey)) {
return this.end(); this.end();
return;
} }
const preName = getPokemonNameWithAffix(this.pokemon); const preName = getPokemonNameWithAffix(this.pokemon);

View File

@ -65,7 +65,7 @@ export class TurnStartPhase extends FieldPhase {
// This occurs before the main loop because of battles with more than two Pokemon // This occurs before the main loop because of battles with more than two Pokemon
const battlerBypassSpeed = {}; const battlerBypassSpeed = {};
globalScene.getField(true).map(p => { globalScene.getField(true).forEach(p => {
const bypassSpeed = new BooleanHolder(false); const bypassSpeed = new BooleanHolder(false);
const canCheckHeldItems = new BooleanHolder(true); const canCheckHeldItems = new BooleanHolder(true);
applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed); applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed);
@ -126,6 +126,8 @@ export class TurnStartPhase extends FieldPhase {
return moveOrder; return moveOrder;
} }
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
// Also need a clearer distinction between "turn command" and queued moves
start() { start() {
super.start(); super.start();
@ -156,8 +158,9 @@ export class TurnStartPhase extends FieldPhase {
return; return;
} }
// TODO: This logic is questionable and needs to be redone, // TODO: Remove `turnData.order` -
// especially given the fact that `order` is used for exactly 1 thing... // it is used exclusively for Fusion Flare/Bolt
// and uses a really jank implementation
if (turnCommand.command === Command.FIGHT) { if (turnCommand.command === Command.FIGHT) {
pokemon.turnData.order = index; pokemon.turnData.order = index;
} }
@ -245,7 +248,7 @@ export class TurnStartPhase extends FieldPhase {
turnCommand.targets ?? turnCommand.move.targets, turnCommand.targets ?? turnCommand.move.targets,
move, move,
false, false,
queuedMove.ignorePP, queuedMove.useMode,
); );
} }
} }

View File

@ -174,7 +174,24 @@ export async function initI18n(): Promise<void> {
"es-MX": ["es-ES", "en"], "es-MX": ["es-ES", "en"],
default: ["en"], default: ["en"],
}, },
supportedLngs: ["en", "es-ES", "es-MX", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca", "da", "tr", "ro", "ru"], supportedLngs: [
"en",
"es-ES",
"es-MX",
"fr",
"it",
"de",
"zh-CN",
"zh-TW",
"pt-BR",
"ko",
"ja",
"ca",
"da",
"tr",
"ro",
"ru",
],
backend: { backend: {
loadPath(lng: string, [ns]: string[]) { loadPath(lng: string, [ns]: string[]) {
let fileName: string; let fileName: string;

View File

@ -32,7 +32,10 @@ const pressAction = i18next.t("settings:pressActionToAssign");
export const settingGamepadOptions = { export const settingGamepadOptions = {
[SettingGamepad.Controller]: [i18next.t("settings:controllerDefault"), i18next.t("settings:controllerChange")], [SettingGamepad.Controller]: [i18next.t("settings:controllerDefault"), i18next.t("settings:controllerChange")],
[SettingGamepad.Gamepad_Support]: [i18next.t("settings:gamepadSupportAuto"), i18next.t("settings:gamepadSupportDisabled")], [SettingGamepad.Gamepad_Support]: [
i18next.t("settings:gamepadSupportAuto"),
i18next.t("settings:gamepadSupportDisabled"),
],
[SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction], [SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction],
[SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction], [SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction],
[SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction], [SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction],

View File

@ -959,7 +959,7 @@ export function setSetting(setting: string, value: number): boolean {
}, },
{ {
label: "Türkçe (Needs Help)", label: "Türkçe (Needs Help)",
handler: () => changeLocaleHandler("tr") handler: () => changeLocaleHandler("tr"),
}, },
{ {
label: "Русский (Needs Help)", label: "Русский (Needs Help)",
@ -967,11 +967,11 @@ export function setSetting(setting: string, value: number): boolean {
}, },
{ {
label: "Dansk (Needs Help)", label: "Dansk (Needs Help)",
handler: () => changeLocaleHandler("da") handler: () => changeLocaleHandler("da"),
}, },
{ {
label: "Română (Needs Help)", label: "Română (Needs Help)",
handler: () => changeLocaleHandler("ro") handler: () => changeLocaleHandler("ro"),
}, },
{ {
label: i18next.t("settings:back"), label: i18next.t("settings:back"),

View File

@ -63,6 +63,10 @@ import * as v1_8_3 from "./versions/v1_8_3";
// biome-ignore lint/style/noNamespaceImport: Convenience // biome-ignore lint/style/noNamespaceImport: Convenience
import * as v1_9_0 from "./versions/v1_9_0"; import * as v1_9_0 from "./versions/v1_9_0";
// --- v1.10.0 PATCHES --- //
// biome-ignore lint/style/noNamespaceImport: Convenience
import * as v1_10_0 from "./versions/v1_10_0";
/** Current game version */ /** Current game version */
const LATEST_VERSION = version; const LATEST_VERSION = version;
@ -85,6 +89,7 @@ const sessionMigrators: SessionSaveMigrator[] = [];
sessionMigrators.push(...v1_0_4.sessionMigrators); sessionMigrators.push(...v1_0_4.sessionMigrators);
sessionMigrators.push(...v1_7_0.sessionMigrators); sessionMigrators.push(...v1_7_0.sessionMigrators);
sessionMigrators.push(...v1_9_0.sessionMigrators); sessionMigrators.push(...v1_9_0.sessionMigrators);
sessionMigrators.push(...v1_10_0.sessionMigrators);
/** All settings migrators */ /** All settings migrators */
const settingsMigrators: SettingsSaveMigrator[] = []; const settingsMigrators: SettingsSaveMigrator[] = [];

View File

@ -0,0 +1,48 @@
import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator";
import type { BattlerIndex } from "#enums/battler-index";
import type { TurnMove } from "#app/field/pokemon";
import type { MoveResult } from "#enums/move-result";
import type { SessionSaveData } from "#app/system/game-data";
import { MoveUseMode } from "#enums/move-use-mode";
import type { MoveId } from "#enums/move-id";
/** Prior signature of `TurnMove`; used to ensure parity */
interface OldTurnMove {
move: MoveId;
targets: BattlerIndex[];
result?: MoveResult;
turn?: number;
virtual?: boolean;
ignorePP?: boolean;
}
/**
* Fix player pokemon move history entries with updated `MoveUseModes`,
* based on the prior values of `virtual` and `ignorePP`.
* Needed to ensure Last Resort and move-calling moves still work OK.
* @param data - {@linkcode SystemSaveData}
*/
const fixMoveHistory: SessionSaveMigrator = {
version: "1.10.0",
migrate: (data: SessionSaveData): void => {
const mapTurnMove = (tm: OldTurnMove): TurnMove => ({
move: tm.move,
targets: tm.targets,
result: tm.result,
turn: tm.turn,
// NOTE: This unfortuately has to mis-classify Dancer and Magic Bounce-induced moves as `FOLLOW_UP`,
// given we previously had _no way_ of distinguishing them from follow-up moves post hoc.
useMode: tm.virtual ? MoveUseMode.FOLLOW_UP : tm.ignorePP ? MoveUseMode.IGNORE_PP : MoveUseMode.NORMAL,
});
data.party.forEach(pkmn => {
pkmn.summonData.moveHistory = (pkmn.summonData.moveHistory as OldTurnMove[]).map(mapTurnMove);
pkmn.summonData.moveQueue = (pkmn.summonData.moveQueue as OldTurnMove[]).map(mapTurnMove);
});
data.enemyParty.forEach(pkmn => {
pkmn.summonData.moveHistory = (pkmn.summonData.moveHistory as OldTurnMove[]).map(mapTurnMove);
pkmn.summonData.moveQueue = (pkmn.summonData.moveQueue as OldTurnMove[]).map(mapTurnMove);
});
},
};
export const sessionMigrators: Readonly<SessionSaveMigrator[]> = [fixMoveHistory] as const;

View File

@ -56,10 +56,6 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
protected defaultTextStyle: TextStyle = TextStyle.WINDOW; protected defaultTextStyle: TextStyle = TextStyle.WINDOW;
protected textContent: string; protected textContent: string;
constructor(mode: UiMode | null) {
super(mode);
}
abstract getWindowWidth(): number; abstract getWindowWidth(): number;
getWindowHeight(): number { getWindowHeight(): number {

View File

@ -69,7 +69,7 @@ export default class AdminUiHandler extends FormModalUiHandler {
case AdminMode.SEARCH: case AdminMode.SEARCH:
inputFieldConfigs.push({ label: "Username" }); inputFieldConfigs.push({ label: "Username" });
break; break;
case AdminMode.ADMIN: case AdminMode.ADMIN: {
const adminResult = this.adminResult ?? { const adminResult = this.adminResult ?? {
username: "", username: "",
discordId: "", discordId: "",
@ -90,6 +90,7 @@ export default class AdminUiHandler extends FormModalUiHandler {
inputFieldConfigs.push({ label: "Last played", isReadOnly: true }); inputFieldConfigs.push({ label: "Last played", isReadOnly: true });
inputFieldConfigs.push({ label: "Registered", isReadOnly: true }); inputFieldConfigs.push({ label: "Registered", isReadOnly: true });
break; break;
}
} }
return inputFieldConfigs; return inputFieldConfigs;
} }

View File

@ -4,11 +4,8 @@ import { UiMode } from "#enums/ui-mode";
import i18next from "i18next"; import i18next from "i18next";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { ConfirmUiMode } from "#enums/confirm-ui-mode";
export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler { export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler {
private confirmUiMode: ConfirmUiMode;
public static readonly windowWidth: number = 48; public static readonly windowWidth: number = 48;
private switchCheck: boolean; private switchCheck: boolean;
@ -108,16 +105,7 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler {
this.optionSelectContainer.setPosition(globalScene.game.canvas.width / 6 - 1 + xOffset, -48 + yOffset); this.optionSelectContainer.setPosition(globalScene.game.canvas.width / 6 - 1 + xOffset, -48 + yOffset);
this.confirmUiMode = args.length >= 6 ? (args[5] as ConfirmUiMode) : ConfirmUiMode.DEFAULT_YES; this.setCursor(this.switchCheck ? this.switchCheckCursor : 0);
switch (this.confirmUiMode) {
case ConfirmUiMode.DEFAULT_YES:
this.setCursor(this.switchCheck ? this.switchCheckCursor : 0);
break;
case ConfirmUiMode.DEFAULT_NO:
this.setCursor(this.switchCheck ? this.switchCheckCursor : 1);
break;
}
return true; return true;
} }

View File

@ -169,12 +169,13 @@ export class DailyRunScoreboard extends Phaser.GameObjects.Container {
entryContainer.add(scoreLabel); entryContainer.add(scoreLabel);
switch (this.category) { switch (this.category) {
case ScoreboardCategory.DAILY: case ScoreboardCategory.DAILY: {
const waveLabel = addTextObject(68, 0, wave, TextStyle.WINDOW, { const waveLabel = addTextObject(68, 0, wave, TextStyle.WINDOW, {
fontSize: "54px", fontSize: "54px",
}); });
entryContainer.add(waveLabel); entryContainer.add(waveLabel);
break; break;
}
case ScoreboardCategory.WEEKLY: case ScoreboardCategory.WEEKLY:
scoreLabel.x -= 16; scoreLabel.x -= 16;
break; break;

View File

@ -131,7 +131,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
gachaInfoContainer.add(gachaUpLabel); gachaInfoContainer.add(gachaUpLabel);
switch (gachaType as GachaType) { switch (gachaType as GachaType) {
case GachaType.LEGENDARY: case GachaType.LEGENDARY: {
if (["de", "es-ES"].includes(currentLanguage)) { if (["de", "es-ES"].includes(currentLanguage)) {
gachaUpLabel.setAlign("center"); gachaUpLabel.setAlign("center");
gachaUpLabel.setY(0); gachaUpLabel.setY(0);
@ -152,6 +152,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
gachaInfoContainer.add(pokemonIcon); gachaInfoContainer.add(pokemonIcon);
break; break;
}
case GachaType.MOVE: case GachaType.MOVE:
if (["de", "es-ES", "fr", "pt-BR", "ru"].includes(currentLanguage)) { if (["de", "es-ES", "fr", "pt-BR", "ru"].includes(currentLanguage)) {
gachaUpLabel.setAlign("center"); gachaUpLabel.setAlign("center");
@ -623,11 +624,12 @@ export default class EggGachaUiHandler extends MessageUiHandler {
updateGachaInfo(gachaType: GachaType): void { updateGachaInfo(gachaType: GachaType): void {
const infoContainer = this.gachaInfoContainers[gachaType]; const infoContainer = this.gachaInfoContainers[gachaType];
switch (gachaType as GachaType) { switch (gachaType as GachaType) {
case GachaType.LEGENDARY: case GachaType.LEGENDARY: {
const species = getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(new Date().getTime())); const species = getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(new Date().getTime()));
const pokemonIcon = infoContainer.getAt(1) as Phaser.GameObjects.Sprite; const pokemonIcon = infoContainer.getAt(1) as Phaser.GameObjects.Sprite;
pokemonIcon.setTexture(species.getIconAtlasKey(), species.getIconId(false)); pokemonIcon.setTexture(species.getIconAtlasKey(), species.getIconId(false));
break; break;
}
} }
} }

View File

@ -16,6 +16,7 @@ import type Pokemon from "#app/field/pokemon";
import type { CommandPhase } from "#app/phases/command-phase"; import type { CommandPhase } from "#app/phases/command-phase";
import MoveInfoOverlay from "./move-info-overlay"; import MoveInfoOverlay from "./move-info-overlay";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { MoveUseMode } from "#enums/move-use-mode";
export default class FightUiHandler extends UiHandler implements InfoToggle { export default class FightUiHandler extends UiHandler implements InfoToggle {
public static readonly MOVES_CONTAINER_NAME = "moves"; public static readonly MOVES_CONTAINER_NAME = "moves";
@ -139,53 +140,63 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
return true; return true;
} }
/**
* Process the player inputting the selected {@linkcode Button}.
* @param button - The {@linkcode Button} being pressed
* @returns Whether the input was successful (ie did anything).
*/
processInput(button: Button): boolean { processInput(button: Button): boolean {
const ui = this.getUi(); const ui = this.getUi();
const cursor = this.getCursor();
let success = false; let success = false;
const cursor = this.getCursor(); switch (button) {
case Button.CANCEL:
if (button === Button.CANCEL || button === Button.ACTION) { {
if (button === Button.ACTION) { // Attempts to back out of the move selection pane are blocked in certain MEs
// TODO: Should we allow showing the summary menu at least?
const { battleType, mysteryEncounter } = globalScene.currentBattle;
if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) {
ui.setMode(UiMode.COMMAND, this.fieldIndex);
success = true;
}
}
break;
case Button.ACTION:
if ( if (
(globalScene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(this.fromCommand, cursor, false) (globalScene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(
this.fromCommand,
cursor,
MoveUseMode.NORMAL,
)
) { ) {
success = true; success = true;
} else { } else {
ui.playError(); ui.playError();
} }
} else { break;
// Cannot back out of fight menu if skipToFightInput is enabled case Button.UP:
const { battleType, mysteryEncounter } = globalScene.currentBattle; if (cursor >= 2) {
if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) { success = this.setCursor(cursor - 2);
ui.setMode(UiMode.COMMAND, this.fieldIndex);
success = true;
} }
} break;
} else { case Button.DOWN:
switch (button) { if (cursor < 2) {
case Button.UP: success = this.setCursor(cursor + 2);
if (cursor >= 2) { }
success = this.setCursor(cursor - 2); break;
} case Button.LEFT:
break; if (cursor % 2 === 1) {
case Button.DOWN: success = this.setCursor(cursor - 1);
if (cursor < 2) { }
success = this.setCursor(cursor + 2); break;
} case Button.RIGHT:
break; if (cursor % 2 === 0) {
case Button.LEFT: success = this.setCursor(cursor + 1);
if (cursor % 2 === 1) { }
success = this.setCursor(cursor - 1); break;
} default:
break; // other inputs do nothing while in fight menu
case Button.RIGHT:
if (cursor % 2 === 0) {
success = this.setCursor(cursor + 1);
}
break;
}
} }
if (success) { if (success) {

View File

@ -686,7 +686,7 @@ export default class MenuUiHandler extends MessageUiHandler {
error = true; error = true;
} }
break; break;
case MenuOptions.LOG_OUT: case MenuOptions.LOG_OUT: {
success = true; success = true;
const doLogout = () => { const doLogout = () => {
ui.setMode(UiMode.LOADING, { ui.setMode(UiMode.LOADING, {
@ -718,6 +718,7 @@ export default class MenuUiHandler extends MessageUiHandler {
doLogout(); doLogout();
} }
break; break;
}
} }
} else if (button === Button.CANCEL) { } else if (button === Button.CANCEL) {
success = true; success = true;

View File

@ -1,4 +1,4 @@
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon, TurnMove } from "#app/field/pokemon";
import type { PokemonMove } from "#app/data/moves/pokemon-move"; import type { PokemonMove } from "#app/data/moves/pokemon-move";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";
@ -1167,13 +1167,13 @@ export default class PartyUiHandler extends MessageUiHandler {
} }
// TODO: add FORCED_SWITCH (and perhaps also BATON_PASS_SWITCH) to the modes // TODO: add FORCED_SWITCH (and perhaps also BATON_PASS_SWITCH) to the modes
// TODO: refactor once moves in flight become a thing...
private isBatonPassMove(): boolean { private isBatonPassMove(): boolean {
const moveHistory = globalScene.getPlayerField()[this.fieldIndex].getMoveHistory(); const lastMove: TurnMove | undefined = globalScene.getPlayerField()[this.fieldIndex].getLastXMoves()[0];
return !!( return (
this.partyUiMode === PartyUiMode.FAINT_SWITCH && this.partyUiMode === PartyUiMode.FAINT_SWITCH &&
moveHistory.length && lastMove?.result === MoveResult.SUCCESS &&
allMoves[moveHistory[moveHistory.length - 1].move].getAttrs("ForceSwitchOutAttr")[0]?.isBatonPass() && allMoves[lastMove.move].getAttrs("ForceSwitchOutAttr")[0]?.isBatonPass()
moveHistory[moveHistory.length - 1].result === MoveResult.SUCCESS
); );
} }
@ -1385,7 +1385,7 @@ export default class PartyUiHandler extends MessageUiHandler {
case PartyOption.MOVE_1: case PartyOption.MOVE_1:
case PartyOption.MOVE_2: case PartyOption.MOVE_2:
case PartyOption.MOVE_3: case PartyOption.MOVE_3:
case PartyOption.MOVE_4: case PartyOption.MOVE_4: {
const move = pokemon.moveset[option - PartyOption.MOVE_1]; const move = pokemon.moveset[option - PartyOption.MOVE_1];
if (this.showMovePp) { if (this.showMovePp) {
const maxPP = move.getMovePp(); const maxPP = move.getMovePp();
@ -1395,7 +1395,8 @@ export default class PartyUiHandler extends MessageUiHandler {
optionName = move.getName(); optionName = move.getName();
} }
break; break;
default: }
default: {
const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon); const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon);
if (formChangeItemModifiers && option >= PartyOption.FORM_CHANGE_ITEM) { if (formChangeItemModifiers && option >= PartyOption.FORM_CHANGE_ITEM) {
const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM]; const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM];
@ -1410,6 +1411,7 @@ export default class PartyUiHandler extends MessageUiHandler {
} }
} }
break; break;
}
} }
} else if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) { } else if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) {
const learnableLevelMoves = pokemon.getLearnableLevelMoves(); const learnableLevelMoves = pokemon.getLearnableLevelMoves();

View File

@ -567,7 +567,7 @@ export default class RunInfoUiHandler extends UiHandler {
case GameModes.SPLICED_ENDLESS: case GameModes.SPLICED_ENDLESS:
modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false); modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false);
break; break;
case GameModes.CHALLENGE: case GameModes.CHALLENGE: {
modeText.appendText(`${i18next.t("gameMode:challenge")}`, false); modeText.appendText(`${i18next.t("gameMode:challenge")}`, false);
modeText.appendText(`${i18next.t("runHistory:challengeRules")}: `); modeText.appendText(`${i18next.t("runHistory:challengeRules")}: `);
modeText.setWrapMode(1); // wrap by word modeText.setWrapMode(1); // wrap by word
@ -582,6 +582,7 @@ export default class RunInfoUiHandler extends UiHandler {
} }
} }
break; break;
}
case GameModes.ENDLESS: case GameModes.ENDLESS:
modeText.appendText(`${i18next.t("gameMode:endless")}`, false); modeText.appendText(`${i18next.t("gameMode:endless")}`, false);
break; break;
@ -687,7 +688,7 @@ export default class RunInfoUiHandler extends UiHandler {
case Challenges.SINGLE_GENERATION: case Challenges.SINGLE_GENERATION:
rules.push(i18next.t(`runHistory:challengeMonoGen${this.runInfo.challenges[i].value}`)); rules.push(i18next.t(`runHistory:challengeMonoGen${this.runInfo.challenges[i].value}`));
break; break;
case Challenges.SINGLE_TYPE: case Challenges.SINGLE_TYPE: {
const typeRule = PokemonType[this.runInfo.challenges[i].value - 1]; const typeRule = PokemonType[this.runInfo.challenges[i].value - 1];
const typeTextColor = `[color=${TypeColor[typeRule]}]`; const typeTextColor = `[color=${TypeColor[typeRule]}]`;
const typeShadowColor = `[shadow=${TypeShadow[typeRule]}]`; const typeShadowColor = `[shadow=${TypeShadow[typeRule]}]`;
@ -695,16 +696,18 @@ export default class RunInfoUiHandler extends UiHandler {
typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)! + "[/color]" + "[/shadow]"; typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)! + "[/color]" + "[/shadow]";
rules.push(typeText); rules.push(typeText);
break; break;
}
case Challenges.INVERSE_BATTLE: case Challenges.INVERSE_BATTLE:
rules.push(i18next.t("challenges:inverseBattle.shortName")); rules.push(i18next.t("challenges:inverseBattle.shortName"));
break; break;
default: default: {
const localisationKey = Challenges[this.runInfo.challenges[i].id] const localisationKey = Challenges[this.runInfo.challenges[i].id]
.split("_") .split("_")
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
.join(""); .join("");
rules.push(i18next.t(`challenges:${localisationKey}.name`)); rules.push(i18next.t(`challenges:${localisationKey}.name`));
break; break;
}
} }
} }
} }

View File

@ -126,6 +126,11 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
); );
this.actionsBg.setOrigin(0, 0); this.actionsBg.setOrigin(0, 0);
/*
* If there isn't enough space to fit all the icons and texts, there will be an overlap
* This currently doesn't happen, but it's something to keep in mind.
*/
const iconAction = globalScene.add.sprite(0, 0, "keyboard"); const iconAction = globalScene.add.sprite(0, 0, "keyboard");
iconAction.setOrigin(0, -0.1); iconAction.setOrigin(0, -0.1);
iconAction.setPositionRelative(this.actionsBg, this.navigationContainer.width - 32, 4); iconAction.setPositionRelative(this.actionsBg, this.navigationContainer.width - 32, 4);
@ -137,7 +142,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
const iconCancel = globalScene.add.sprite(0, 0, "keyboard"); const iconCancel = globalScene.add.sprite(0, 0, "keyboard");
iconCancel.setOrigin(0, -0.1); iconCancel.setOrigin(0, -0.1);
iconCancel.setPositionRelative(this.actionsBg, this.navigationContainer.width - 100, 4); iconCancel.setPositionRelative(this.actionsBg, actionText.x - 28, 4);
this.navigationIcons["BUTTON_CANCEL"] = iconCancel; this.navigationIcons["BUTTON_CANCEL"] = iconCancel;
const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL); const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL);
@ -146,7 +151,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
const iconReset = globalScene.add.sprite(0, 0, "keyboard"); const iconReset = globalScene.add.sprite(0, 0, "keyboard");
iconReset.setOrigin(0, -0.1); iconReset.setOrigin(0, -0.1);
iconReset.setPositionRelative(this.actionsBg, this.navigationContainer.width - 180, 4); iconReset.setPositionRelative(this.actionsBg, cancelText.x - 28, 4);
this.navigationIcons["BUTTON_HOME"] = iconReset; this.navigationIcons["BUTTON_HOME"] = iconReset;
const resetText = addTextObject(0, 0, i18next.t("settings:reset"), TextStyle.SETTINGS_LABEL); const resetText = addTextObject(0, 0, i18next.t("settings:reset"), TextStyle.SETTINGS_LABEL);

View File

@ -94,7 +94,7 @@ export default class AbstractSettingsUiHandler extends MessageUiHandler {
const iconCancel = globalScene.add.sprite(0, 0, "keyboard"); const iconCancel = globalScene.add.sprite(0, 0, "keyboard");
iconCancel.setOrigin(0, -0.1); iconCancel.setOrigin(0, -0.1);
iconCancel.setPositionRelative(actionsBg, this.navigationContainer.width - 100, 4); iconCancel.setPositionRelative(actionsBg, actionText.x - 28, 4);
this.navigationIcons["BUTTON_CANCEL"] = iconCancel; this.navigationIcons["BUTTON_CANCEL"] = iconCancel;
const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL); const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL);
@ -332,12 +332,13 @@ export default class AbstractSettingsUiHandler extends MessageUiHandler {
case Button.CYCLE_SHINY: case Button.CYCLE_SHINY:
success = this.navigationContainer.navigate(button); success = this.navigationContainer.navigate(button);
break; break;
case Button.ACTION: case Button.ACTION: {
const setting: Setting = this.settings[cursor]; const setting: Setting = this.settings[cursor];
if (setting?.activatable) { if (setting?.activatable) {
success = this.activateSetting(setting); success = this.activateSetting(setting);
} }
break; break;
}
} }
} }

View File

@ -1763,7 +1763,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
} }
} else if (this.randomCursorObj.visible) { } else if (this.randomCursorObj.visible) {
switch (button) { switch (button) {
case Button.ACTION: case Button.ACTION: {
if (this.starterSpecies.length >= 6) { if (this.starterSpecies.length >= 6) {
error = true; error = true;
break; break;
@ -1815,6 +1815,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
} }
}); });
break; break;
}
case Button.UP: case Button.UP:
this.randomCursorObj.setVisible(false); this.randomCursorObj.setVisible(false);
this.filterBarCursor = this.filterBar.numFilters - 1; this.filterBarCursor = this.filterBar.numFilters - 1;

View File

@ -10,10 +10,6 @@ import { UiMode } from "#enums/ui-mode";
export default class TestDialogueUiHandler extends FormModalUiHandler { export default class TestDialogueUiHandler extends FormModalUiHandler {
keys: string[]; keys: string[];
constructor(mode) {
super(mode);
}
setup() { setup() {
super.setup(); super.setup();

View File

@ -620,3 +620,25 @@ export function coerceArray<T>(input: T): T extends any[] ? T : [T];
export function coerceArray<T>(input: T): T | [T] { export function coerceArray<T>(input: T): T | [T] {
return Array.isArray(input) ? input : [input]; return Array.isArray(input) ? input : [input];
} }
/**
* Returns the name of the key that matches the enum [object] value.
* @param input - The enum [object] to check
* @param val - The value to get the key of
* @returns The name of the key with the specified value
* @example
* const thing = {
* one: 1,
* two: 2,
* } as const;
* console.log(enumValueToKey(thing, thing.two)); // output: "two"
* @throws An `Error` if an invalid enum value is passed to the function
*/
export function enumValueToKey<T extends Record<string, string | number>>(input: T, val: T[keyof T]): keyof T {
for (const [key, value] of Object.entries(input)) {
if (val === value) {
return key as keyof T;
}
}
throw new Error(`Invalid value passed to \`enumValueToKey\`! Value: ${val}`);
}

View File

@ -50,5 +50,5 @@ describe("Ability Timing", () => {
await game.phaseInterceptor.to("MessagePhase"); await game.phaseInterceptor.to("MessagePhase");
expect(i18next.t).toHaveBeenCalledWith("battle:statFell", expect.objectContaining({ count: 1 })); expect(i18next.t).toHaveBeenCalledWith("battle:statFell", expect.objectContaining({ count: 1 }));
}, 5000); });
}); });

View File

@ -26,11 +26,12 @@ describe("Abilities - Battery", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("double"); game.override
game.override.enemySpecies(SpeciesId.SHUCKLE); .battleStyle("double")
game.override.enemyAbility(AbilityId.BALL_FETCH); .enemySpecies(SpeciesId.SHUCKLE)
game.override.moveset([MoveId.TACKLE, MoveId.BREAKING_SWIPE, MoveId.SPLASH, MoveId.DAZZLING_GLEAM]); .enemyAbility(AbilityId.BALL_FETCH)
game.override.enemyMoveset(MoveId.SPLASH); .moveset([MoveId.TACKLE, MoveId.BREAKING_SWIPE, MoveId.SPLASH, MoveId.DAZZLING_GLEAM])
.enemyMoveset(MoveId.SPLASH);
}); });
it("raises the power of allies' special moves by 30%", async () => { it("raises the power of allies' special moves by 30%", async () => {

View File

@ -47,7 +47,7 @@ describe("Abilities - Beast Boost", () => {
await game.phaseInterceptor.to("VictoryPhase"); await game.phaseInterceptor.to("VictoryPhase");
expect(playerPokemon.getStatStage(Stat.DEF)).toBe(1); expect(playerPokemon.getStatStage(Stat.DEF)).toBe(1);
}, 20000); });
it("should use in-battle overriden stats when determining the stat stage to raise by 1", async () => { it("should use in-battle overriden stats when determining the stat stage to raise by 1", async () => {
game.override.enemyMoveset([MoveId.GUARD_SPLIT]); game.override.enemyMoveset([MoveId.GUARD_SPLIT]);
@ -66,7 +66,7 @@ describe("Abilities - Beast Boost", () => {
await game.phaseInterceptor.to("VictoryPhase"); await game.phaseInterceptor.to("VictoryPhase");
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
}, 20000); });
it("should have order preference in case of stat ties", async () => { it("should have order preference in case of stat ties", async () => {
// Order preference follows the order of EFFECTIVE_STAT // Order preference follows the order of EFFECTIVE_STAT
@ -84,5 +84,5 @@ describe("Abilities - Beast Boost", () => {
await game.phaseInterceptor.to("VictoryPhase"); await game.phaseInterceptor.to("VictoryPhase");
expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1);
}, 20000); });
}); });

View File

@ -36,7 +36,7 @@ describe("Abilities - Contrary", () => {
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000); });
describe("With Clear Body", () => { describe("With Clear Body", () => {
it("should apply positive effects", async () => { it("should apply positive effects", async () => {

View File

@ -24,10 +24,11 @@ describe("Abilities - COSTAR", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("double"); game.override
game.override.ability(AbilityId.COSTAR); .battleStyle("double")
game.override.moveset([MoveId.SPLASH, MoveId.NASTY_PLOT]); .ability(AbilityId.COSTAR)
game.override.enemyMoveset(MoveId.SPLASH); .moveset([MoveId.SPLASH, MoveId.NASTY_PLOT])
.enemyMoveset(MoveId.SPLASH);
}); });
test("ability copies positive stat stages", async () => { test("ability copies positive stat stages", async () => {

View File

@ -66,8 +66,7 @@ describe("Abilities - Disguise", () => {
}); });
it("takes no damage from the first hit of a multihit move and transforms to Busted form, then takes damage from the second hit", async () => { it("takes no damage from the first hit of a multihit move and transforms to Busted form, then takes damage from the second hit", async () => {
game.override.moveset([MoveId.SURGING_STRIKES]); game.override.moveset([MoveId.SURGING_STRIKES]).enemyLevel(5);
game.override.enemyLevel(5);
await game.classicMode.startBattle(); await game.classicMode.startBattle();
const mimikyu = game.scene.getEnemyPokemon()!; const mimikyu = game.scene.getEnemyPokemon()!;
@ -106,8 +105,7 @@ describe("Abilities - Disguise", () => {
}); });
it("persists form change when switched out", async () => { it("persists form change when switched out", async () => {
game.override.enemyMoveset([MoveId.SHADOW_SNEAK]); game.override.enemyMoveset([MoveId.SHADOW_SNEAK]).starterSpecies(0);
game.override.starterSpecies(0);
await game.classicMode.startBattle([SpeciesId.MIMIKYU, SpeciesId.FURRET]); await game.classicMode.startBattle([SpeciesId.MIMIKYU, SpeciesId.FURRET]);
@ -131,8 +129,7 @@ describe("Abilities - Disguise", () => {
}); });
it("persists form change when wave changes with no arena reset", async () => { it("persists form change when wave changes with no arena reset", async () => {
game.override.starterSpecies(0); game.override.starterSpecies(0).starterForms({
game.override.starterForms({
[SpeciesId.MIMIKYU]: bustedForm, [SpeciesId.MIMIKYU]: bustedForm,
}); });
await game.classicMode.startBattle([SpeciesId.FURRET, SpeciesId.MIMIKYU]); await game.classicMode.startBattle([SpeciesId.FURRET, SpeciesId.MIMIKYU]);
@ -148,11 +145,12 @@ describe("Abilities - Disguise", () => {
}); });
it("reverts to Disguised form on arena reset", async () => { it("reverts to Disguised form on arena reset", async () => {
game.override.startingWave(4); game.override
game.override.starterSpecies(SpeciesId.MIMIKYU); .startingWave(4)
game.override.starterForms({ .starterSpecies(SpeciesId.MIMIKYU)
[SpeciesId.MIMIKYU]: bustedForm, .starterForms({
}); [SpeciesId.MIMIKYU]: bustedForm,
});
await game.classicMode.startBattle(); await game.classicMode.startBattle();
@ -168,11 +166,12 @@ describe("Abilities - Disguise", () => {
}); });
it("reverts to Disguised form on biome change when fainted", async () => { it("reverts to Disguised form on biome change when fainted", async () => {
game.override.startingWave(10); game.override
game.override.starterSpecies(0); .startingWave(10)
game.override.starterForms({ .starterSpecies(0)
[SpeciesId.MIMIKYU]: bustedForm, .starterForms({
}); [SpeciesId.MIMIKYU]: bustedForm,
});
await game.classicMode.startBattle([SpeciesId.MIMIKYU, SpeciesId.FURRET]); await game.classicMode.startBattle([SpeciesId.MIMIKYU, SpeciesId.FURRET]);
@ -206,8 +205,7 @@ describe("Abilities - Disguise", () => {
}); });
it("activates when Aerilate circumvents immunity to the move's base type", async () => { it("activates when Aerilate circumvents immunity to the move's base type", async () => {
game.override.ability(AbilityId.AERILATE); game.override.ability(AbilityId.AERILATE).moveset([MoveId.TACKLE]);
game.override.moveset([MoveId.TACKLE]);
await game.classicMode.startBattle(); await game.classicMode.startBattle();

View File

@ -44,7 +44,7 @@ describe("Abilities - Flash Fire", () => {
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(blissey.hp).toBe(blissey.getMaxHp()); expect(blissey.hp).toBe(blissey.getMaxHp());
}, 20000); });
it("not activate if the Pokémon is protected from the Fire-type move", async () => { it("not activate if the Pokémon is protected from the Fire-type move", async () => {
game.override.enemyMoveset([MoveId.EMBER]).moveset([MoveId.PROTECT]); game.override.enemyMoveset([MoveId.EMBER]).moveset([MoveId.PROTECT]);
@ -55,7 +55,7 @@ describe("Abilities - Flash Fire", () => {
game.move.select(MoveId.PROTECT); game.move.select(MoveId.PROTECT);
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeUndefined(); expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeUndefined();
}, 20000); });
it("activated by Will-O-Wisp", async () => { it("activated by Will-O-Wisp", async () => {
game.override.enemyMoveset([MoveId.WILL_O_WISP]).moveset(MoveId.SPLASH); game.override.enemyMoveset([MoveId.WILL_O_WISP]).moveset(MoveId.SPLASH);
@ -70,11 +70,10 @@ describe("Abilities - Flash Fire", () => {
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeDefined(); expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeDefined();
}, 20000); });
it("activated after being frozen", async () => { it("activated after being frozen", async () => {
game.override.enemyMoveset([MoveId.EMBER]).moveset(MoveId.SPLASH); game.override.enemyMoveset([MoveId.EMBER]).moveset(MoveId.SPLASH).statusEffect(StatusEffect.FREEZE);
game.override.statusEffect(StatusEffect.FREEZE);
await game.classicMode.startBattle([SpeciesId.BLISSEY]); await game.classicMode.startBattle([SpeciesId.BLISSEY]);
const blissey = game.scene.getPlayerPokemon()!; const blissey = game.scene.getPlayerPokemon()!;
@ -83,7 +82,7 @@ describe("Abilities - Flash Fire", () => {
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeDefined(); expect(blissey!.getTag(BattlerTagType.FIRE_BOOST)).toBeDefined();
}, 20000); });
it("not passing with baton pass", async () => { it("not passing with baton pass", async () => {
game.override.enemyMoveset([MoveId.EMBER]).moveset([MoveId.BATON_PASS]); game.override.enemyMoveset([MoveId.EMBER]).moveset([MoveId.BATON_PASS]);
@ -99,11 +98,14 @@ describe("Abilities - Flash Fire", () => {
const chansey = game.scene.getPlayerPokemon()!; const chansey = game.scene.getPlayerPokemon()!;
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(SpeciesId.CHANSEY); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(SpeciesId.CHANSEY);
expect(chansey!.getTag(BattlerTagType.FIRE_BOOST)).toBeUndefined(); expect(chansey!.getTag(BattlerTagType.FIRE_BOOST)).toBeUndefined();
}, 20000); });
it("boosts Fire-type move when the ability is activated", async () => { it("boosts Fire-type move when the ability is activated", async () => {
game.override.enemyMoveset([MoveId.FIRE_PLEDGE]).moveset([MoveId.EMBER, MoveId.SPLASH]); game.override
game.override.enemyAbility(AbilityId.FLASH_FIRE).ability(AbilityId.NONE); .enemyMoveset([MoveId.FIRE_PLEDGE])
.moveset([MoveId.EMBER, MoveId.SPLASH])
.enemyAbility(AbilityId.FLASH_FIRE)
.ability(AbilityId.NONE);
await game.classicMode.startBattle([SpeciesId.BLISSEY]); await game.classicMode.startBattle([SpeciesId.BLISSEY]);
const blissey = game.scene.getPlayerPokemon()!; const blissey = game.scene.getPlayerPokemon()!;
const initialHP = 1000; const initialHP = 1000;
@ -124,12 +126,15 @@ describe("Abilities - Flash Fire", () => {
const flashFireDmg = initialHP - blissey.hp; const flashFireDmg = initialHP - blissey.hp;
expect(flashFireDmg).toBeGreaterThan(originalDmg); expect(flashFireDmg).toBeGreaterThan(originalDmg);
}, 20000); });
it("still activates regardless of accuracy check", async () => { it("still activates regardless of accuracy check", async () => {
game.override.moveset(MoveId.FIRE_PLEDGE).enemyMoveset(MoveId.EMBER); game.override
game.override.enemyAbility(AbilityId.NONE).ability(AbilityId.FLASH_FIRE); .moveset(MoveId.FIRE_PLEDGE)
game.override.enemySpecies(SpeciesId.BLISSEY); .enemyMoveset(MoveId.EMBER)
.enemyAbility(AbilityId.NONE)
.ability(AbilityId.FLASH_FIRE)
.enemySpecies(SpeciesId.BLISSEY);
await game.classicMode.startBattle([SpeciesId.RATTATA]); await game.classicMode.startBattle([SpeciesId.RATTATA]);
const blissey = game.scene.getEnemyPokemon()!; const blissey = game.scene.getEnemyPokemon()!;
@ -153,5 +158,5 @@ describe("Abilities - Flash Fire", () => {
const flashFireDmg = initialHP - blissey.hp; const flashFireDmg = initialHP - blissey.hp;
expect(flashFireDmg).toBeGreaterThan(originalDmg); expect(flashFireDmg).toBeGreaterThan(originalDmg);
}, 20000); });
}); });

View File

@ -47,9 +47,10 @@ describe("Abilities - Flower Gift", () => {
allyAbility = AbilityId.BALL_FETCH, allyAbility = AbilityId.BALL_FETCH,
enemyAbility = AbilityId.BALL_FETCH, enemyAbility = AbilityId.BALL_FETCH,
): Promise<[number, number]> => { ): Promise<[number, number]> => {
game.override.battleStyle("double"); game.override
game.override.moveset([MoveId.SPLASH, MoveId.SUNNY_DAY, move, MoveId.HEAL_PULSE]); .battleStyle("double")
game.override.enemyMoveset([MoveId.SPLASH, MoveId.HEAL_PULSE]); .moveset([MoveId.SPLASH, MoveId.SUNNY_DAY, move, MoveId.HEAL_PULSE])
.enemyMoveset([MoveId.SPLASH, MoveId.HEAL_PULSE]);
const target_index = allyAttacker ? BattlerIndex.ENEMY : BattlerIndex.PLAYER_2; const target_index = allyAttacker ? BattlerIndex.ENEMY : BattlerIndex.PLAYER_2;
const attacker_index = allyAttacker ? BattlerIndex.PLAYER_2 : BattlerIndex.ENEMY; const attacker_index = allyAttacker ? BattlerIndex.PLAYER_2 : BattlerIndex.ENEMY;
const ally_move = allyAttacker ? move : MoveId.SPLASH; const ally_move = allyAttacker ? move : MoveId.SPLASH;

View File

@ -89,7 +89,6 @@ describe("Abilities - Flower Veil", () => {
await game.move.selectEnemyMove(MoveId.THUNDER_WAVE); await game.move.selectEnemyMove(MoveId.THUNDER_WAVE);
await game.toNextTurn(); await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); expect(game.scene.getPlayerPokemon()!.status).toBeUndefined();
vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockClear();
}); });
it("should not prevent status conditions for a non-grass user and its non-grass allies", async () => { it("should not prevent status conditions for a non-grass user and its non-grass allies", async () => {

View File

@ -63,9 +63,10 @@ describe("Abilities - Good As Gold", () => {
}); });
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 () => {
game.override.battleStyle("double"); game.override
game.override.enemyMoveset([MoveId.STEALTH_ROCK, MoveId.HAZE]); .battleStyle("double")
game.override.moveset([MoveId.SWORDS_DANCE, MoveId.SAFEGUARD]); .enemyMoveset([MoveId.STEALTH_ROCK, MoveId.HAZE])
.moveset([MoveId.SWORDS_DANCE, MoveId.SAFEGUARD]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
const [good_as_gold, ball_fetch] = game.scene.getPlayerField(); const [good_as_gold, ball_fetch] = game.scene.getPlayerField();
@ -85,8 +86,7 @@ describe("Abilities - Good As Gold", () => {
}); });
it("should not block field targeted effects in singles", async () => { it("should not block field targeted effects in singles", async () => {
game.override.battleStyle("single"); game.override.battleStyle("single").enemyMoveset([MoveId.SPIKES]);
game.override.enemyMoveset([MoveId.SPIKES]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.SPLASH, 0); game.move.select(MoveId.SPLASH, 0);
@ -96,8 +96,7 @@ describe("Abilities - Good As Gold", () => {
}); });
it("should block the ally's helping hand", async () => { it("should block the ally's helping hand", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double").moveset([MoveId.HELPING_HAND, MoveId.TACKLE]);
game.override.moveset([MoveId.HELPING_HAND, MoveId.TACKLE]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
game.move.select(MoveId.HELPING_HAND, 0); game.move.select(MoveId.HELPING_HAND, 0);
@ -129,8 +128,7 @@ describe("Abilities - Good As Gold", () => {
}); });
it("should not block field targeted effects like rain dance", async () => { it("should not block field targeted effects like rain dance", async () => {
game.override.battleStyle("single"); game.override.battleStyle("single").enemyMoveset([MoveId.RAIN_DANCE]);
game.override.enemyMoveset([MoveId.RAIN_DANCE]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.use(MoveId.SPLASH, 0); game.move.use(MoveId.SPLASH, 0);

View File

@ -1,15 +1,19 @@
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { RandomMoveAttr } from "#app/data/moves/move";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { AbilityId } from "#enums/ability-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#app/enums/stat"; import { Stat } from "#app/enums/stat";
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, vi } from "vitest";
import { MoveResult } from "#enums/move-result";
import { MoveUseMode } from "#enums/move-use-mode";
describe("Abilities - Gorilla Tactics", () => { describe("Abilities - Gorilla Tactics", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
beforeAll(() => { beforeAll(() => {
phaserGame = new Phaser.Game({ phaserGame = new Phaser.Game({
type: Phaser.HEADLESS, type: Phaser.HEADLESS,
@ -25,10 +29,9 @@ describe("Abilities - Gorilla Tactics", () => {
game.override game.override
.battleStyle("single") .battleStyle("single")
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset([MoveId.SPLASH, MoveId.DISABLE])
.enemySpecies(SpeciesId.MAGIKARP) .enemySpecies(SpeciesId.MAGIKARP)
.enemyLevel(30) .enemyLevel(30)
.moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.GROWL]) .moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.GROWL, MoveId.METRONOME])
.ability(AbilityId.GORILLA_TACTICS); .ability(AbilityId.GORILLA_TACTICS);
}); });
@ -39,9 +42,8 @@ describe("Abilities - Gorilla Tactics", () => {
const initialAtkStat = darmanitan.getStat(Stat.ATK); const initialAtkStat = darmanitan.getStat(Stat.ATK);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.move.selectEnemyMove(MoveId.SPLASH); await game.move.forceEnemyMove(MoveId.SPLASH);
await game.toEndOfTurn();
await game.phaseInterceptor.to("TurnEndPhase");
expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5); expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5);
// Other moves should be restricted // Other moves should be restricted
@ -52,32 +54,50 @@ describe("Abilities - Gorilla Tactics", () => {
it("should struggle if the only usable move is disabled", async () => { it("should struggle if the only usable move is disabled", async () => {
await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]); await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]);
const darmanitan = game.scene.getPlayerPokemon()!; const darmanitan = game.field.getPlayerPokemon();
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.field.getEnemyPokemon();
// First turn, lock move to Growl // First turn, lock move to Growl
game.move.select(MoveId.GROWL); game.move.select(MoveId.GROWL);
await game.move.selectEnemyMove(MoveId.SPLASH); await game.move.forceEnemyMove(MoveId.SPLASH);
// Second turn, Growl is interrupted by Disable
await game.toNextTurn(); await game.toNextTurn();
// Second turn, Growl is interrupted by Disable
game.move.select(MoveId.GROWL); game.move.select(MoveId.GROWL);
await game.move.selectEnemyMove(MoveId.DISABLE); await game.move.forceEnemyMove(MoveId.DISABLE);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.toNextTurn();
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.getStatStage(Stat.ATK)).toBe(-1); // Only the effect of the first Growl should be applied expect(enemy.getStatStage(Stat.ATK)).toBe(-1); // Only the effect of the first Growl should be applied
// Third turn, Struggle is used // Third turn, Struggle is used
await game.toNextTurn();
game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE);
await game.move.forceEnemyMove(MoveId.SPLASH); //prevent protect from being used by the enemy await game.move.forceEnemyMove(MoveId.SPLASH); //prevent protect from being used by the enemy
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEndPhase"); await game.phaseInterceptor.to("MoveEndPhase");
expect(darmanitan.hp).toBeLessThan(darmanitan.getMaxHp()); expect(darmanitan.hp).toBeLessThan(darmanitan.getMaxHp());
await game.toNextTurn();
expect(darmanitan.getLastXMoves()[0].move).toBe(MoveId.STRUGGLE);
});
it("should lock into calling moves, even if also in moveset", async () => {
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.TACKLE);
await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]);
const darmanitan = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.METRONOME);
await game.phaseInterceptor.to("TurnEndPhase");
// Gorilla Tactics should bypass dancer and instruct
expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(true);
expect(darmanitan.isMoveRestricted(MoveId.METRONOME)).toBe(false);
expect(darmanitan.getLastXMoves(-1)).toEqual([
expect.objectContaining({ move: MoveId.TACKLE, result: MoveResult.SUCCESS, useMode: MoveUseMode.FOLLOW_UP }),
expect.objectContaining({ move: MoveId.METRONOME, result: MoveResult.SUCCESS, useMode: MoveUseMode.NORMAL }),
]);
}); });
it("should activate when the opponenet protects", async () => { it("should activate when the opponenet protects", async () => {

View File

@ -4,17 +4,15 @@ import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
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, type MockInstance } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { isNullOrUndefined } from "#app/utils/common"; import { isNullOrUndefined } from "#app/utils/common";
import { allAbilities } from "#app/data/data-lists"; import { allAbilities } from "#app/data/data-lists";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import type { PostTurnResetStatusAbAttr } from "#app/@types/ability-types"; import { PostTurnResetStatusAbAttr } from "#app/data/abilities/ability";
describe("Abilities - Healer", () => { describe("Abilities - Healer", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
let healerAttrSpy: MockInstance;
let healerAttr: PostTurnResetStatusAbAttr;
beforeAll(() => { beforeAll(() => {
phaserGame = new Phaser.Game({ phaserGame = new Phaser.Game({
@ -24,7 +22,6 @@ describe("Abilities - Healer", () => {
afterEach(() => { afterEach(() => {
game.phaseInterceptor.restoreOg(); game.phaseInterceptor.restoreOg();
healerAttrSpy.mockRestore();
}); });
beforeEach(() => { beforeEach(() => {
@ -38,30 +35,28 @@ describe("Abilities - Healer", () => {
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH); .enemyMoveset(MoveId.SPLASH);
healerAttr = allAbilities[AbilityId.HEALER].getAttrs("PostTurnResetStatusAbAttr")[0]; // Mock healer to have a 100% chance of healing its ally
healerAttrSpy = vi vi.spyOn(allAbilities[AbilityId.HEALER].getAttrs("PostTurnResetStatusAbAttr")[0], "getCondition").mockReturnValue(
.spyOn(healerAttr, "getCondition") (pokemon: Pokemon) => !isNullOrUndefined(pokemon.getAlly()),
.mockReturnValue((pokemon: Pokemon) => !isNullOrUndefined(pokemon.getAlly())); );
}); });
it("should not queue a message phase for healing if the ally has fainted", async () => { it("should not queue a message phase for healing if the ally has fainted", async () => {
const abSpy = vi.spyOn(PostTurnResetStatusAbAttr.prototype, "canApplyPostTurn");
game.override.moveset([MoveId.SPLASH, MoveId.LUNAR_DANCE]); game.override.moveset([MoveId.SPLASH, MoveId.LUNAR_DANCE]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
const user = game.scene.getPlayerPokemon()!; const user = game.scene.getPlayerPokemon()!;
// Only want one magikarp to have the ability. // Only want one magikarp to have the ability
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]); vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
// faint the ally // faint the ally
game.move.select(MoveId.LUNAR_DANCE, 1); game.move.select(MoveId.LUNAR_DANCE, 1);
const abSpy = vi.spyOn(healerAttr, "canApplyPostTurn");
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
// It's not enough to just test that the ally still has its status. // It's not enough to just test that the ally still has its status.
// We need to ensure that the ability failed to meet its condition // We need to ensure that the ability failed to meet its condition
expect(abSpy).toHaveReturnedWith(false); expect(abSpy).toHaveReturnedWith(false);
// Explicitly restore the mock to ensure pollution doesn't happen
abSpy.mockRestore();
}); });
it("should heal the status of an ally if the ally has a status", async () => { it("should heal the status of an ally if the ally has a status", async () => {

View File

@ -75,8 +75,7 @@ describe("Abilities - Hustle", () => {
}); });
it("does not affect OHKO moves", async () => { it("does not affect OHKO moves", async () => {
game.override.startingLevel(100); game.override.startingLevel(100).enemyLevel(30);
game.override.enemyLevel(30);
await game.classicMode.startBattle([SpeciesId.PIKACHU]); await game.classicMode.startBattle([SpeciesId.PIKACHU]);
const pikachu = game.scene.getPlayerPokemon()!; const pikachu = game.scene.getPlayerPokemon()!;

View File

@ -30,10 +30,11 @@ describe("Abilities - Ice Face", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("single"); game.override
game.override.enemySpecies(SpeciesId.EISCUE); .battleStyle("single")
game.override.enemyAbility(AbilityId.ICE_FACE); .enemySpecies(SpeciesId.EISCUE)
game.override.moveset([MoveId.TACKLE, MoveId.ICE_BEAM, MoveId.TOXIC_THREAD, MoveId.HAIL]); .enemyAbility(AbilityId.ICE_FACE)
.moveset([MoveId.TACKLE, MoveId.ICE_BEAM, MoveId.TOXIC_THREAD, MoveId.HAIL]);
}); });
it("takes no damage from physical move and transforms to Noice", async () => { it("takes no damage from physical move and transforms to Noice", async () => {
@ -51,8 +52,7 @@ describe("Abilities - Ice Face", () => {
}); });
it("takes no damage from the first hit of multihit physical move and transforms to Noice", async () => { it("takes no damage from the first hit of multihit physical move and transforms to Noice", async () => {
game.override.moveset([MoveId.SURGING_STRIKES]); game.override.moveset([MoveId.SURGING_STRIKES]).enemyLevel(1);
game.override.enemyLevel(1);
await game.classicMode.startBattle([SpeciesId.HITMONLEE]); await game.classicMode.startBattle([SpeciesId.HITMONLEE]);
game.move.select(MoveId.SURGING_STRIKES); game.move.select(MoveId.SURGING_STRIKES);
@ -196,12 +196,13 @@ describe("Abilities - Ice Face", () => {
}); });
it("reverts to Ice Face on arena reset", async () => { it("reverts to Ice Face on arena reset", async () => {
game.override.startingWave(4); game.override
game.override.startingLevel(4); .startingWave(4)
game.override.enemySpecies(SpeciesId.MAGIKARP); .startingLevel(4)
game.override.starterForms({ .enemySpecies(SpeciesId.MAGIKARP)
[SpeciesId.EISCUE]: noiceForm, .starterForms({
}); [SpeciesId.EISCUE]: noiceForm,
});
await game.classicMode.startBattle([SpeciesId.EISCUE]); await game.classicMode.startBattle([SpeciesId.EISCUE]);

View File

@ -116,26 +116,23 @@ describe("Abilities - Illusion", () => {
expect(psychicEffectiveness).above(flameThrowerEffectiveness); expect(psychicEffectiveness).above(flameThrowerEffectiveness);
}); });
it("does not break from indirect damage", async () => { it("should not break from indirect damage from status, weather or recoil", async () => {
game.override.enemySpecies(SpeciesId.GIGALITH); game.override.enemySpecies(SpeciesId.GIGALITH).enemyAbility(AbilityId.SAND_STREAM);
game.override.enemyAbility(AbilityId.SAND_STREAM);
game.override.enemyMoveset(MoveId.WILL_O_WISP);
game.override.moveset([MoveId.FLARE_BLITZ]);
await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.AZUMARILL]); await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.AZUMARILL]);
game.move.select(MoveId.FLARE_BLITZ); game.move.use(MoveId.FLARE_BLITZ);
await game.move.forceEnemyMove(MoveId.WILL_O_WISP);
await game.phaseInterceptor.to("TurnEndPhase"); await game.toEndOfTurn();
const zoroark = game.scene.getPlayerPokemon()!; const zoroark = game.scene.getPlayerPokemon()!;
expect(!!zoroark.summonData.illusion).equals(true); expect(!!zoroark.summonData.illusion).equals(true);
}); });
it("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => { it("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => {
game.override.enemyMoveset(MoveId.SPLASH); game.override.enemyMoveset(MoveId.SPLASH);
await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.ZOROARK, SpeciesId.AXEW]); await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.ZOROARK, SpeciesId.AXEW]);
const axew = game.scene.getPlayerParty().at(2)!; const axew = game.scene.getPlayerParty().at(2)!;
axew.shiny = true; axew.shiny = true;
axew.nickname = btoa(unescape(encodeURIComponent("axew nickname"))); axew.nickname = btoa(unescape(encodeURIComponent("axew nickname")));

View File

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

@ -62,7 +62,7 @@ describe("Abilities - Intimidate", () => {
expect(playerPokemon.species.speciesId).toBe(SpeciesId.POOCHYENA); expect(playerPokemon.species.speciesId).toBe(SpeciesId.POOCHYENA);
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2);
}, 20000); });
it("should lower ATK stat stage by 1 for every enemy Pokemon in a double battle on entry", async () => { it("should lower ATK stat stage by 1 for every enemy Pokemon in a double battle on entry", async () => {
game.override.battleStyle("double").startingWave(3); game.override.battleStyle("double").startingWave(3);
@ -85,11 +85,10 @@ describe("Abilities - Intimidate", () => {
expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2); expect(enemyField[1].getStatStage(Stat.ATK)).toBe(-2);
expect(playerField[0].getStatStage(Stat.ATK)).toBe(-2); expect(playerField[0].getStatStage(Stat.ATK)).toBe(-2);
expect(playerField[1].getStatStage(Stat.ATK)).toBe(-2); expect(playerField[1].getStatStage(Stat.ATK)).toBe(-2);
}, 20000); });
it("should not activate again if there is no switch or new entry", async () => { it("should not activate again if there is no switch or new entry", async () => {
game.override.startingWave(2); game.override.startingWave(2).moveset([MoveId.SPLASH]);
game.override.moveset([MoveId.SPLASH]);
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
@ -103,7 +102,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);
}, 20000); });
it("should lower ATK stat stage by 1 for every switch", async () => { it("should lower ATK stat stage by 1 for every switch", async () => {
game.override.moveset([MoveId.SPLASH]).enemyMoveset([MoveId.VOLT_SWITCH]).startingWave(5); game.override.moveset([MoveId.SPLASH]).enemyMoveset([MoveId.VOLT_SWITCH]).startingWave(5);
@ -130,5 +129,5 @@ describe("Abilities - Intimidate", () => {
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(-3);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0);
}, 200000); });
}); });

View File

@ -22,10 +22,11 @@ describe("Abilities - Intrepid Sword", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("single"); game.override
game.override.enemySpecies(SpeciesId.ZACIAN); .battleStyle("single")
game.override.enemyAbility(AbilityId.INTREPID_SWORD); .enemySpecies(SpeciesId.ZACIAN)
game.override.ability(AbilityId.INTREPID_SWORD); .enemyAbility(AbilityId.INTREPID_SWORD)
.ability(AbilityId.INTREPID_SWORD);
}); });
it("should raise ATK stat stage by 1 on entry", async () => { it("should raise ATK stat stage by 1 on entry", async () => {
@ -38,5 +39,5 @@ describe("Abilities - Intrepid Sword", () => {
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000); });
}); });

View File

@ -108,8 +108,7 @@ describe("Abilities - Libero", () => {
}); });
test("ability applies correctly even if the type has changed by another ability", async () => { test("ability applies correctly even if the type has changed by another ability", async () => {
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).passiveAbility(AbilityId.REFRIGERATE);
game.override.passiveAbility(AbilityId.REFRIGERATE);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -156,8 +155,7 @@ describe("Abilities - Libero", () => {
}); });
test("ability applies correctly even if the pokemon's move misses", async () => { test("ability applies correctly even if the pokemon's move misses", async () => {
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).enemyMoveset(MoveId.SPLASH);
game.override.enemyMoveset(MoveId.SPLASH);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -188,8 +186,7 @@ describe("Abilities - Libero", () => {
}); });
test("ability applies correctly even if the pokemon's move fails because of type immunity", async () => { test("ability applies correctly even if the pokemon's move fails because of type immunity", async () => {
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).enemySpecies(SpeciesId.GASTLY);
game.override.enemySpecies(SpeciesId.GASTLY);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -262,8 +259,7 @@ describe("Abilities - Libero", () => {
}); });
test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => { test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => {
game.override.moveset([MoveId.TRICK_OR_TREAT]); game.override.moveset([MoveId.TRICK_OR_TREAT]).enemySpecies(SpeciesId.GASTLY);
game.override.enemySpecies(SpeciesId.GASTLY);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);

View File

@ -41,18 +41,16 @@ describe("Abilities - Magic Bounce", () => {
it("should reflect basic status moves", async () => { it("should reflect basic status moves", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.GROWL); game.move.use(MoveId.GROWL);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
}); });
it("should not bounce moves while the target is in the semi-invulnerable state", async () => { it("should not bounce moves while the target is in the semi-invulnerable state", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.override.moveset([MoveId.GROWL]);
game.override.enemyMoveset([MoveId.FLY]);
game.move.select(MoveId.GROWL); game.move.use(MoveId.GROWL);
await game.move.selectEnemyMove(MoveId.FLY); await game.move.forceEnemyMove(MoveId.FLY);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
@ -61,11 +59,10 @@ describe("Abilities - Magic Bounce", () => {
it("should individually bounce back multi-target moves", async () => { it("should individually bounce back multi-target moves", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double");
game.override.moveset([MoveId.GROWL, MoveId.SPLASH]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
game.move.select(MoveId.GROWL, 0); game.move.use(MoveId.GROWL, 0);
game.move.select(MoveId.SPLASH, 1); game.move.use(MoveId.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
const user = game.scene.getPlayerField()[0]; const user = game.scene.getPlayerField()[0];
@ -75,9 +72,8 @@ describe("Abilities - Magic Bounce", () => {
it("should still bounce back a move that would otherwise fail", async () => { it("should still bounce back a move that would otherwise fail", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6); game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6);
game.override.moveset([MoveId.GROWL]);
game.move.select(MoveId.GROWL); game.move.use(MoveId.GROWL);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
@ -107,29 +103,26 @@ describe("Abilities - Magic Bounce", () => {
game.override.ability(AbilityId.MOLD_BREAKER); game.override.ability(AbilityId.MOLD_BREAKER);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.GROWL); game.move.use(MoveId.GROWL);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
}); });
it("should bounce back a spread status move against both pokemon", async () => { it("should bounce back a spread status move against both pokemon", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double").enemyMoveset([MoveId.SPLASH]);
game.override.moveset([MoveId.GROWL, MoveId.SPLASH]);
game.override.enemyMoveset([MoveId.SPLASH]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
game.move.select(MoveId.GROWL, 0); game.move.use(MoveId.GROWL, 0);
game.move.select(MoveId.SPLASH, 1); game.move.use(MoveId.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -2)).toBeTruthy(); expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -2)).toBeTruthy();
}); });
it("should only bounce spikes back once in doubles when both targets have magic bounce", async () => { it("should only bounce spikes back once in doubles when both targets have magic bounce", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double").moveset([MoveId.SPIKES]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.override.moveset([MoveId.SPIKES]);
game.move.select(MoveId.SPIKES); game.move.select(MoveId.SPIKES);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
@ -139,8 +132,7 @@ describe("Abilities - Magic Bounce", () => {
}); });
it("should bounce spikes even when the target is protected", async () => { it("should bounce spikes even when the target is protected", async () => {
game.override.moveset([MoveId.SPIKES]); game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.PROTECT]);
game.override.enemyMoveset([MoveId.PROTECT]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.SPIKES); game.move.select(MoveId.SPIKES);
@ -149,8 +141,7 @@ describe("Abilities - Magic Bounce", () => {
}); });
it("should not bounce spikes when the target is in the semi-invulnerable state", async () => { it("should not bounce spikes when the target is in the semi-invulnerable state", async () => {
game.override.moveset([MoveId.SPIKES]); game.override.moveset([MoveId.SPIKES]).enemyMoveset([MoveId.FLY]);
game.override.enemyMoveset([MoveId.FLY]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.move.select(MoveId.SPIKES); game.move.select(MoveId.SPIKES);
@ -160,9 +151,8 @@ describe("Abilities - Magic Bounce", () => {
}); });
it("should not bounce back curse", async () => { it("should not bounce back curse", async () => {
game.override.starterSpecies(SpeciesId.GASTLY);
await game.classicMode.startBattle([SpeciesId.GASTLY]);
game.override.moveset([MoveId.CURSE]); game.override.moveset([MoveId.CURSE]);
await game.classicMode.startBattle([SpeciesId.GASTLY]);
game.move.select(MoveId.CURSE); game.move.select(MoveId.CURSE);
await game.phaseInterceptor.to("BerryPhase"); await game.phaseInterceptor.to("BerryPhase");
@ -171,8 +161,7 @@ describe("Abilities - Magic Bounce", () => {
}); });
it("should not cause encore to be interrupted after bouncing", async () => { it("should not cause encore to be interrupted after bouncing", async () => {
game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]); game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]).enemyMoveset([MoveId.TACKLE, MoveId.GROWL]);
game.override.enemyMoveset([MoveId.TACKLE, MoveId.GROWL]);
// game.override.ability(AbilityId.MOLD_BREAKER); // game.override.ability(AbilityId.MOLD_BREAKER);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
@ -199,9 +188,10 @@ describe("Abilities - Magic Bounce", () => {
// TODO: encore is failing if the last move was virtual. // TODO: encore is failing if the last move was virtual.
it.todo("should not cause the bounced move to count for encore", async () => { it.todo("should not cause the bounced move to count for encore", async () => {
game.override.moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE]); game.override
game.override.enemyMoveset([MoveId.GROWL, MoveId.TACKLE]); .moveset([MoveId.SPLASH, MoveId.GROWL, MoveId.ENCORE])
game.override.enemyAbility(AbilityId.MAGIC_BOUNCE); .enemyMoveset([MoveId.GROWL, MoveId.TACKLE])
.enemyAbility(AbilityId.MAGIC_BOUNCE);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;
@ -227,9 +217,8 @@ describe("Abilities - Magic Bounce", () => {
// TODO: stomping tantrum should consider moves that were bounced. // TODO: stomping tantrum should consider moves that were bounced.
it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => { it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => {
game.override.battleStyle("single"); game.override.battleStyle("single").moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
game.override.moveset([MoveId.STOMPING_TANTRUM, MoveId.CHARM]);
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM]; const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
vi.spyOn(stomping_tantrum, "calculateBattlePower"); vi.spyOn(stomping_tantrum, "calculateBattlePower");
@ -242,36 +231,30 @@ describe("Abilities - Magic Bounce", () => {
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150); expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
}); });
// TODO: stomping tantrum should consider moves that were bounced. // TODO: stomping tantrum should consider moves that were bounced
it.todo( it.todo("should boost enemy's stomping tantrum after failed bounce", async () => {
"should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", game.override.enemyMoveset([MoveId.STOMPING_TANTRUM, MoveId.SPLASH, MoveId.CHARM]);
async () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
game.override.enemyMoveset([MoveId.STOMPING_TANTRUM, MoveId.SPLASH, MoveId.CHARM]);
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM]; const stomping_tantrum = allMoves[MoveId.STOMPING_TANTRUM];
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(stomping_tantrum, "calculateBattlePower"); vi.spyOn(stomping_tantrum, "calculateBattlePower");
game.move.select(MoveId.SPORE); // Spore gets reflected back onto us
await game.move.selectEnemyMove(MoveId.CHARM); game.move.select(MoveId.SPORE);
await game.phaseInterceptor.to("TurnEndPhase"); await game.move.selectEnemyMove(MoveId.CHARM);
expect(enemy.getLastXMoves(1)[0].result).toBe("success"); await game.toNextTurn();
expect(enemy.getLastXMoves(1)[0].result).toBe("success");
await game.phaseInterceptor.to("BerryPhase"); game.move.select(MoveId.SPORE);
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); await game.move.selectEnemyMove(MoveId.STOMPING_TANTRUM);
await game.toNextTurn();
await game.toNextTurn(); expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150);
game.move.select(MoveId.GROWL); });
await game.phaseInterceptor.to("BerryPhase");
expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75);
},
);
it("should respect immunities when bouncing a move", async () => { it("should respect immunities when bouncing a move", async () => {
vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100); vi.spyOn(allMoves[MoveId.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100);
game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]); game.override.moveset([MoveId.THUNDER_WAVE, MoveId.GROWL]).ability(AbilityId.SOUNDPROOF);
game.override.ability(AbilityId.SOUNDPROOF);
await game.classicMode.startBattle([SpeciesId.PHANPY]); await game.classicMode.startBattle([SpeciesId.PHANPY]);
// Turn 1 - thunder wave immunity test // Turn 1 - thunder wave immunity test
@ -309,8 +292,7 @@ describe("Abilities - Magic Bounce", () => {
}); });
it("should always apply the leftmost available target's magic bounce when bouncing moves like sticky webs in doubles", async () => { it("should always apply the leftmost available target's magic bounce when bouncing moves like sticky webs in doubles", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double").moveset([MoveId.STICKY_WEB, MoveId.SPLASH, MoveId.TRICK_ROOM]);
game.override.moveset([MoveId.STICKY_WEB, MoveId.SPLASH, MoveId.TRICK_ROOM]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
const [enemy_1, enemy_2] = game.scene.getEnemyField(); const [enemy_1, enemy_2] = game.scene.getEnemyField();
@ -345,6 +327,7 @@ describe("Abilities - Magic Bounce", () => {
it("should not bounce back status moves that hit through semi-invulnerable states", async () => { it("should not bounce back status moves that hit through semi-invulnerable states", async () => {
game.override.moveset([MoveId.TOXIC, MoveId.CHARM]); game.override.moveset([MoveId.TOXIC, MoveId.CHARM]);
await game.classicMode.startBattle([SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
game.move.select(MoveId.TOXIC); game.move.select(MoveId.TOXIC);
await game.move.selectEnemyMove(MoveId.FLY); await game.move.selectEnemyMove(MoveId.FLY);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);

View File

@ -30,16 +30,16 @@ describe("Abilities - Magic Guard", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
/** Player Pokemon overrides */ game.override
game.override.ability(AbilityId.MAGIC_GUARD); /** Player Pokemon overrides */
game.override.moveset([MoveId.SPLASH]); .ability(AbilityId.MAGIC_GUARD)
game.override.startingLevel(100); .moveset([MoveId.SPLASH])
.startingLevel(100)
/** Enemy Pokemon overrides */ /** Enemy Pokemon overrides */
game.override.enemySpecies(SpeciesId.SNORLAX); .enemySpecies(SpeciesId.SNORLAX)
game.override.enemyAbility(AbilityId.INSOMNIA); .enemyAbility(AbilityId.INSOMNIA)
game.override.enemyMoveset(MoveId.SPLASH); .enemyMoveset(MoveId.SPLASH)
game.override.enemyLevel(100); .enemyLevel(100);
}); });
//Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Magic_Guard_(Ability) //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Magic_Guard_(Ability)
@ -89,8 +89,9 @@ describe("Abilities - Magic Guard", () => {
}); });
it("ability effect should not persist when the ability is replaced", async () => { it("ability effect should not persist when the ability is replaced", async () => {
game.override.enemyMoveset([MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED]); game.override
game.override.statusEffect(StatusEffect.POISON); .enemyMoveset([MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED, MoveId.WORRY_SEED])
.statusEffect(StatusEffect.POISON);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -108,8 +109,7 @@ describe("Abilities - Magic Guard", () => {
}); });
it("Magic Guard prevents damage caused by burn but other non-damaging effects are still applied", async () => { it("Magic Guard prevents damage caused by burn but other non-damaging effects are still applied", async () => {
game.override.enemyStatusEffect(StatusEffect.BURN); game.override.enemyStatusEffect(StatusEffect.BURN).enemyAbility(AbilityId.MAGIC_GUARD);
game.override.enemyAbility(AbilityId.MAGIC_GUARD);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -130,8 +130,7 @@ describe("Abilities - Magic Guard", () => {
}); });
it("Magic Guard prevents damage caused by toxic but other non-damaging effects are still applied", async () => { it("Magic Guard prevents damage caused by toxic but other non-damaging effects are still applied", async () => {
game.override.enemyStatusEffect(StatusEffect.TOXIC); game.override.enemyStatusEffect(StatusEffect.TOXIC).enemyAbility(AbilityId.MAGIC_GUARD);
game.override.enemyAbility(AbilityId.MAGIC_GUARD);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -208,8 +207,7 @@ describe("Abilities - Magic Guard", () => {
it("Magic Guard prevents against damage from volatile status effects", async () => { it("Magic Guard prevents against damage from volatile status effects", async () => {
await game.classicMode.startBattle([SpeciesId.DUSKULL]); await game.classicMode.startBattle([SpeciesId.DUSKULL]);
game.override.moveset([MoveId.CURSE]); game.override.moveset([MoveId.CURSE]).enemyAbility(AbilityId.MAGIC_GUARD);
game.override.enemyAbility(AbilityId.MAGIC_GUARD);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;
@ -331,8 +329,9 @@ describe("Abilities - Magic Guard", () => {
//Tests the ability Bad Dreams //Tests the ability Bad Dreams
game.override.statusEffect(StatusEffect.SLEEP); game.override.statusEffect(StatusEffect.SLEEP);
//enemy pokemon is given Spore just in case player pokemon somehow awakens during test //enemy pokemon is given Spore just in case player pokemon somehow awakens during test
game.override.enemyMoveset([MoveId.SPORE, MoveId.SPORE, MoveId.SPORE, MoveId.SPORE]); game.override
game.override.enemyAbility(AbilityId.BAD_DREAMS); .enemyMoveset([MoveId.SPORE, MoveId.SPORE, MoveId.SPORE, MoveId.SPORE])
.enemyAbility(AbilityId.BAD_DREAMS);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -353,8 +352,7 @@ describe("Abilities - Magic Guard", () => {
it("Magic Guard prevents damage from abilities with PostFaintContactDamageAbAttr", async () => { it("Magic Guard prevents damage from abilities with PostFaintContactDamageAbAttr", async () => {
//Tests the abilities Innards Out/Aftermath //Tests the abilities Innards Out/Aftermath
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.AFTERMATH);
game.override.enemyAbility(AbilityId.AFTERMATH);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -377,8 +375,7 @@ describe("Abilities - Magic Guard", () => {
it("Magic Guard prevents damage from abilities with PostDefendContactDamageAbAttr", async () => { it("Magic Guard prevents damage from abilities with PostDefendContactDamageAbAttr", async () => {
//Tests the abilities Iron Barbs/Rough Skin //Tests the abilities Iron Barbs/Rough Skin
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.IRON_BARBS);
game.override.enemyAbility(AbilityId.IRON_BARBS);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -400,8 +397,7 @@ describe("Abilities - Magic Guard", () => {
it("Magic Guard prevents damage from abilities with ReverseDrainAbAttr", async () => { it("Magic Guard prevents damage from abilities with ReverseDrainAbAttr", async () => {
//Tests the ability Liquid Ooze //Tests the ability Liquid Ooze
game.override.moveset([MoveId.ABSORB]); game.override.moveset([MoveId.ABSORB]).enemyAbility(AbilityId.LIQUID_OOZE);
game.override.enemyAbility(AbilityId.LIQUID_OOZE);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -422,9 +418,7 @@ describe("Abilities - Magic Guard", () => {
}); });
it("Magic Guard prevents HP loss from abilities with PostWeatherLapseDamageAbAttr", async () => { it("Magic Guard prevents HP loss from abilities with PostWeatherLapseDamageAbAttr", async () => {
//Tests the abilities Solar Power/Dry Skin game.override.passiveAbility(AbilityId.SOLAR_POWER).weather(WeatherType.SUNNY);
game.override.passiveAbility(AbilityId.SOLAR_POWER);
game.override.weather(WeatherType.SUNNY);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const leadPokemon = game.scene.getPlayerPokemon()!; const leadPokemon = game.scene.getPlayerPokemon()!;

View File

@ -37,8 +37,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Player side + single battle Intimidate - opponent loses stats", async () => { it("Player side + single battle Intimidate - opponent loses stats", async () => {
game.override.ability(AbilityId.MIRROR_ARMOR); game.override.ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE);
game.override.enemyAbility(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -54,8 +53,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Enemy side + single battle Intimidate - player loses stats", async () => { it("Enemy side + single battle Intimidate - player loses stats", async () => {
game.override.enemyAbility(AbilityId.MIRROR_ARMOR); game.override.enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE);
game.override.ability(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -71,9 +69,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Player side + double battle Intimidate - opponents each lose -2 atk", async () => { it("Player side + double battle Intimidate - opponents each lose -2 atk", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double").ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE);
game.override.ability(AbilityId.MIRROR_ARMOR);
game.override.enemyAbility(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
const [enemy1, enemy2] = game.scene.getEnemyField(); const [enemy1, enemy2] = game.scene.getEnemyField();
@ -93,9 +89,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Enemy side + double battle Intimidate - players each lose -2 atk", async () => { it("Enemy side + double battle Intimidate - players each lose -2 atk", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double").enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE);
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
game.override.ability(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
const [enemy1, enemy2] = game.scene.getEnemyField(); const [enemy1, enemy2] = game.scene.getEnemyField();
@ -115,8 +109,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Player side + single battle Intimidate + Tickle - opponent loses stats", async () => { it("Player side + single battle Intimidate + Tickle - opponent loses stats", async () => {
game.override.ability(AbilityId.MIRROR_ARMOR); game.override.ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE);
game.override.enemyAbility(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -134,9 +127,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Player side + double battle Intimidate + Tickle - opponents each lose -3 atk, -1 def", async () => { it("Player side + double battle Intimidate + Tickle - opponents each lose -3 atk, -1 def", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double").ability(AbilityId.MIRROR_ARMOR).enemyAbility(AbilityId.INTIMIDATE);
game.override.ability(AbilityId.MIRROR_ARMOR);
game.override.enemyAbility(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
const [enemy1, enemy2] = game.scene.getEnemyField(); const [enemy1, enemy2] = game.scene.getEnemyField();
@ -159,8 +150,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Enemy side + single battle Intimidate + Tickle - player loses stats", async () => { it("Enemy side + single battle Intimidate + Tickle - player loses stats", async () => {
game.override.enemyAbility(AbilityId.MIRROR_ARMOR); game.override.enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE);
game.override.ability(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -178,8 +168,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Player side + single battle Intimidate + oppoenent has white smoke - no one loses stats", async () => { it("Player side + single battle Intimidate + oppoenent has white smoke - no one loses stats", async () => {
game.override.enemyAbility(AbilityId.WHITE_SMOKE); game.override.enemyAbility(AbilityId.WHITE_SMOKE).ability(AbilityId.MIRROR_ARMOR);
game.override.ability(AbilityId.MIRROR_ARMOR);
await game.classicMode.startBattle([SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -197,8 +186,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Enemy side + single battle Intimidate + player has white smoke - no one loses stats", async () => { it("Enemy side + single battle Intimidate + player has white smoke - no one loses stats", async () => {
game.override.ability(AbilityId.WHITE_SMOKE); game.override.ability(AbilityId.WHITE_SMOKE).enemyAbility(AbilityId.MIRROR_ARMOR);
game.override.enemyAbility(AbilityId.MIRROR_ARMOR);
await game.classicMode.startBattle([SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -252,9 +240,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Both sides have mirror armor - does not loop, player loses attack", async () => { it("Both sides have mirror armor - does not loop, player loses attack", async () => {
game.override.enemyAbility(AbilityId.MIRROR_ARMOR); game.override.enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE);
game.override.ability(AbilityId.MIRROR_ARMOR);
game.override.ability(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.BULBASAUR]); await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const enemyPokemon = game.scene.getEnemyPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!;
@ -288,8 +274,7 @@ describe("Ability - Mirror Armor", () => {
}); });
it("Double battle + sticky web applied player side - player switches out and enemy 1 should lose -1 speed", async () => { it("Double battle + sticky web applied player side - player switches out and enemy 1 should lose -1 speed", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double").ability(AbilityId.MIRROR_ARMOR);
game.override.ability(AbilityId.MIRROR_ARMOR);
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]);
const [enemy1, enemy2] = game.scene.getEnemyField(); const [enemy1, enemy2] = game.scene.getEnemyField();

View File

@ -27,13 +27,14 @@ describe("Abilities - Moxie", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
const moveToUse = MoveId.AERIAL_ACE; const moveToUse = MoveId.AERIAL_ACE;
game.override.battleStyle("single"); game.override
game.override.enemySpecies(SpeciesId.RATTATA); .battleStyle("single")
game.override.enemyAbility(AbilityId.MOXIE); .enemySpecies(SpeciesId.RATTATA)
game.override.ability(AbilityId.MOXIE); .enemyAbility(AbilityId.MOXIE)
game.override.startingLevel(2000); .ability(AbilityId.MOXIE)
game.override.moveset([moveToUse]); .startingLevel(2000)
game.override.enemyMoveset(MoveId.SPLASH); .moveset([moveToUse])
.enemyMoveset(MoveId.SPLASH);
}); });
it("should raise ATK stat stage by 1 when winning a battle", async () => { it("should raise ATK stat stage by 1 when winning a battle", async () => {
@ -48,7 +49,7 @@ describe("Abilities - Moxie", () => {
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase); await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase);
expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1);
}, 20000); });
// TODO: Activate this test when MOXIE is corrected to work on faint and not on battle victory // TODO: Activate this test when MOXIE is corrected to work on faint and not on battle victory
it.todo( it.todo(

View File

@ -63,7 +63,7 @@ describe("Abilities - Mycelium Might", () => {
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1); expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
}, 20000); });
it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => { it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => {
game.override.enemyMoveset(MoveId.TACKLE); game.override.enemyMoveset(MoveId.TACKLE);
@ -86,7 +86,7 @@ describe("Abilities - Mycelium Might", () => {
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to(TurnEndPhase);
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1); expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
}, 20000); });
it("will not affect non-status moves", async () => { it("will not affect non-status moves", async () => {
await game.classicMode.startBattle([SpeciesId.REGIELEKI]); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
@ -105,5 +105,5 @@ describe("Abilities - Mycelium Might", () => {
// This means that the commandOrder should be identical to the speedOrder // This means that the commandOrder should be identical to the speedOrder
expect(speedOrder).toEqual([playerIndex, enemyIndex]); expect(speedOrder).toEqual([playerIndex, enemyIndex]);
expect(commandOrder).toEqual([playerIndex, enemyIndex]); expect(commandOrder).toEqual([playerIndex, enemyIndex]);
}, 20000); });
}); });

View File

@ -45,8 +45,9 @@ describe("Abilities - Normalize", () => {
}); });
it("should not apply the old type boost item after changing a move's type", async () => { it("should not apply the old type boost item after changing a move's type", async () => {
game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.GRASS }]); game.override
game.override.moveset([MoveId.LEAFAGE]); .startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.GRASS }])
.moveset([MoveId.LEAFAGE]);
const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower"); const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower");
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -58,8 +59,9 @@ describe("Abilities - Normalize", () => {
}); });
it("should apply silk scarf's power boost after changing a move's type", async () => { it("should apply silk scarf's power boost after changing a move's type", async () => {
game.override.startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }]); game.override
game.override.moveset([MoveId.LEAFAGE]); .startingHeldItems([{ name: "ATTACK_TYPE_BOOSTER", count: 1, type: PokemonType.NORMAL }])
.moveset([MoveId.LEAFAGE]);
const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower"); const powerSpy = vi.spyOn(allMoves[MoveId.LEAFAGE], "calculateBattlePower");
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);

View File

@ -26,14 +26,15 @@ describe("Abilities - Parental Bond", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("single"); game.override
game.override.disableCrits(); .battleStyle("single")
game.override.ability(AbilityId.PARENTAL_BOND); .disableCrits()
game.override.enemySpecies(SpeciesId.SNORLAX); .ability(AbilityId.PARENTAL_BOND)
game.override.enemyAbility(AbilityId.FUR_COAT); .enemySpecies(SpeciesId.SNORLAX)
game.override.enemyMoveset(MoveId.SPLASH); .enemyAbility(AbilityId.FUR_COAT)
game.override.startingLevel(100); .enemyMoveset(MoveId.SPLASH)
game.override.enemyLevel(100); .startingLevel(100)
.enemyLevel(100);
}); });
it("should add second strike to attack move", async () => { it("should add second strike to attack move", async () => {
@ -61,8 +62,7 @@ describe("Abilities - Parental Bond", () => {
}); });
it("should apply secondary effects to both strikes", async () => { it("should apply secondary effects to both strikes", async () => {
game.override.moveset([MoveId.POWER_UP_PUNCH]); game.override.moveset([MoveId.POWER_UP_PUNCH]).enemySpecies(SpeciesId.AMOONGUSS);
game.override.enemySpecies(SpeciesId.AMOONGUSS);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -148,8 +148,7 @@ describe("Abilities - Parental Bond", () => {
}); });
it("should not apply multiplier to counter moves", async () => { it("should not apply multiplier to counter moves", async () => {
game.override.moveset([MoveId.COUNTER]); game.override.moveset([MoveId.COUNTER]).enemyMoveset([MoveId.TACKLE]);
game.override.enemyMoveset([MoveId.TACKLE]);
await game.classicMode.startBattle([SpeciesId.SHUCKLE]); await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
@ -167,9 +166,7 @@ describe("Abilities - Parental Bond", () => {
}); });
it("should not apply to multi-target moves", async () => { it("should not apply to multi-target moves", async () => {
game.override.battleStyle("double"); game.override.battleStyle("double").moveset([MoveId.EARTHQUAKE]).passiveAbility(AbilityId.LEVITATE);
game.override.moveset([MoveId.EARTHQUAKE]);
game.override.passiveAbility(AbilityId.LEVITATE);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
@ -237,8 +234,7 @@ describe("Abilities - Parental Bond", () => {
}); });
it("Moves boosted by this ability and Multi-Lens should strike 3 times", async () => { it("Moves boosted by this ability and Multi-Lens should strike 3 times", async () => {
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -252,8 +248,7 @@ describe("Abilities - Parental Bond", () => {
}); });
it("Seismic Toss boosted by this ability and Multi-Lens should strike 3 times", async () => { it("Seismic Toss boosted by this ability and Multi-Lens should strike 3 times", async () => {
game.override.moveset([MoveId.SEISMIC_TOSS]); game.override.moveset([MoveId.SEISMIC_TOSS]).startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -378,8 +373,7 @@ describe("Abilities - Parental Bond", () => {
}); });
it("should not cause user to hit into King's Shield more than once", async () => { it("should not cause user to hit into King's Shield more than once", async () => {
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).enemyMoveset([MoveId.KINGS_SHIELD]);
game.override.enemyMoveset([MoveId.KINGS_SHIELD]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -393,8 +387,7 @@ describe("Abilities - Parental Bond", () => {
}); });
it("should not cause user to hit into Storm Drain more than once", async () => { it("should not cause user to hit into Storm Drain more than once", async () => {
game.override.moveset([MoveId.WATER_GUN]); game.override.moveset([MoveId.WATER_GUN]).enemyAbility(AbilityId.STORM_DRAIN);
game.override.enemyAbility(AbilityId.STORM_DRAIN);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);

View File

@ -21,19 +21,18 @@ describe("Abilities - Perish Song", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("single"); game.override
game.override.disableCrits(); .battleStyle("single")
.disableCrits()
game.override.enemySpecies(SpeciesId.MAGIKARP); .enemySpecies(SpeciesId.MAGIKARP)
game.override.enemyAbility(AbilityId.BALL_FETCH); .enemyAbility(AbilityId.BALL_FETCH)
.starterSpecies(SpeciesId.CURSOLA)
game.override.starterSpecies(SpeciesId.CURSOLA); .ability(AbilityId.PERISH_BODY)
game.override.ability(AbilityId.PERISH_BODY); .moveset([MoveId.SPLASH])
game.override.moveset([MoveId.SPLASH]); .enemyMoveset([MoveId.AQUA_JET]);
}); });
it("should trigger when hit with damaging move", async () => { it("should trigger when hit with damaging move", async () => {
game.override.enemyMoveset([MoveId.AQUA_JET]);
await game.classicMode.startBattle(); await game.classicMode.startBattle();
const cursola = game.scene.getPlayerPokemon(); const cursola = game.scene.getPlayerPokemon();
const magikarp = game.scene.getEnemyPokemon(); const magikarp = game.scene.getEnemyPokemon();
@ -46,7 +45,7 @@ describe("Abilities - Perish Song", () => {
}); });
it("should trigger even when fainting", async () => { it("should trigger even when fainting", async () => {
game.override.enemyMoveset([MoveId.AQUA_JET]).enemyLevel(100).startingLevel(1); game.override.enemyLevel(100).startingLevel(1);
await game.classicMode.startBattle([SpeciesId.CURSOLA, SpeciesId.FEEBAS]); await game.classicMode.startBattle([SpeciesId.CURSOLA, SpeciesId.FEEBAS]);
const magikarp = game.scene.getEnemyPokemon(); const magikarp = game.scene.getEnemyPokemon();
@ -87,9 +86,10 @@ describe("Abilities - Perish Song", () => {
}); });
it("should activate if cursola already has perish song, but not reset its counter", async () => { it("should activate if cursola already has perish song, but not reset its counter", async () => {
game.override.enemyMoveset([MoveId.PERISH_SONG, MoveId.AQUA_JET, MoveId.SPLASH]); game.override
game.override.moveset([MoveId.WHIRLWIND, MoveId.SPLASH]); .enemyMoveset([MoveId.PERISH_SONG, MoveId.AQUA_JET, MoveId.SPLASH])
game.override.startingWave(5); .moveset([MoveId.WHIRLWIND, MoveId.SPLASH])
.startingWave(5);
await game.classicMode.startBattle([SpeciesId.CURSOLA]); await game.classicMode.startBattle([SpeciesId.CURSOLA]);
const cursola = game.scene.getPlayerPokemon(); const cursola = game.scene.getPlayerPokemon();

View File

@ -35,8 +35,7 @@ describe("Abilities - POWER CONSTRUCT", () => {
test("check if fainted 50% Power Construct Pokemon switches to base form on arena reset", async () => { test("check if fainted 50% Power Construct Pokemon switches to base form on arena reset", async () => {
const baseForm = 2, const baseForm = 2,
completeForm = 4; completeForm = 4;
game.override.startingWave(4); game.override.startingWave(4).starterForms({
game.override.starterForms({
[SpeciesId.ZYGARDE]: completeForm, [SpeciesId.ZYGARDE]: completeForm,
}); });
@ -62,8 +61,7 @@ describe("Abilities - POWER CONSTRUCT", () => {
test("check if fainted 10% Power Construct Pokemon switches to base form on arena reset", async () => { test("check if fainted 10% Power Construct Pokemon switches to base form on arena reset", async () => {
const baseForm = 3, const baseForm = 3,
completeForm = 5; completeForm = 5;
game.override.startingWave(4); game.override.startingWave(4).starterForms({
game.override.starterForms({
[SpeciesId.ZYGARDE]: completeForm, [SpeciesId.ZYGARDE]: completeForm,
}); });

View File

@ -26,11 +26,12 @@ describe("Abilities - Power Spot", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("double"); game.override
game.override.moveset([MoveId.TACKLE, MoveId.BREAKING_SWIPE, MoveId.SPLASH, MoveId.DAZZLING_GLEAM]); .battleStyle("double")
game.override.enemyMoveset(MoveId.SPLASH); .moveset([MoveId.TACKLE, MoveId.BREAKING_SWIPE, MoveId.SPLASH, MoveId.DAZZLING_GLEAM])
game.override.enemySpecies(SpeciesId.SHUCKLE); .enemyMoveset(MoveId.SPLASH)
game.override.enemyAbility(AbilityId.BALL_FETCH); .enemySpecies(SpeciesId.SHUCKLE)
.enemyAbility(AbilityId.BALL_FETCH);
}); });
it("raises the power of allies' special moves by 30%", async () => { it("raises the power of allies' special moves by 30%", async () => {

View File

@ -108,8 +108,7 @@ describe("Abilities - Protean", () => {
}); });
test("ability applies correctly even if the type has changed by another ability", async () => { test("ability applies correctly even if the type has changed by another ability", async () => {
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).passiveAbility(AbilityId.REFRIGERATE);
game.override.passiveAbility(AbilityId.REFRIGERATE);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -156,8 +155,7 @@ describe("Abilities - Protean", () => {
}); });
test("ability applies correctly even if the pokemon's move misses", async () => { test("ability applies correctly even if the pokemon's move misses", async () => {
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).enemyMoveset(MoveId.SPLASH);
game.override.enemyMoveset(MoveId.SPLASH);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -188,8 +186,7 @@ describe("Abilities - Protean", () => {
}); });
test("ability applies correctly even if the pokemon's move fails because of type immunity", async () => { test("ability applies correctly even if the pokemon's move fails because of type immunity", async () => {
game.override.moveset([MoveId.TACKLE]); game.override.moveset([MoveId.TACKLE]).enemySpecies(SpeciesId.GASTLY);
game.override.enemySpecies(SpeciesId.GASTLY);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
@ -262,8 +259,7 @@ describe("Abilities - Protean", () => {
}); });
test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => { test("ability applies correctly even if the pokemon's Trick-or-Treat fails", async () => {
game.override.moveset([MoveId.TRICK_OR_TREAT]); game.override.moveset([MoveId.TRICK_OR_TREAT]).enemySpecies(SpeciesId.GASTLY);
game.override.enemySpecies(SpeciesId.GASTLY);
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);

View File

@ -23,16 +23,15 @@ describe("Abilities - Quick Draw", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("single"); game.override
.battleStyle("single")
game.override.starterSpecies(SpeciesId.MAGIKARP); .starterSpecies(SpeciesId.MAGIKARP)
game.override.ability(AbilityId.QUICK_DRAW); .ability(AbilityId.QUICK_DRAW)
game.override.moveset([MoveId.TACKLE, MoveId.TAIL_WHIP]); .moveset([MoveId.TACKLE, MoveId.TAIL_WHIP])
.enemyLevel(100)
game.override.enemyLevel(100); .enemySpecies(SpeciesId.MAGIKARP)
game.override.enemySpecies(SpeciesId.MAGIKARP); .enemyAbility(AbilityId.BALL_FETCH)
game.override.enemyAbility(AbilityId.BALL_FETCH); .enemyMoveset([MoveId.TACKLE]);
game.override.enemyMoveset([MoveId.TACKLE]);
vi.spyOn( vi.spyOn(
allAbilities[AbilityId.QUICK_DRAW].getAttrs("BypassSpeedChanceAbAttr")[0], allAbilities[AbilityId.QUICK_DRAW].getAttrs("BypassSpeedChanceAbAttr")[0],
@ -55,8 +54,8 @@ describe("Abilities - Quick Draw", () => {
expect(pokemon.isFainted()).toBe(false); expect(pokemon.isFainted()).toBe(false);
expect(enemy.isFainted()).toBe(true); expect(enemy.isFainted()).toBe(true);
expect(pokemon.waveData.abilitiesApplied).contain(AbilityId.QUICK_DRAW); expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW);
}, 20000); });
test( test(
"does not triggered by non damage moves", "does not triggered by non damage moves",
@ -97,6 +96,6 @@ describe("Abilities - Quick Draw", () => {
expect(pokemon.isFainted()).toBe(true); expect(pokemon.isFainted()).toBe(true);
expect(enemy.isFainted()).toBe(false); expect(enemy.isFainted()).toBe(false);
expect(pokemon.waveData.abilitiesApplied).contain(AbilityId.QUICK_DRAW); expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW);
}, 20000); });
}); });

View File

@ -22,15 +22,14 @@ describe("Abilities - Sand Spit", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("single"); game.override
game.override.disableCrits(); .battleStyle("single")
.disableCrits()
game.override.enemySpecies(SpeciesId.MAGIKARP); .enemySpecies(SpeciesId.MAGIKARP)
game.override.enemyAbility(AbilityId.BALL_FETCH); .enemyAbility(AbilityId.BALL_FETCH)
.starterSpecies(SpeciesId.SILICOBRA)
game.override.starterSpecies(SpeciesId.SILICOBRA); .ability(AbilityId.SAND_SPIT)
game.override.ability(AbilityId.SAND_SPIT); .moveset([MoveId.SPLASH, MoveId.COIL]);
game.override.moveset([MoveId.SPLASH, MoveId.COIL]);
}); });
it("should trigger when hit with damaging move", async () => { it("should trigger when hit with damaging move", async () => {
@ -41,7 +40,7 @@ describe("Abilities - Sand Spit", () => {
await game.toNextTurn(); await game.toNextTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SANDSTORM); expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SANDSTORM);
}, 20000); });
it("should trigger even when fainting", async () => { it("should trigger even when fainting", async () => {
game.override.enemyMoveset([MoveId.TACKLE]).enemyLevel(100).startingLevel(1); game.override.enemyMoveset([MoveId.TACKLE]).enemyLevel(100).startingLevel(1);
@ -62,5 +61,5 @@ describe("Abilities - Sand Spit", () => {
await game.toNextTurn(); await game.toNextTurn();
expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SANDSTORM); expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SANDSTORM);
}, 20000); });
}); });

View File

@ -31,8 +31,7 @@ describe("Abilities - SCHOOLING", () => {
test("check if fainted pokemon switches to base form on arena reset", async () => { test("check if fainted pokemon switches to base form on arena reset", async () => {
const soloForm = 0, const soloForm = 0,
schoolForm = 1; schoolForm = 1;
game.override.startingWave(4); game.override.startingWave(4).starterForms({
game.override.starterForms({
[SpeciesId.WISHIWASHI]: schoolForm, [SpeciesId.WISHIWASHI]: schoolForm,
}); });

View File

@ -24,9 +24,7 @@ describe("Abilities - Screen Cleaner", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleStyle("single"); game.override.battleStyle("single").ability(AbilityId.SCREEN_CLEANER).enemySpecies(SpeciesId.SHUCKLE);
game.override.ability(AbilityId.SCREEN_CLEANER);
game.override.enemySpecies(SpeciesId.SHUCKLE);
}); });
it("removes Aurora Veil", async () => { it("removes Aurora Veil", async () => {

Some files were not shown because too many files have changed in this diff Show More