Merge remote-tracking branch 'upstream/beta' into rest

This commit is contained in:
Bertie690 2025-06-17 15:11:20 -04:00
commit 779c95ba93
364 changed files with 3574 additions and 3135 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

@ -1,6 +1,7 @@
VITE_BYPASS_LOGIN=1 VITE_BYPASS_LOGIN=1
VITE_BYPASS_TUTORIAL=0 VITE_BYPASS_TUTORIAL=0
VITE_SERVER_URL=http://localhost:8001 VITE_SERVER_URL=http://localhost:8001
# IDs for discord/google auth go unused due to VITE_BYPASS_LOGIN
VITE_DISCORD_CLIENT_ID=1234567890 VITE_DISCORD_CLIENT_ID=1234567890
VITE_GOOGLE_CLIENT_ID=1234567890 VITE_GOOGLE_CLIENT_ID=1234567890
VITE_I18N_DEBUG=0 VITE_I18N_DEBUG=0

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

@ -20,7 +20,7 @@
"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 --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

@ -48,7 +48,7 @@ async function promptTestType() {
{ {
type: "list", type: "list",
name: "selectedOption", name: "selectedOption",
message: "What type of test would you like to create:", message: "What type of test would you like to create?",
choices: [...choices.map(choice => ({ name: choice.label, value: choice })), "EXIT"], choices: [...choices.map(choice => ({ name: choice.label, value: choice })), "EXIT"],
}, },
]); ]);

View File

@ -24,7 +24,7 @@ describe("{{description}}", () => {
game.override game.override
.ability(AbilityId.BALL_FETCH) .ability(AbilityId.BALL_FETCH)
.battleStyle("single") .battleStyle("single")
.disableCrits() .criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP) .enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH) .enemyMoveset(MoveId.SPLASH)

View File

@ -4,7 +4,8 @@ import type Pokemon from "#app/field/pokemon";
import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import type { PokemonSpeciesFilter } from "#app/data/pokemon-species"; import type { PokemonSpeciesFilter } from "#app/data/pokemon-species";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { allSpecies } from "#app/data/data-lists";
import { import {
fixedInt, fixedInt,
getIvsFromId, getIvsFromId,
@ -2094,12 +2095,15 @@ export default class BattleScene extends SceneBase {
} }
getMaxExpLevel(ignoreLevelCap = false): number { getMaxExpLevel(ignoreLevelCap = false): number {
if (Overrides.LEVEL_CAP_OVERRIDE > 0) { const capOverride = Overrides.LEVEL_CAP_OVERRIDE ?? 0;
return Overrides.LEVEL_CAP_OVERRIDE; if (capOverride > 0) {
return capOverride;
} }
if (ignoreLevelCap || Overrides.LEVEL_CAP_OVERRIDE < 0) {
if (ignoreLevelCap || capOverride < 0) {
return Number.MAX_SAFE_INTEGER; return Number.MAX_SAFE_INTEGER;
} }
const waveIndex = Math.ceil((this.currentBattle?.waveIndex || 1) / 10) * 10; const waveIndex = Math.ceil((this.currentBattle?.waveIndex || 1) / 10) * 10;
const difficultyWaveIndex = this.gameMode.getWaveForDifficulty(waveIndex); const difficultyWaveIndex = this.gameMode.getWaveForDifficulty(waveIndex);
const baseLevel = (1 + difficultyWaveIndex / 2 + Math.pow(difficultyWaveIndex / 25, 2)) * 1.2; const baseLevel = (1 + difficultyWaveIndex / 2 + Math.pow(difficultyWaveIndex / 25, 2)) * 1.2;
@ -2963,6 +2967,13 @@ export default class BattleScene extends SceneBase {
) { ) {
modifiers.splice(m--, 1); modifiers.splice(m--, 1);
} }
if (
modifier instanceof PokemonHeldItemModifier &&
!isNullOrUndefined(modifier.getSpecies()) &&
!this.getPokemonById(modifier.pokemonId)?.hasSpecies(modifier.getSpecies()!)
) {
modifiers.splice(m--, 1);
}
} }
for (const modifier of modifiers) { for (const modifier of modifiers) {
if (modifier instanceof PersistentModifier) { if (modifier instanceof PersistentModifier) {
@ -3492,17 +3503,13 @@ export default class BattleScene extends SceneBase {
sessionEncounterRate + sessionEncounterRate +
Math.min(currentRunDiffFromAvg * ANTI_VARIANCE_WEIGHT_MODIFIER, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT / 2); Math.min(currentRunDiffFromAvg * ANTI_VARIANCE_WEIGHT_MODIFIER, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT / 2);
const successRate = isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE) const successRate = Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE ?? favoredEncounterRate;
? favoredEncounterRate
: Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE!;
// If the most recent ME was 3 or fewer waves ago, can never spawn a ME // MEs can only spawn 3 or more waves after the previous ME, barring overrides
const canSpawn = const canSpawn =
encounteredEvents.length === 0 || encounteredEvents.length === 0 || waveIndex - encounteredEvents[encounteredEvents.length - 1].waveIndex > 3;
waveIndex - encounteredEvents[encounteredEvents.length - 1].waveIndex > 3 ||
!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE);
if (canSpawn) { if (canSpawn || Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE !== null) {
let roll = MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT; let roll = MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT;
// Always rolls the check on the same offset to ensure no RNG changes from reloading session // Always rolls the check on the same offset to ensure no RNG changes from reloading session
this.executeWithSeedOffset( this.executeWithSeedOffset(

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";
@ -25,22 +24,21 @@ import { allMoves } from "../data-lists";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { TerrainType } from "#app/data/terrain"; import { TerrainType } from "#app/data/terrain";
import { pokemonFormChanges } from "../pokemon-forms";
import { import {
SpeciesFormChangeRevertWeatherFormTrigger,
SpeciesFormChangeWeatherTrigger, SpeciesFormChangeWeatherTrigger,
SpeciesFormChangeAbilityTrigger,
} from "../pokemon-forms/form-change-triggers"; } from "../pokemon-forms/form-change-triggers";
import { SpeciesFormChangeAbilityTrigger } from "../pokemon-forms/form-change-triggers";
import i18next from "i18next"; import i18next from "i18next";
import { Command } from "#enums/command"; 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 {
@ -3970,27 +3971,32 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
this.ability = ability; this.ability = ability;
} }
/**
* Determine if the pokemon has a forme change that is triggered by the weather
*
* @param pokemon - The pokemon with the forme change ability
* @param _passive - unused
* @param _simulated - unused
* @param _args - unused
*/
override canApplyPostSummon(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean { override canApplyPostSummon(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
const isCastformWithForecast = return !!pokemonFormChanges[pokemon.species.speciesId]?.some(
pokemon.species.speciesId === SpeciesId.CASTFORM && this.ability === AbilityId.FORECAST; fc => fc.findTrigger(SpeciesFormChangeWeatherTrigger) && fc.canChange(pokemon),
const isCherrimWithFlowerGift = );
pokemon.species.speciesId === SpeciesId.CHERRIM && this.ability === AbilityId.FLOWER_GIFT;
return isCastformWithForecast || isCherrimWithFlowerGift;
} }
/** /**
* Calls the {@linkcode BattleScene.triggerPokemonFormChange | triggerPokemonFormChange} for both * Trigger the pokemon's forme change by invoking
* {@linkcode SpeciesFormChange.SpeciesFormChangeWeatherTrigger | SpeciesFormChangeWeatherTrigger} and * {@linkcode BattleScene.triggerPokemonFormChange | triggerPokemonFormChange}
* {@linkcode SpeciesFormChange.SpeciesFormChangeWeatherTrigger | SpeciesFormChangeRevertWeatherFormTrigger} if it *
* is the specific Pokemon and ability * @param pokemon - The Pokemon with this ability
* @param {Pokemon} pokemon the Pokemon with this ability * @param _passive - unused
* @param _passive n/a * @param simulated - unused
* @param _args n/a * @param _args - unused
*/ */
override applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): void { override applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): void {
if (!simulated) { if (!simulated) {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeWeatherTrigger); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeWeatherTrigger);
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeRevertWeatherFormTrigger);
} }
} }
} }
@ -4616,7 +4622,7 @@ export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbA
* @param stat The stat being affected * @param stat The stat being affected
* @param cancelled Holds whether the stat change was already prevented. * @param cancelled Holds whether the stat change was already prevented.
* @param args Args[0] is the target pokemon of the stat change. * @param args Args[0] is the target pokemon of the stat change.
* @returns * @returns `true` if the ability can be applied
*/ */
override canApplyPreStatStageChange( override canApplyPreStatStageChange(
_pokemon: Pokemon, _pokemon: Pokemon,
@ -4777,17 +4783,17 @@ export class BlockCritAbAttr extends AbAttr {
} }
/** /**
* Apply the block crit ability by setting the value in the provided boolean holder to false * Apply the block crit ability by setting the value in the provided boolean holder to `true`.
* @param args - [0] is a boolean holder representing whether the attack can crit * @param args - `[0]`: A {@linkcode BooleanHolder} containing whether the attack is prevented from critting.
*/ */
override apply( override apply(
_pokemon: Pokemon, _pokemon: Pokemon,
_passive: boolean, _passive: boolean,
_simulated: boolean, _simulated: boolean,
_cancelled: BooleanHolder, _cancelled: BooleanHolder,
args: [BooleanHolder, ...any], args: [BooleanHolder],
): void { ): void {
args[0].value = false; args[0].value = true;
} }
} }
@ -5300,10 +5306,11 @@ export class PostWeatherChangeFormChangeAbAttr extends PostWeatherChangeAbAttr {
/** /**
* Calls {@linkcode Arena.triggerWeatherBasedFormChangesToNormal | triggerWeatherBasedFormChangesToNormal} when the * Calls {@linkcode Arena.triggerWeatherBasedFormChangesToNormal | triggerWeatherBasedFormChangesToNormal} when the
* weather changed to form-reverting weather, otherwise calls {@linkcode Arena.triggerWeatherBasedFormChanges | triggerWeatherBasedFormChanges} * weather changed to form-reverting weather, otherwise calls {@linkcode Arena.triggerWeatherBasedFormChanges | triggerWeatherBasedFormChanges}
* @param {Pokemon} _pokemon the Pokemon with this ability * @param _pokemon - The Pokemon with this ability
* @param _passive n/a * @param _passive - unused
* @param _weather n/a * @param simulated - unused
* @param _args n/a * @param _weather - unused
* @param _args - unused
*/ */
override applyPostWeatherChange( override applyPostWeatherChange(
_pokemon: Pokemon, _pokemon: Pokemon,
@ -6063,14 +6070,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 +7390,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 +8391,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)
@ -8654,7 +8667,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)
@ -8797,7 +8816,9 @@ export function initAbilities() {
.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(
@ -881,13 +882,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?
} }

File diff suppressed because it is too large Load Diff

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)
@ -267,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(
@ -327,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;
@ -336,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);
} }
@ -378,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;
}
} }
/** /**
@ -414,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 */
@ -660,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,
}); });
} }
@ -985,24 +976,30 @@ 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 (movePhase?.is("MovePhase")) { if (lapseType !== BattlerTagLapseType.PRE_MOVE || !movePhase?.is("MovePhase")) {
return false;
}
const move = movePhase.move.getMove(); const move = movePhase.move.getMove();
const weather = globalScene.arena.weather; const weather = globalScene.arena.weather;
if ( if (
pokemon.getMoveType(move) === PokemonType.FIRE && pokemon.getMoveType(move) !== PokemonType.FIRE ||
!(weather && weather.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed()) (weather?.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed()) // Heavy rain takes priority over powder
) { ) {
movePhase.fail(); return true;
}
// Disable the target's fire type move and damage it (subject to Magic Guard)
movePhase.showMoveText(); movePhase.showMoveText();
movePhase.fail();
const idx = pokemon.getBattlerIndex(); const idx = pokemon.getBattlerIndex();
@ -1016,12 +1013,9 @@ export class PowderTag extends BattlerTag {
// "When the flame touched the powder\non the Pokémon, it exploded!" // "When the flame touched the powder\non the Pokémon, it exploded!"
globalScene.phaseManager.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name })); globalScene.phaseManager.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name }));
}
}
return true; return true;
} }
return super.lapse(pokemon, lapseType);
}
} }
export class NightmareTag extends BattlerTag { export class NightmareTag extends BattlerTag {
@ -1115,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(
@ -1158,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,
),
); );
} }
} }
@ -1884,13 +1872,19 @@ 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 lastMove = pokemon.getLastXMoves()[0];
if (!lastMove) {
// Don't interrupt move if last move was `Moves.NONE` OR no prior move was found
return true;
}
// Interrupt move usage in favor of slacking off
const passive = pokemon.getAbility().id !== AbilityId.TRUANT; const passive = pokemon.getAbility().id !== AbilityId.TRUANT;
const lastMove = pokemon.getLastXMoves().find(() => true);
if (lastMove && lastMove.move !== MoveId.NONE) {
(globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel(); (globalScene.phaseManager.getCurrentPhase() as MovePhase).cancel();
// TODO: Ability displays should be handled by the ability // TODO: Ability displays should be handled by the ability
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true); globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
@ -1900,7 +1894,6 @@ export class TruantTag extends AbilityBattlerTag {
}), }),
); );
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false); globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false);
}
return true; return true;
} }

View File

@ -4,7 +4,8 @@ import i18next from "i18next";
import type { DexAttrProps, GameData } from "#app/system/game-data"; import type { DexAttrProps, GameData } from "#app/system/game-data";
import { defaultStarterSpecies } from "#app/constants"; import { defaultStarterSpecies } from "#app/constants";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { PokemonMove } from "./moves/pokemon-move"; import { PokemonMove } from "./moves/pokemon-move";

View File

@ -5,7 +5,8 @@ import { PlayerPokemon } from "#app/field/pokemon";
import type { Starter } from "#app/ui/starter-select-ui-handler"; import type { Starter } from "#app/ui/starter-select-ui-handler";
import { randSeedGauss, randSeedInt, randSeedItem, getEnumValues } from "#app/utils/common"; import { randSeedGauss, randSeedInt, randSeedItem, getEnumValues } from "#app/utils/common";
import type { PokemonSpeciesForm } from "#app/data/pokemon-species"; import type { PokemonSpeciesForm } from "#app/data/pokemon-species";
import PokemonSpecies, { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import PokemonSpecies, { getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";

View File

@ -1,9 +1,11 @@
import type PokemonSpecies from "#app/data/pokemon-species";
import type { ModifierTypes } from "#app/modifier/modifier-type"; import type { ModifierTypes } from "#app/modifier/modifier-type";
import type { Ability } from "./abilities/ability"; import type { Ability } from "./abilities/ability";
import type Move from "./moves/move"; import type Move from "./moves/move";
export const allAbilities: Ability[] = []; export const allAbilities: Ability[] = [];
export const allMoves: Move[] = []; export const allMoves: Move[] = [];
export const allSpecies: PokemonSpecies[] = [];
// TODO: Figure out what this is used for and provide an appropriate tsdoc comment // TODO: Figure out what this is used for and provide an appropriate tsdoc comment
export const modifierTypes = {} as ModifierTypes; export const modifierTypes = {} as ModifierTypes;

View File

@ -1,7 +1,7 @@
import type BattleScene from "#app/battle-scene"; import type BattleScene from "#app/battle-scene";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { VariantTier } from "#enums/variant-tier"; import { VariantTier } from "#enums/variant-tier";
import { randInt, randomString, randSeedInt, getIvsFromId } from "#app/utils/common"; import { randInt, randomString, randSeedInt, getIvsFromId } from "#app/utils/common";

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

@ -81,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";
@ -3197,7 +3193,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 {
@ -4154,15 +4150,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;
} }
} }
@ -5536,13 +5543,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;
@ -6297,6 +6311,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);
@ -6838,20 +6853,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;
} }
} }
@ -6946,9 +6967,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:
@ -7078,9 +7100,9 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
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;
} }
} }
@ -7099,64 +7121,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() ];
} }
} }
@ -7165,15 +7186,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
@ -7183,6 +7204,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,
@ -7220,23 +7242,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;
}; };
} }
@ -7267,42 +7300,40 @@ 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 maxPp = movesetMove.getMovePp();
const ppLeft = maxPp - movesetMove.ppUsed; const ppLeft = maxPp - movesetMove.ppUsed;
const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5)); const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5));
@ -7310,10 +7341,7 @@ export class ReducePpMoveAttr extends MoveEffectAttr {
return (value / 4) * ppLeft; return (value / 4) * ppLeft;
} }
return value; return value;
}
}
return 0;
} }
} }
@ -7329,40 +7357,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;
} }
@ -7375,14 +7399,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);
@ -7390,8 +7418,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 }));
@ -7429,9 +7458,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;
} }
@ -7454,28 +7483,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);
}; };
} }
} }
@ -7915,19 +7926,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))
}; };
} }
} }
@ -7949,27 +7960,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;
@ -7994,6 +8004,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 -
@ -8006,7 +8017,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)
); );
} }
} }
@ -8014,7 +8025,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
@ -8109,8 +8126,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
@ -8143,6 +8159,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
@ -8156,7 +8173,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;
} }
@ -8198,9 +8216,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;
}; };
} }
} }
@ -8618,9 +8636,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(),
@ -9167,7 +9185,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)
@ -10045,7 +10066,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),
@ -10616,9 +10644,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)
@ -10676,7 +10707,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

@ -22,7 +22,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { BerryModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier"; import { BerryModifier, PokemonInstantReviveModifier } from "#app/modifier/modifier";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { randInt } from "#app/utils/common"; import { randInt } from "#app/utils/common";
@ -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

@ -18,7 +18,7 @@ import {
} from "#app/data/mystery-encounters/mystery-encounter-requirements"; } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { getHighestStatTotalPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getHighestStatTotalPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { EXTORTION_ABILITIES, EXTORTION_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { EXTORTION_ABILITIES, EXTORTION_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";

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";
@ -23,7 +22,7 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { import {
applyAbilityOverrideToPokemon, applyAbilityOverrideToPokemon,
@ -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

@ -19,7 +19,7 @@ import {
getEncounterPokemonLevelForWave, getEncounterPokemonLevelForWave,
STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
@ -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

@ -4,7 +4,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";

View File

@ -15,7 +15,7 @@ import {
updatePlayerMoney, updatePlayerMoney,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";

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";
@ -21,7 +20,7 @@ import {
TypeRequirement, TypeRequirement,
} from "#app/data/mystery-encounters/mystery-encounter-requirements"; } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
@ -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

@ -14,7 +14,7 @@ import { TrainerSlot } from "#enums/trainer-slot";
import type { PlayerPokemon } from "#app/field/pokemon"; import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";

View File

@ -16,7 +16,8 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { allSpecies } from "#app/data/data-lists";
import { getTypeRgb } from "#app/data/type"; import { getTypeRgb } from "#app/data/type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";

View File

@ -1,4 +1,4 @@
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";

View File

@ -14,7 +14,7 @@ import {
getHighestLevelPlayerPokemon, getHighestLevelPlayerPokemon,
koPlayerPokemon, koPlayerPokemon,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";

View File

@ -17,7 +17,7 @@ import { PokeballType } from "#enums/pokeball";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { NumberHolder, randSeedInt } from "#app/utils/common"; import { NumberHolder, randSeedInt } from "#app/utils/common";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { import {
doPlayerFlee, doPlayerFlee,

View File

@ -24,13 +24,14 @@ import { MoveId } from "#enums/move-id";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import { AiType } from "#enums/ai-type"; import { AiType } from "#enums/ai-type";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { BerryType } from "#enums/berry-type"; 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

@ -15,7 +15,7 @@ import { BiomeId } from "#enums/biome-id";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import i18next from "i18next"; import i18next from "i18next";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";

View File

@ -15,7 +15,7 @@ import {
getSpriteKeysFromPokemon, getSpriteKeysFromPokemon,
} from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";

View File

@ -13,7 +13,7 @@ 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";
import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
@ -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

@ -17,7 +17,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";

View File

@ -22,12 +22,13 @@ import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/u
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { BattlerIndex } from "#enums/battler-index"; 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

@ -19,7 +19,8 @@ import type Pokemon from "#app/field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import { NumberHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils/common"; import { NumberHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils/common";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { allSpecies } from "#app/data/data-lists";
import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#app/modifier/modifier"; import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";

View File

@ -1,6 +1,5 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { allAbilities } from "../data-lists"; import { allAbilities } from "../data-lists";
import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { pokemonFormChanges } from "#app/data/pokemon-forms"; import { pokemonFormChanges } from "#app/data/pokemon-forms";
import { SpeciesFormChangeItemTrigger } from "../pokemon-forms/form-change-triggers"; import { SpeciesFormChangeItemTrigger } from "../pokemon-forms/form-change-triggers";
@ -16,7 +15,6 @@ import type { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { SpeciesFormKey } from "#enums/species-form-key";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
export interface EncounterRequirement { export interface EncounterRequirement {
@ -834,70 +832,6 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen
} }
} }
export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement {
requiredEvolutionItem: EvolutionItem[];
minNumberOfPokemon: number;
invertQuery: boolean;
constructor(evolutionItems: EvolutionItem | EvolutionItem[], minNumberOfPokemon = 1, invertQuery = false) {
super();
this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery;
this.requiredEvolutionItem = coerceArray(evolutionItems);
}
override meetsRequirement(): boolean {
const partyPokemon = globalScene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionItem?.length < 0) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
}
filterByEvo(pokemon, evolutionItem) {
if (
pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) &&
pokemonEvolutions[pokemon.species.speciesId].filter(
e => e.item === evolutionItem && (!e.condition || e.condition.predicate(pokemon)),
).length &&
pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX
) {
return true;
}
return (
pokemon.isFusion() &&
pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) &&
pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter(
e => e.item === evolutionItem && (!e.condition || e.condition.predicate(pokemon)),
).length &&
pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX
);
}
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) {
return partyPokemon.filter(
pokemon =>
this.requiredEvolutionItem.filter(evolutionItem => this.filterByEvo(pokemon, evolutionItem)).length > 0,
);
}
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed evolutionItemss
return partyPokemon.filter(
pokemon =>
this.requiredEvolutionItem.filter(evolutionItems => this.filterByEvo(pokemon, evolutionItems)).length === 0,
);
}
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
const requiredItems = this.requiredEvolutionItem.filter(evoItem => this.filterByEvo(pokemon, evoItem));
if (requiredItems.length > 0) {
return ["evolutionItem", EvolutionItem[requiredItems[0]]];
}
return ["evolutionItem", ""];
}
}
export class HeldItemRequirement extends EncounterPokemonRequirement { export class HeldItemRequirement extends EncounterPokemonRequirement {
requiredHeldItemModifiers: string[]; requiredHeldItemModifiers: string[];
minNumberOfPokemon: number; minNumberOfPokemon: number;

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";
@ -49,7 +48,7 @@ import type HeldModifierConfig from "#app/@types/held-modifier-config";
import type { Variant } from "#app/sprites/variant"; import type { Variant } from "#app/sprites/variant";
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 { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { getNatureName } from "#app/data/nature"; import { getNatureName } from "#app/data/nature";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
@ -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

@ -20,7 +20,7 @@ import { PartyUiMode } from "#app/ui/party-ui-handler";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import type { PokemonType } from "#enums/pokemon-type"; import type { PokemonType } from "#enums/pokemon-type";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { import {
getEncounterText, getEncounterText,

View File

@ -42,6 +42,8 @@ import { starterPassiveAbilities } from "#app/data/balance/passives";
import { loadPokemonVariantAssets } from "#app/sprites/pokemon-sprite"; import { loadPokemonVariantAssets } from "#app/sprites/pokemon-sprite";
import { hasExpSprite } from "#app/sprites/sprite-utils"; import { hasExpSprite } from "#app/sprites/sprite-utils";
import { Gender } from "./gender"; import { Gender } from "./gender";
import { allSpecies } from "#app/data/data-lists";
import { getPokemonSpecies } from "#app/utils/pokemon-utils";
export enum Region { export enum Region {
NORMAL, NORMAL,
@ -82,23 +84,6 @@ export const normalForm: SpeciesId[] = [
SpeciesId.CALYREX, SpeciesId.CALYREX,
]; ];
/**
* Gets the {@linkcode PokemonSpecies} object associated with the {@linkcode SpeciesId} enum given
* @param species The species to fetch
* @returns The associated {@linkcode PokemonSpecies} object
*/
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)) {
// Pick a random species from the list
species = species[Math.floor(Math.random() * species.length)];
}
if (species >= 2000) {
return allSpecies.find(s => s.speciesId === species)!; // TODO: is this bang correct?
}
return allSpecies[species - 1];
}
export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): PokemonSpeciesForm { export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): PokemonSpeciesForm {
const retSpecies: PokemonSpecies = const retSpecies: PokemonSpecies =
species >= 2000 species >= 2000
@ -1448,8 +1433,6 @@ export function getPokerusStarters(): PokemonSpecies[] {
return pokerusStarters; return pokerusStarters;
} }
export const allSpecies: PokemonSpecies[] = [];
// biome-ignore format: manually formatted // biome-ignore format: manually formatted
export function initSpecies() { export function initSpecies() {
allSpecies.push( allSpecies.push(

View File

@ -10,7 +10,7 @@ import {
randSeedIntRange, randSeedIntRange,
} from "#app/utils/common"; } from "#app/utils/common";
import { pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import { pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { tmSpecies } from "#app/data/balance/tms"; import { tmSpecies } from "#app/data/balance/tms";
import { doubleBattleDialogue } from "../double-battle-dialogue"; import { doubleBattleDialogue } from "../double-battle-dialogue";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";

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

@ -3,7 +3,7 @@ import type { BiomeTierTrainerPools, PokemonPools } from "#app/data/balance/biom
import { biomePokemonPools, BiomePoolTier, biomeTrainerPools } from "#app/data/balance/biomes"; import { biomePokemonPools, BiomePoolTier, biomeTrainerPools } from "#app/data/balance/biomes";
import { randSeedInt, NumberHolder, isNullOrUndefined, type Constructor } from "#app/utils/common"; import { randSeedInt, NumberHolder, isNullOrUndefined, type Constructor } from "#app/utils/common";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { import {
getTerrainClearMessage, getTerrainClearMessage,
getTerrainStartMessage, getTerrainStartMessage,
@ -765,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

@ -15,12 +15,8 @@ import { allMoves } from "#app/data/data-lists";
import { MoveTarget } from "#enums/MoveTarget"; import { MoveTarget } from "#enums/MoveTarget";
import { MoveCategory } from "#enums/MoveCategory"; import { MoveCategory } from "#enums/MoveCategory";
import type { PokemonSpeciesForm } from "#app/data/pokemon-species"; import type { PokemonSpeciesForm } from "#app/data/pokemon-species";
import { import { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpeciesForm } from "#app/data/pokemon-species";
default as PokemonSpecies, import { getPokemonSpecies } from "#app/utils/pokemon-utils";
getFusedSpeciesName,
getPokemonSpecies,
getPokemonSpeciesForm,
} from "#app/data/pokemon-species";
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
import { import {
NumberHolder, NumberHolder,
@ -79,11 +75,12 @@ import {
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { Status, getRandomStatus } from "#app/data/status-effect"; import { Status, getRandomStatus } from "#app/data/status-effect";
import type { SpeciesFormEvolution, SpeciesEvolutionCondition } from "#app/data/balance/pokemon-evolutions"; import type { SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions";
import { import {
pokemonEvolutions, pokemonEvolutions,
pokemonPrevolutions, pokemonPrevolutions,
FusionSpeciesFormEvolution, FusionSpeciesFormEvolution,
validateShedinjaEvo,
} from "#app/data/balance/pokemon-evolutions"; } from "#app/data/balance/pokemon-evolutions";
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms";
import { import {
@ -185,6 +182,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";
@ -323,7 +321,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;
@ -369,7 +367,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.metWave = dataSource.metWave ?? (this.metBiome === -1 ? -1 : 0); this.metWave = dataSource.metWave ?? (this.metBiome === -1 ? -1 : 0);
this.pauseEvolutions = dataSource.pauseEvolutions; this.pauseEvolutions = dataSource.pauseEvolutions;
this.pokerus = !!dataSource.pokerus; this.pokerus = !!dataSource.pokerus;
this.evoCounter = dataSource.evoCounter ?? 0;
this.fusionSpecies = this.fusionSpecies =
dataSource.fusionSpecies instanceof PokemonSpecies dataSource.fusionSpecies instanceof PokemonSpecies
? dataSource.fusionSpecies ? dataSource.fusionSpecies
@ -1355,8 +1352,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Calculate the critical-hit stage of a move used against this pokemon by * Calculate the critical-hit stage of a move used **against** this pokemon by
* the given source * the given source.
*
* @param source - The {@linkcode Pokemon} using the move * @param source - The {@linkcode Pokemon} using the move
* @param move - The {@linkcode Move} being used * @param move - The {@linkcode Move} being used
* @returns The final critical-hit stage value * @returns The final critical-hit stage value
@ -1369,11 +1367,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyAbAttrs("BonusCritAbAttr", source, null, false, critStage); applyAbAttrs("BonusCritAbAttr", source, null, false, critStage);
const critBoostTag = source.getTag(CritBoostTag); const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) { if (critBoostTag) {
if (critBoostTag instanceof DragonCheerTag) { // Dragon cheer only gives +1 crit stage to non-dragon types
critStage.value += critBoostTag.typesOnAdd.includes(PokemonType.DRAGON) ? 2 : 1; critStage.value +=
} else { critBoostTag instanceof DragonCheerTag && !critBoostTag.typesOnAdd.includes(PokemonType.DRAGON) ? 1 : 2;
critStage.value += 2;
}
} }
console.log(`crit stage: +${critStage.value}`); console.log(`crit stage: +${critStage.value}`);
@ -2518,34 +2514,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) { if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) {
const evolutions = pokemonEvolutions[this.species.speciesId]; const evolutions = pokemonEvolutions[this.species.speciesId];
for (const e of evolutions) { for (const e of evolutions) {
if ( if (e.validate(this)) {
!e.item &&
this.level >= e.level &&
(isNullOrUndefined(e.preFormKey) || this.getFormKey() === e.preFormKey)
) {
if (e.condition === null || (e.condition as SpeciesEvolutionCondition).predicate(this)) {
return e; return e;
} }
} }
} }
}
if (this.isFusion() && this.fusionSpecies && pokemonEvolutions.hasOwnProperty(this.fusionSpecies.speciesId)) { if (this.isFusion() && this.fusionSpecies && pokemonEvolutions.hasOwnProperty(this.fusionSpecies.speciesId)) {
const fusionEvolutions = pokemonEvolutions[this.fusionSpecies.speciesId].map( const fusionEvolutions = pokemonEvolutions[this.fusionSpecies.speciesId].map(
e => new FusionSpeciesFormEvolution(this.species.speciesId, e), e => new FusionSpeciesFormEvolution(this.species.speciesId, e),
); );
for (const fe of fusionEvolutions) { for (const fe of fusionEvolutions) {
if ( if (fe.validate(this)) {
!fe.item &&
this.level >= fe.level &&
(isNullOrUndefined(fe.preFormKey) || this.getFusionFormKey() === fe.preFormKey)
) {
if (fe.condition === null || (fe.condition as SpeciesEvolutionCondition).predicate(this)) {
return fe; return fe;
} }
} }
} }
}
return null; return null;
} }
@ -2784,17 +2768,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
*/ */
public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean {
if (!this.shiny) { if (!this.shiny) {
const shinyThreshold = new NumberHolder(BASE_SHINY_CHANCE); const shinyThreshold = new NumberHolder(thresholdOverride ?? BASE_SHINY_CHANCE);
if (thresholdOverride === undefined || applyModifiersToOverride) { if (applyModifiersToOverride) {
if (thresholdOverride !== undefined && applyModifiersToOverride) {
shinyThreshold.value = thresholdOverride;
}
if (timedEventManager.isEventActive()) { if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier(); shinyThreshold.value *= timedEventManager.getShinyMultiplier();
} }
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold);
} else {
shinyThreshold.value = thresholdOverride;
} }
this.shiny = randSeedInt(65536) < shinyThreshold.value; this.shiny = randSeedInt(65536) < shinyThreshold.value;
@ -2863,16 +2842,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!this.species.abilityHidden) { if (!this.species.abilityHidden) {
return false; return false;
} }
const haThreshold = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); const haThreshold = new NumberHolder(thresholdOverride ?? BASE_HIDDEN_ABILITY_CHANCE);
if (thresholdOverride === undefined || applyModifiersToOverride) { if (applyModifiersToOverride) {
if (thresholdOverride !== undefined && applyModifiersToOverride) {
haThreshold.value = thresholdOverride;
}
if (!this.hasTrainer()) { if (!this.hasTrainer()) {
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, haThreshold); globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, haThreshold);
} }
} else {
haThreshold.value = thresholdOverride;
} }
if (randSeedInt(65536) < haThreshold.value) { if (randSeedInt(65536) < haThreshold.value) {
@ -3136,7 +3110,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) {
@ -3189,7 +3163,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
@ -3887,33 +3861,39 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}; };
} }
/** Calculate whether the given move critically hits this pokemon /**
* Determine whether the given move will score a critical hit **against** this Pokemon.
* @param source - The {@linkcode Pokemon} using the move * @param source - The {@linkcode Pokemon} using the move
* @param move - The {@linkcode Move} being used * @param move - The {@linkcode Move} being used
* @param simulated - If `true`, suppresses changes to game state during calculation (defaults to `true`) * @returns Whether the move will critically hit the defender.
* @returns whether the move critically hits the pokemon
*/ */
getCriticalHitResult(source: Pokemon, move: Move, simulated = true): boolean { getCriticalHitResult(source: Pokemon, move: Move): boolean {
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (move.hasAttr("FixedDamageAttr")) {
const noCritTag = globalScene.arena.getTagOnSide(NoCritTag, defendingSide); // fixed damage moves (Dragon Rage, etc.) will nevet crit
if (noCritTag || Overrides.NEVER_CRIT_OVERRIDE || move.hasAttr("FixedDamageAttr")) {
return false; return false;
} }
const isCritical = new BooleanHolder(false);
if (source.getTag(BattlerTagType.ALWAYS_CRIT)) { const alwaysCrit = new BooleanHolder(false);
isCritical.value = true; applyMoveAttrs("CritOnlyAttr", source, this, move, alwaysCrit);
} applyAbAttrs("ConditionalCritAbAttr", source, null, false, alwaysCrit, this, move);
applyMoveAttrs("CritOnlyAttr", source, this, move, isCritical); const alwaysCritTag = !!source.getTag(BattlerTagType.ALWAYS_CRIT);
applyAbAttrs("ConditionalCritAbAttr", source, null, simulated, isCritical, this, move); const critChance = [24, 8, 2, 1][Phaser.Math.Clamp(this.getCritStage(source, move), 0, 3)];
if (!isCritical.value) {
const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))];
isCritical.value = critChance === 1 || !globalScene.randBattleSeedInt(critChance);
}
applyAbAttrs("BlockCritAbAttr", this, null, simulated, isCritical); let isCritical = alwaysCrit.value || alwaysCritTag || critChance === 1;
return isCritical.value; // If we aren't already guaranteed to crit, do a random roll & check overrides
isCritical ||= Overrides.CRITICAL_HIT_OVERRIDE ?? globalScene.randBattleSeedInt(critChance) === 0;
// apply crit block effects from lucky chant & co., overriding previous effects
const blockCrit = new BooleanHolder(false);
applyAbAttrs("BlockCritAbAttr", this, null, false, blockCrit);
const blockCritTag = globalScene.arena.getTagOnSide(
NoCritTag,
this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY,
);
isCritical &&= !blockCritTag && !blockCrit.value; // need to roll a crit and not be blocked by either crit prevention effect
return isCritical;
} }
/** /**
@ -4325,10 +4305,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(
@ -5489,6 +5500,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
this.turnData.berriesEaten.push(berryType); this.turnData.berriesEaten.push(berryType);
} }
getPersistentTreasureCount(): number {
return (
this.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length +
globalScene.findModifiers(m => m.is("MoneyMultiplierModifier") || m.is("ExtraModifierModifier")).length
);
}
} }
export class PlayerPokemon extends Pokemon { export class PlayerPokemon extends Pokemon {
@ -5827,7 +5845,7 @@ export class PlayerPokemon extends Pokemon {
if (evoSpecies?.speciesId === SpeciesId.NINCADA && evolution.speciesId === SpeciesId.NINJASK) { if (evoSpecies?.speciesId === SpeciesId.NINCADA && evolution.speciesId === SpeciesId.NINJASK) {
const newEvolution = pokemonEvolutions[evoSpecies.speciesId][1]; const newEvolution = pokemonEvolutions[evoSpecies.speciesId][1];
if (newEvolution.condition?.predicate(this)) { if (validateShedinjaEvo()) {
const newPokemon = globalScene.addPlayerPokemon( const newPokemon = globalScene.addPlayerPokemon(
this.species, this.species,
this.level, this.level,
@ -5857,7 +5875,6 @@ export class PlayerPokemon extends Pokemon {
newPokemon.fusionLuck = this.fusionLuck; newPokemon.fusionLuck = this.fusionLuck;
newPokemon.fusionTeraType = this.fusionTeraType; newPokemon.fusionTeraType = this.fusionTeraType;
newPokemon.usedTMs = this.usedTMs; newPokemon.usedTMs = this.usedTMs;
newPokemon.evoCounter = this.evoCounter;
globalScene.getPlayerParty().push(newPokemon); globalScene.getPlayerParty().push(newPokemon);
newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution), evoSpecies); newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution), evoSpecies);
@ -5946,7 +5963,6 @@ export class PlayerPokemon extends Pokemon {
this.fusionGender = pokemon.gender; this.fusionGender = pokemon.gender;
this.fusionLuck = pokemon.luck; this.fusionLuck = pokemon.luck;
this.fusionCustomPokemonData = pokemon.customPokemonData; this.fusionCustomPokemonData = pokemon.customPokemonData;
this.evoCounter = Math.max(pokemon.evoCounter, this.evoCounter);
if (pokemon.pauseEvolutions || this.pauseEvolutions) { if (pokemon.pauseEvolutions || this.pauseEvolutions) {
this.pauseEvolutions = true; this.pauseEvolutions = true;
} }
@ -6015,7 +6031,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;
@ -6102,18 +6118,6 @@ export class EnemyPokemon extends Pokemon {
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0); this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
let prevolution: SpeciesId;
let speciesId = species.speciesId;
while ((prevolution = pokemonPrevolutions[speciesId])) {
const evolution = pokemonEvolutions[prevolution].find(
pe => pe.speciesId === speciesId && (!pe.evoFormKey || pe.evoFormKey === this.getFormKey()),
);
if (evolution?.condition?.enforceFunc) {
evolution.condition.enforceFunc(this);
}
speciesId = prevolution;
}
if (this.hasTrainer() && globalScene.currentBattle) { if (this.hasTrainer() && globalScene.currentBattle) {
const { waveIndex } = globalScene.currentBattle; const { waveIndex } = globalScene.currentBattle;
const ivs: number[] = []; const ivs: number[] = [];
@ -6195,33 +6199,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);
@ -6229,6 +6239,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,
}; };
} }
} }
@ -6236,7 +6247,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: {
@ -6405,14 +6416,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,
}; };
} }
@ -6758,10 +6775,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 {
@ -6780,6 +6796,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;
@ -6899,7 +6921,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;
@ -6921,8 +6942,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

@ -1,7 +1,7 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import type { TrainerConfig } from "#app/data/trainers/trainer-config"; import type { TrainerConfig } from "#app/data/trainers/trainer-config";
import type { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import type { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate";
import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { trainerConfigs } from "#app/data/trainers/trainer-config";

View File

@ -5,7 +5,7 @@ import type { Challenge } from "./data/challenge";
import { allChallenges, applyChallenges, copyChallenge } from "./data/challenge"; import { allChallenges, applyChallenges, copyChallenge } from "./data/challenge";
import { ChallengeType } from "#enums/challenge-type"; import { ChallengeType } from "#enums/challenge-type";
import type PokemonSpecies from "./data/pokemon-species"; import type PokemonSpecies from "./data/pokemon-species";
import { allSpecies } from "./data/pokemon-species"; import { allSpecies } from "#app/data/data-lists";
import type { Arena } from "./field/arena"; import type { Arena } from "./field/arena";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { isNullOrUndefined, randSeedInt, randSeedItem } from "#app/utils/common"; import { isNullOrUndefined, randSeedInt, randSeedItem } from "#app/utils/common";
@ -90,13 +90,14 @@ export class GameMode implements GameModeConfig {
} }
/** /**
* Helper function to get starting level for game mode.
* @returns either: * @returns either:
* - override from overrides.ts * - starting level override from overrides.ts
* - 20 for Daily Runs * - 20 for Daily Runs
* - 5 for all other modes * - 5 for all other modes
*/ */
getStartingLevel(): number { getStartingLevel(): number {
if (Overrides.STARTING_LEVEL_OVERRIDE) { if (Overrides.STARTING_LEVEL_OVERRIDE > 0) {
return Overrides.STARTING_LEVEL_OVERRIDE; return Overrides.STARTING_LEVEL_OVERRIDE;
} }
switch (this.modeId) { switch (this.modeId) {

View File

@ -1218,12 +1218,8 @@ export class EvolutionItemModifierType extends PokemonModifierType implements Ge
(pokemon: PlayerPokemon) => { (pokemon: PlayerPokemon) => {
if ( if (
pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) && pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) &&
pokemonEvolutions[pokemon.species.speciesId].filter( pokemonEvolutions[pokemon.species.speciesId].filter(e => e.validate(pokemon, false, this.evolutionItem))
e => .length &&
e.item === this.evolutionItem &&
(!e.condition || e.condition.predicate(pokemon)) &&
(e.preFormKey === null || e.preFormKey === pokemon.getFormKey()),
).length &&
pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX
) { ) {
return null; return null;
@ -1232,12 +1228,8 @@ export class EvolutionItemModifierType extends PokemonModifierType implements Ge
pokemon.isFusion() && pokemon.isFusion() &&
pokemon.fusionSpecies && pokemon.fusionSpecies &&
pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) &&
pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter( pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter(e => e.validate(pokemon, true, this.evolutionItem))
e => .length &&
e.item === this.evolutionItem &&
(!e.condition || e.condition.predicate(pokemon)) &&
(e.preFormKey === null || e.preFormKey === pokemon.getFusionFormKey()),
).length &&
pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX
) { ) {
return null; return null;
@ -1597,12 +1589,7 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator {
) )
.flatMap(p => { .flatMap(p => {
const evolutions = pokemonEvolutions[p.species.speciesId]; const evolutions = pokemonEvolutions[p.species.speciesId];
return evolutions.filter( return evolutions.filter(e => e.isValidItemEvolution(p));
e =>
e.item !== EvolutionItem.NONE &&
(e.evoFormKey === null || (e.preFormKey || "") === p.getFormKey()) &&
(!e.condition || e.condition.predicate(p)),
);
}), }),
party party
.filter( .filter(
@ -1616,17 +1603,12 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator {
) )
.flatMap(p => { .flatMap(p => {
const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId]; const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId];
return evolutions.filter( return evolutions.filter(e => e.isValidItemEvolution(p, true));
e =>
e.item !== EvolutionItem.NONE &&
(e.evoFormKey === null || (e.preFormKey || "") === p.getFusionFormKey()) &&
(!e.condition || e.condition.predicate(p)),
);
}), }),
] ]
.flat() .flat()
.flatMap(e => e.item) .flatMap(e => e.evoItem)
.filter(i => (!!i && i > 50) === rare); .filter(i => !!i && i > 50 === rare);
if (!evolutionItemPool.length) { if (!evolutionItemPool.length) {
return null; return null;
@ -1892,7 +1874,8 @@ const modifierTypeInitObj = Object.freeze({
new PokemonHeldItemModifierType( new PokemonHeldItemModifierType(
"modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL", "modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL",
"relic_gold", "relic_gold",
(type, args) => new EvoTrackerModifier(type, (args[0] as Pokemon).id, SpeciesId.GIMMIGHOUL, 10), (type, args) =>
new EvoTrackerModifier(type, (args[0] as Pokemon).id, SpeciesId.GIMMIGHOUL, 10, (args[1] as number) ?? 1),
), ),
MEGA_BRACELET: () => MEGA_BRACELET: () =>

View File

@ -772,6 +772,10 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
return this.getMaxHeldItemCount(pokemon); return this.getMaxHeldItemCount(pokemon);
} }
getSpecies(): SpeciesId | null {
return null;
}
abstract getMaxHeldItemCount(pokemon?: Pokemon): number; abstract getMaxHeldItemCount(pokemon?: Pokemon): number;
} }
@ -918,27 +922,14 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
return true; return true;
} }
getIconStackText(virtual?: boolean): Phaser.GameObjects.BitmapText | null { getIconStackText(_virtual?: boolean): Phaser.GameObjects.BitmapText | null {
if (this.getMaxStackCount() === 1 || (virtual && !this.virtualStackCount)) { const pokemon = this.getPokemon();
return null;
}
const pokemon = globalScene.getPokemonById(this.pokemonId); const count = (pokemon?.getPersistentTreasureCount() || 0) + this.getStackCount();
this.stackCount = pokemon const text = globalScene.add.bitmapText(10, 15, "item-count", count.toString(), 11);
? pokemon.evoCounter +
pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length +
globalScene.findModifiers(
m =>
m instanceof MoneyMultiplierModifier ||
m instanceof ExtraModifierModifier ||
m instanceof TempExtraModifierModifier,
).length
: this.stackCount;
const text = globalScene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11);
text.letterSpacing = -0.5; text.letterSpacing = -0.5;
if (this.getStackCount() >= this.required) { if (count >= this.required) {
text.setTint(0xf89890); text.setTint(0xf89890);
} }
text.setOrigin(0, 0); text.setOrigin(0, 0);
@ -946,18 +937,13 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
return text; return text;
} }
getMaxHeldItemCount(pokemon: Pokemon): number { getMaxHeldItemCount(_pokemon: Pokemon): number {
this.stackCount =
pokemon.evoCounter +
pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length +
globalScene.findModifiers(
m =>
m instanceof MoneyMultiplierModifier ||
m instanceof ExtraModifierModifier ||
m instanceof TempExtraModifierModifier,
).length;
return 999; return 999;
} }
override getSpecies(): SpeciesId {
return this.species;
}
} }
/** /**
@ -2402,19 +2388,13 @@ export class EvolutionItemModifier extends ConsumablePokemonModifier {
override apply(playerPokemon: PlayerPokemon): boolean { override apply(playerPokemon: PlayerPokemon): boolean {
let matchingEvolution = pokemonEvolutions.hasOwnProperty(playerPokemon.species.speciesId) let matchingEvolution = pokemonEvolutions.hasOwnProperty(playerPokemon.species.speciesId)
? pokemonEvolutions[playerPokemon.species.speciesId].find( ? pokemonEvolutions[playerPokemon.species.speciesId].find(
e => e => e.evoItem === this.type.evolutionItem && e.validate(playerPokemon, false, e.item!),
e.item === this.type.evolutionItem &&
(e.evoFormKey === null || (e.preFormKey || "") === playerPokemon.getFormKey()) &&
(!e.condition || e.condition.predicate(playerPokemon)),
) )
: null; : null;
if (!matchingEvolution && playerPokemon.isFusion()) { if (!matchingEvolution && playerPokemon.isFusion()) {
matchingEvolution = pokemonEvolutions[playerPokemon.fusionSpecies!.speciesId].find( matchingEvolution = pokemonEvolutions[playerPokemon.fusionSpecies!.speciesId].find(
e => e => e.evoItem === this.type.evolutionItem && e.validate(playerPokemon, true, e.item!),
e.item === this.type.evolutionItem && // TODO: is the bang correct?
(e.evoFormKey === null || (e.preFormKey || "") === playerPokemon.getFusionFormKey()) &&
(!e.condition || e.condition.predicate(playerPokemon)),
); );
if (matchingEvolution) { if (matchingEvolution) {
matchingEvolution = new FusionSpeciesFormEvolution(playerPokemon.species.speciesId, matchingEvolution); matchingEvolution = new FusionSpeciesFormEvolution(playerPokemon.species.speciesId, matchingEvolution);
@ -2934,11 +2914,10 @@ export class MoneyRewardModifier extends ConsumableModifier {
globalScene.getPlayerParty().map(p => { globalScene.getPlayerParty().map(p => {
if (p.species?.speciesId === SpeciesId.GIMMIGHOUL || p.fusionSpecies?.speciesId === SpeciesId.GIMMIGHOUL) { if (p.species?.speciesId === SpeciesId.GIMMIGHOUL || p.fusionSpecies?.speciesId === SpeciesId.GIMMIGHOUL) {
p.evoCounter const factor = Math.min(Math.floor(this.moneyMultiplier), 3);
? (p.evoCounter += Math.min(Math.floor(this.moneyMultiplier), 3))
: (p.evoCounter = Math.min(Math.floor(this.moneyMultiplier), 3));
const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier( const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier(
p, p,
factor,
) as EvoTrackerModifier; ) as EvoTrackerModifier;
globalScene.addModifier(modifier); globalScene.addModifier(modifier);
} }

View File

@ -80,7 +80,11 @@ class DefaultOverrides {
/** Sets the level cap to this number during experience gain calculations. Set to `0` to disable override & use normal wave-based level caps, /** Sets the level cap to this number during experience gain calculations. Set to `0` to disable override & use normal wave-based level caps,
or any negative number to set it to 9 quadrillion (effectively disabling it). */ or any negative number to set it to 9 quadrillion (effectively disabling it). */
readonly LEVEL_CAP_OVERRIDE: number = 0; readonly LEVEL_CAP_OVERRIDE: number = 0;
readonly NEVER_CRIT_OVERRIDE: boolean = false; /**
* If defined, overrides random critical hit rolls to always or never succeed.
* Ignored if the move is guaranteed to always/never crit.
*/
readonly CRITICAL_HIT_OVERRIDE: boolean | null = null;
/** default 1000 */ /** default 1000 */
readonly STARTING_MONEY_OVERRIDE: number = 0; readonly STARTING_MONEY_OVERRIDE: number = 0;
/** Sets all shop item prices to 0 */ /** Sets all shop item prices to 0 */
@ -285,17 +289,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

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

View File

@ -4,7 +4,7 @@ import { globalScene } from "#app/global-scene";
import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { getCharVariantFromDialogue } from "#app/data/dialogue";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { trainerConfigs } from "#app/data/trainers/trainer-config";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { modifierTypes } from "#app/data/data-lists"; import { modifierTypes } from "#app/data/data-lists";

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,20 +65,20 @@ 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.
// TODO: This checks status twice for a single-turn usage...
if (instantCharge.value) { if (instantCharge.value) {
// this MoveEndPhase will be duplicated by the queued MovePhase if not removed
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.getMoveQueue().push({ move: move.id, targets: [this.targetIndex] }); user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useMode: this.useMode });
} }
// Add this move's charging phase to the user's move history // Add this move's charging phase to the user's move history
@ -73,8 +86,9 @@ export class MoveChargePhase extends PokemonPhase {
move: this.move.moveId, move: this.move.moveId,
targets: [this.targetIndex], targets: [this.targetIndex],
result: MoveResult.OTHER, 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);
@ -797,7 +821,7 @@ export class MoveEffectPhase extends PokemonPhase {
* @param effectiveness - The effectiveness of the move against the target * @param effectiveness - The effectiveness of the move against the target
*/ */
protected applyMoveDamage(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): HitResult { protected applyMoveDamage(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): HitResult {
const isCritical = target.getCriticalHitResult(user, this.move, false); const isCritical = target.getCriticalHitResult(user, this.move);
/* /*
* Apply stat changes from {@linkcode move} and gives it to {@linkcode source} * Apply stat changes from {@linkcode move} and gives it to {@linkcode source}

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,9 +119,10 @@ 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();
@ -142,21 +136,21 @@ export class MovePhase extends BattlePhase {
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 this.move.getMove().doesFlagEffectApply({
.getMove() flag: MoveFlags.IGNORE_ABILITIES,
.doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user: this.pokemon, isFollowUp: this.followUp }) user: this.pokemon,
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();
@ -188,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();
@ -201,18 +195,34 @@ 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)
if (!this.pokemon.status?.effect || this.pokemon.status.isPostTurn() || isIgnoreStatus(this.useMode)) {
return;
}
if (
this.useMode === MoveUseMode.INDIRECT &&
[StatusEffect.SLEEP, StatusEffect.FREEZE].includes(this.pokemon.status.effect)
) {
// Dancer thaws out or wakes up a frozen/sleeping user prior to use
this.pokemon.resetStatus(false);
return;
}
this.pokemon.status.incrementTurn(); this.pokemon.status.incrementTurn();
/** Whether to prevent us from using the move */
let activated = false; let activated = false;
/** Whether to cure the status */
let healed = false; let healed = false;
switch (this.pokemon.status.effect) { switch (this.pokemon.status.effect) {
case StatusEffect.PARALYSIS: case StatusEffect.PARALYSIS:
activated = activated =
(!this.pokemon.randBattleSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && (this.pokemon.randBattleSeedInt(4) === 0 || Overrides.STATUS_ACTIVATION_OVERRIDE === true) &&
Overrides.STATUS_ACTIVATION_OVERRIDE !== false; Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
break; break;
case StatusEffect.SLEEP: { case StatusEffect.SLEEP: {
@ -236,10 +246,7 @@ export class MovePhase extends BattlePhase {
!!this.move !!this.move
.getMove() .getMove()
.findAttr( .findAttr(
attr => attr => attr.is("HealStatusEffectAttr") && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE),
attr.is("HealStatusEffectAttr") &&
attr.selfTarget &&
(attr as unknown as HealStatusEffectAttr).isOfEffect(StatusEffect.FREEZE),
) || ) ||
(!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) || (!this.pokemon.randBattleSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) ||
Overrides.STATUS_ACTIVATION_OVERRIDE === false; Overrides.STATUS_ACTIVATION_OVERRIDE === false;
@ -249,6 +256,7 @@ export class MovePhase extends BattlePhase {
} }
if (activated) { if (activated) {
// Cancel move activation and play effect
this.cancel(); this.cancel();
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)), getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
@ -257,27 +265,28 @@ export class MovePhase extends BattlePhase {
"CommonAnimPhase", "CommonAnimPhase",
this.pokemon.getBattlerIndex(), this.pokemon.getBattlerIndex(),
undefined, undefined,
CommonAnim.POISON + (this.pokemon.status.effect - 1), CommonAnim.POISON + (this.pokemon.status.effect - 1), // offset anim # by effect #
); );
} else if (healed) { } else if (healed) {
// cure status and play effect
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)), getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
); );
// cannot use `unshiftPhase` as it will cause status to be reset _after_ move condition checks fire // cannot use `asPhase=true` as it will cause status to be reset _after_ move condition checks fire
this.pokemon.resetStatus(false, false, false, false); this.pokemon.resetStatus(false, false, false, false);
} }
} }
}
/** /**
* 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);
} }
} }
@ -285,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);
@ -329,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);
} }
if (!isIgnorePP(this.useMode)) {
// "commit" to using the move, deducting PP. // "commit" to using the move, deducting PP.
if (!this.ignorePp) {
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));
} }
/** /**
@ -357,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?
@ -394,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)) {
@ -406,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);
@ -426,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);
}); });
@ -439,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)) {
// Protean and Libero apply on the charging turn of charge moves
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
this.showMoveText(); this.showMoveText();
globalScene.phaseManager.unshiftNew(
"MoveChargePhase", // Conditions currently assume single target
this.pokemon.getBattlerIndex(), // TODO: Is this sustainable?
this.targets[0], if (!move.applyConditions(this.pokemon, targets[0], move)) {
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);
@ -464,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,
);
} }
/** /**
@ -475,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();
@ -607,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();
@ -624,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);
@ -647,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

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

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

@ -3,7 +3,7 @@ import { applyChallenges } from "#app/data/challenge";
import { ChallengeType } from "#enums/challenge-type"; import { ChallengeType } from "#enums/challenge-type";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms/form-change-triggers";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier"; import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { Phase } from "#app/phase"; import { Phase } from "#app/phase";

View File

@ -63,7 +63,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);
@ -124,6 +124,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();
@ -157,44 +159,38 @@ export class TurnStartPhase extends FieldPhase {
} }
switch (turnCommand?.command) { switch (turnCommand?.command) {
case Command.FIGHT: case Command.FIGHT: {
{
const queuedMove = turnCommand.move; const queuedMove = turnCommand.move;
pokemon.turnData.order = orderIndex++; pokemon.turnData.order = orderIndex++;
if (!queuedMove) { if (!queuedMove) {
continue; continue;
} }
const move = const move =
pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) || pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ??
new PokemonMove(queuedMove.move); new PokemonMove(queuedMove.move);
if (move.getMove().hasAttr("MoveHeaderAttr")) { if (move.getMove().hasAttr("MoveHeaderAttr")) {
phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move); phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move);
} }
if (pokemon.isPlayer()) {
if (turnCommand.cursor === -1) { if (pokemon.isPlayer() && turnCommand.cursor === -1) {
phaseManager.pushNew("MovePhase", pokemon, turnCommand.targets || turnCommand.move!.targets, move);
} else {
phaseManager.pushNew( phaseManager.pushNew(
"MovePhase", "MovePhase",
pokemon, pokemon,
turnCommand.targets || turnCommand.move!.targets, // TODO: is the bang correct here? turnCommand.targets || turnCommand.move!.targets,
move, move,
false, turnCommand.move!.useMode,
queuedMove.ignorePP, ); //TODO: is the bang correct here?
);
}
} else { } else {
phaseManager.pushNew( phaseManager.pushNew(
"MovePhase", "MovePhase",
pokemon, pokemon,
turnCommand.targets || turnCommand.move!.targets, turnCommand.targets || turnCommand.move!.targets,
move, move,
false, queuedMove.useMode,
queuedMove.ignorePP, ); // TODO: is the bang correct here?
);
}
} }
break; break;
}
case Command.BALL: case Command.BALL:
phaseManager.unshiftNew("AttemptCapturePhase", turnCommand.targets![0] % 2, turnCommand.cursor!); //TODO: is the bang correct here? phaseManager.unshiftNew("AttemptCapturePhase", turnCommand.targets![0] % 2, turnCommand.cursor!); //TODO: is the bang correct here?
break; break;

View File

@ -6,7 +6,8 @@ import type { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { allSpecies } from "#app/data/data-lists";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { randInt, getEnumKeys, isLocal, executeIf, fixedInt, randSeedItem, NumberHolder } from "#app/utils/common"; import { randInt, getEnumKeys, isLocal, executeIf, fixedInt, randSeedItem, NumberHolder } from "#app/utils/common";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";

View File

@ -3,7 +3,8 @@ import { globalScene } from "#app/global-scene";
import type { Gender } from "../data/gender"; import type { Gender } from "../data/gender";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species"; import { getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { Status } from "../data/status-effect"; import { Status } from "../data/status-effect";
import Pokemon, { EnemyPokemon, PokemonBattleData, PokemonSummonData } from "../field/pokemon"; import Pokemon, { EnemyPokemon, PokemonBattleData, PokemonSummonData } from "../field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
@ -45,7 +46,6 @@ export default class PokemonData {
public pauseEvolutions: boolean; public pauseEvolutions: boolean;
public pokerus: boolean; public pokerus: boolean;
public usedTMs: MoveId[]; public usedTMs: MoveId[];
public evoCounter: number;
public teraType: PokemonType; public teraType: PokemonType;
public isTerastallized: boolean; public isTerastallized: boolean;
public stellarTypesBoosted: PokemonType[]; public stellarTypesBoosted: PokemonType[];
@ -118,7 +118,6 @@ export default class PokemonData {
this.pauseEvolutions = !!source.pauseEvolutions; this.pauseEvolutions = !!source.pauseEvolutions;
this.pokerus = !!source.pokerus; this.pokerus = !!source.pokerus;
this.usedTMs = source.usedTMs ?? []; this.usedTMs = source.usedTMs ?? [];
this.evoCounter = source.evoCounter ?? 0;
this.teraType = source.teraType as PokemonType; this.teraType = source.teraType as PokemonType;
this.isTerastallized = !!source.isTerastallized; this.isTerastallized = !!source.isTerastallized;
this.stellarTypesBoosted = source.stellarTypesBoosted ?? []; this.stellarTypesBoosted = source.stellarTypesBoosted ?? [];

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

@ -3,7 +3,7 @@ import type { SystemSaveData, SessionSaveData } from "#app/system/game-data";
import { defaultStarterSpecies } from "#app/constants"; import { defaultStarterSpecies } from "#app/constants";
import { AbilityAttr } from "#enums/ability-attr"; import { AbilityAttr } from "#enums/ability-attr";
import { DexAttr } from "#enums/dex-attr"; import { DexAttr } from "#enums/dex-attr";
import { allSpecies } from "#app/data/pokemon-species"; import { allSpecies } from "#app/data/data-lists";
import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { isNullOrUndefined } from "#app/utils/common"; import { isNullOrUndefined } from "#app/utils/common";
import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator"; import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator";

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

@ -1,6 +1,7 @@
import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator"; import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator";
import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator"; import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator";
import { getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { SessionSaveData, SystemSaveData } from "#app/system/game-data"; import type { SessionSaveData, SystemSaveData } from "#app/system/game-data";
import { DexAttr } from "#enums/dex-attr"; import { DexAttr } from "#enums/dex-attr";

View File

@ -1,5 +1,5 @@
import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator"; import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import type { SystemSaveData } from "#app/system/game-data"; import type { SystemSaveData } from "#app/system/game-data";
import { DexAttr } from "#enums/dex-attr"; import { DexAttr } from "#enums/dex-attr";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";

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;
switch (this.confirmUiMode) {
case ConfirmUiMode.DEFAULT_YES:
this.setCursor(this.switchCheck ? this.switchCheckCursor : 0); 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

@ -5,7 +5,7 @@ import { getEnumValues, getEnumKeys, fixedInt, randSeedShuffle } from "#app/util
import type { IEggOptions } from "../data/egg"; import type { IEggOptions } from "../data/egg";
import { Egg, getLegendaryGachaSpeciesForTimestamp } from "../data/egg"; import { Egg, getLegendaryGachaSpeciesForTimestamp } from "../data/egg";
import { VoucherType, getVoucherTypeIcon } from "../system/voucher"; import { VoucherType, getVoucherTypeIcon } from "../system/voucher";
import { getPokemonSpecies } from "../data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { addWindow } from "./ui-theme"; import { addWindow } from "./ui-theme";
import { Tutorial, handleTutorial } from "../tutorial"; import { Tutorial, handleTutorial } from "../tutorial";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";

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,32 +140,41 @@ 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
if ( // TODO: Should we allow showing the summary menu at least?
(globalScene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(this.fromCommand, cursor, false)
) {
success = true;
} else {
ui.playError();
}
} else {
// Cannot back out of fight menu if skipToFightInput is enabled
const { battleType, mysteryEncounter } = globalScene.currentBattle; const { battleType, mysteryEncounter } = globalScene.currentBattle;
if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) { if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) {
ui.setMode(UiMode.COMMAND, this.fieldIndex); ui.setMode(UiMode.COMMAND, this.fieldIndex);
success = true; success = true;
} }
} }
break;
case Button.ACTION:
if (
(globalScene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(
this.fromCommand,
cursor,
MoveUseMode.NORMAL,
)
) {
success = true;
} else { } else {
switch (button) { ui.playError();
}
break;
case Button.UP: case Button.UP:
if (cursor >= 2) { if (cursor >= 2) {
success = this.setCursor(cursor - 2); success = this.setCursor(cursor - 2);
@ -185,7 +195,8 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
success = this.setCursor(cursor + 1); success = this.setCursor(cursor + 1);
} }
break; break;
} default:
// other inputs do nothing while in fight menu
} }
if (success) { if (success) {

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

View File

@ -16,7 +16,9 @@ import { pokemonFormChanges } from "#app/data/pokemon-forms";
import type { LevelMoves } from "#app/data/balance/pokemon-level-moves"; import type { LevelMoves } from "#app/data/balance/pokemon-level-moves";
import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { allSpecies, getPokemonSpecies, getPokemonSpeciesForm, normalForm } from "#app/data/pokemon-species"; import { getPokemonSpeciesForm, normalForm } from "#app/data/pokemon-species";
import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { allSpecies } from "#app/data/data-lists";
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
import { starterPassiveAbilities } from "#app/data/balance/passives"; import { starterPassiveAbilities } from "#app/data/balance/passives";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";

View File

@ -8,7 +8,7 @@ import { UiMode } from "#enums/ui-mode";
import { FilterTextRow } from "./filter-text"; import { FilterTextRow } from "./filter-text";
import { allAbilities } from "#app/data/data-lists"; import { allAbilities } from "#app/data/data-lists";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { allSpecies } from "#app/data/pokemon-species"; import { allSpecies } from "#app/data/data-lists";
import i18next from "i18next"; import i18next from "i18next";
export default class PokedexScanUiHandler extends FormModalUiHandler { export default class PokedexScanUiHandler extends FormModalUiHandler {

View File

@ -7,7 +7,8 @@ import { speciesEggMoves } from "#app/data/balance/egg-moves";
import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves";
import type { PokemonForm } from "#app/data/pokemon-species"; import type { PokemonForm } from "#app/data/pokemon-species";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { allSpecies, getPokemonSpeciesForm, getPokerusStarters, normalForm } from "#app/data/pokemon-species"; import { getPokemonSpeciesForm, getPokerusStarters, normalForm } from "#app/data/pokemon-species";
import { allSpecies } from "#app/data/data-lists";
import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters";
import { catchableSpecies } from "#app/data/balance/biomes"; import { catchableSpecies } from "#app/data/balance/biomes";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";

View File

@ -19,7 +19,8 @@ import { pokemonFormChanges } from "#app/data/pokemon-forms";
import type { LevelMoves } from "#app/data/balance/pokemon-level-moves"; import type { LevelMoves } from "#app/data/balance/pokemon-level-moves";
import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves";
import type PokemonSpecies from "#app/data/pokemon-species"; import type PokemonSpecies from "#app/data/pokemon-species";
import { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data/pokemon-species"; import { getPokemonSpeciesForm, getPokerusStarters } from "#app/data/pokemon-species";
import { allSpecies } from "#app/data/data-lists";
import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { GameModes } from "#enums/game-modes"; import { GameModes } from "#enums/game-modes";

View File

@ -9,7 +9,7 @@ import { version } from "../../package.json";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { SpeciesId } from "#enums/species-id"; import type { SpeciesId } from "#enums/species-id";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";

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

@ -0,0 +1,21 @@
import { allSpecies } from "#app/data/data-lists";
import type PokemonSpecies from "#app/data/pokemon-species";
import type { SpeciesId } from "#enums/species-id";
/**
* Gets the {@linkcode PokemonSpecies} object associated with the {@linkcode SpeciesId} enum given
* @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
*/
export function getPokemonSpecies(species: SpeciesId | SpeciesId[]): PokemonSpecies {
if (Array.isArray(species)) {
// TODO: this RNG roll should not be handled by this function
species = species[Math.floor(Math.random() * species.length)];
}
if (species >= 2000) {
return allSpecies.find(s => s.speciesId === species)!; // TODO: is this bang correct?
}
return allSpecies[species - 1];
}

View File

@ -27,7 +27,7 @@ describe("Ability Activation Order", () => {
.moveset([MoveId.SPLASH]) .moveset([MoveId.SPLASH])
.ability(AbilityId.BALL_FETCH) .ability(AbilityId.BALL_FETCH)
.battleStyle("single") .battleStyle("single")
.disableCrits() .criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP) .enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH); .enemyMoveset(MoveId.SPLASH);

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

@ -27,7 +27,7 @@ describe("Abilities - Analytic", () => {
.moveset([MoveId.SPLASH, MoveId.TACKLE]) .moveset([MoveId.SPLASH, MoveId.TACKLE])
.ability(AbilityId.ANALYTIC) .ability(AbilityId.ANALYTIC)
.battleStyle("single") .battleStyle("single")
.disableCrits() .criticalHits(false)
.startingLevel(200) .startingLevel(200)
.enemyLevel(200) .enemyLevel(200)
.enemySpecies(SpeciesId.SNORLAX) .enemySpecies(SpeciesId.SNORLAX)

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

@ -35,7 +35,7 @@ describe("Abilities - Commander", () => {
.moveset([MoveId.LIQUIDATION, MoveId.MEMENTO, MoveId.SPLASH, MoveId.FLIP_TURN]) .moveset([MoveId.LIQUIDATION, MoveId.MEMENTO, MoveId.SPLASH, MoveId.FLIP_TURN])
.ability(AbilityId.COMMANDER) .ability(AbilityId.COMMANDER)
.battleStyle("double") .battleStyle("double")
.disableCrits() .criticalHits(false)
.enemySpecies(SpeciesId.SNORLAX) .enemySpecies(SpeciesId.SNORLAX)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.TACKLE); .enemyMoveset(MoveId.TACKLE);

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,7 +24,7 @@ describe("Abilities - Corrosion", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.battleStyle("single") .battleStyle("single")
.disableCrits() .criticalHits(false)
.enemySpecies(SpeciesId.GRIMER) .enemySpecies(SpeciesId.GRIMER)
.enemyAbility(AbilityId.CORROSION) .enemyAbility(AbilityId.CORROSION)
.enemyMoveset(MoveId.TOXIC); .enemyMoveset(MoveId.TOXIC);

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

@ -33,7 +33,7 @@ describe("Abilities - Cud Chew", () => {
.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]) .startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }])
.ability(AbilityId.CUD_CHEW) .ability(AbilityId.CUD_CHEW)
.battleStyle("single") .battleStyle("single")
.disableCrits() .criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP) .enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH); .enemyMoveset(MoveId.SPLASH);

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,9 +145,10 @@ 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)
.starterForms({
[SpeciesId.MIMIKYU]: bustedForm, [SpeciesId.MIMIKYU]: bustedForm,
}); });
@ -168,9 +166,10 @@ 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)
.starterForms({
[SpeciesId.MIMIKYU]: bustedForm, [SpeciesId.MIMIKYU]: bustedForm,
}); });
@ -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

@ -23,7 +23,7 @@ describe("Abilities - Dry Skin", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.battleStyle("single") .battleStyle("single")
.disableCrits() .criticalHits(false)
.enemyAbility(AbilityId.DRY_SKIN) .enemyAbility(AbilityId.DRY_SKIN)
.enemyMoveset(MoveId.SPLASH) .enemyMoveset(MoveId.SPLASH)
.enemySpecies(SpeciesId.CHARMANDER) .enemySpecies(SpeciesId.CHARMANDER)

View File

@ -28,7 +28,7 @@ describe("Abilities - Early Bird", () => {
.moveset([MoveId.REST, MoveId.BELLY_DRUM, MoveId.SPLASH]) .moveset([MoveId.REST, MoveId.BELLY_DRUM, MoveId.SPLASH])
.ability(AbilityId.EARLY_BIRD) .ability(AbilityId.EARLY_BIRD)
.battleStyle("single") .battleStyle("single")
.disableCrits() .criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP) .enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH); .enemyMoveset(MoveId.SPLASH);

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